1
1
Fork 0
mirror of https://github.com/OJ/gobuster.git synced 2024-05-19 02:36:02 +02:00

implement VHOST bruteforcing

This commit is contained in:
Christian Mehlmauer 2018-12-11 21:41:42 +01:00 committed by OJ
parent ef6e9b00c1
commit 59029be1e8
No known key found for this signature in database
GPG Key ID: D5DC61FB93260597
8 changed files with 513 additions and 49 deletions

View File

@ -43,6 +43,7 @@ All funds that are donated to this project will be donated to charity. A full lo
```text
dir uses dir mode
dns uses dns mode
vhost uses vhost mode
```
## Common Command line options
@ -91,6 +92,21 @@ dns uses dns mode
--wildcard Force continued operation when wildcard found
```
## Command line options for `vhost` mode
```text
-c, --cookies string Cookies to use for the requests
-h, --help help for vhost
-k, --insecuressl Skip SSL certificate verification
-P, --password string Password for Basic Auth
-p, --proxy string Proxy to use for requests [http(s)://host:port]
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster 2.0.1")
-U, --username string Username for Basic Auth
--wildcard Force continued operation when wildcard found
```
## Easy Installation
### Binary Releases
@ -435,6 +451,38 @@ Found: test.127.0.0.1.xip.io
=====================================================
```
### `vhost` mode
Command line might look like this:
```bash
gobuster vhost -u https://mysite.com -w common-vhosts.txt
```
Normal sample run goes like this:
```bash
gobuster vhost -u https://mysite.com -w common-vhosts.txt
=====================================================
Gobuster v2.0.1 OJ Reeves (@TheColonial)
=====================================================
[+] Url: https://mysite.com
[+] Threads: 10
[+] Wordlist: common-vhosts.txt
[+] User Agent: gobuster 2.0.1
[+] Timeout: 10s
=====================================================
2018/10/09 08:36:00 Starting gobuster
=====================================================
Found: www.mysite.com
Found: piwik.mysite.com
Found: mail.mysite.com
=====================================================
2018/10/09 08:36:05 Finished
=====================================================
```
## License
See the LICENSE file.

178
cli/cmd/vhost.go Normal file
View File

@ -0,0 +1,178 @@
package cmd
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/OJ/gobuster/cli"
"github.com/OJ/gobuster/gobustervhost"
"github.com/OJ/gobuster/libgobuster"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
var cmdVhost *cobra.Command
func runVhost(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseVhostOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
plugin, err := gobustervhost.NewGobusterVhost(ctx, globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobustervhost: %v", err)
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
for range signalChan {
// caught CTRL+C
if !globalopts.Quiet {
fmt.Println("\n[!] Keyboard interrupt detected, terminating.")
}
cancel()
}
}()
if err := cli.Gobuster(ctx, globalopts, plugin); err != nil {
return fmt.Errorf("error on running goubster: %v", err)
}
return nil
}
func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
var plugin gobustervhost.OptionsVhost
url, err := cmdVhost.Flags().GetString("url")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for url: %v", err)
}
plugin.URL = url
if !strings.HasPrefix(plugin.URL, "http") {
// check to see if a port was specified
re := regexp.MustCompile(`^[^/]+:(\d+)`)
match := re.FindStringSubmatch(plugin.URL)
if len(match) < 2 {
// no port, default to http on 80
plugin.URL = fmt.Sprintf("http://%s", plugin.URL)
} else {
port, err2 := strconv.Atoi(match[1])
if err2 != nil || (port != 80 && port != 443) {
return nil, nil, fmt.Errorf("url scheme not specified")
} else if port == 80 {
plugin.URL = fmt.Sprintf("http://%s", plugin.URL)
} else {
plugin.URL = fmt.Sprintf("https://%s", plugin.URL)
}
}
}
cookies, err := cmdVhost.Flags().GetString("cookies")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for cookies: %v", err)
}
plugin.Cookies = cookies
username, err := cmdVhost.Flags().GetString("username")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for username: %v", err)
}
plugin.Username = username
password, err := cmdVhost.Flags().GetString("password")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for password: %v", err)
}
plugin.Password = password
useragent, err := cmdVhost.Flags().GetString("useragent")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for useragent: %v", err)
}
plugin.UserAgent = useragent
proxy, err := cmdVhost.Flags().GetString("proxy")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for proxy: %v", err)
}
plugin.Proxy = proxy
timeout, err := cmdVhost.Flags().GetDuration("timeout")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for timeout: %v", err)
}
plugin.Timeout = timeout
followredirect, err := cmdDir.Flags().GetBool("followredirect")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for followredirect: %v", err)
}
plugin.FollowRedirect = followredirect
insecuressl, err := cmdVhost.Flags().GetBool("insecuressl")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for insecuressl: %v", err)
}
plugin.InsecureSSL = insecuressl
// Prompt for PW if not provided
if plugin.Username != "" && plugin.Password == "" {
fmt.Printf("[?] Auth Password: ")
passBytes, err := terminal.ReadPassword(int(syscall.Stdin))
// print a newline to simulate the newline that was entered
// this means that formatting/printing after doesn't look bad.
fmt.Println("")
if err != nil {
return nil, nil, fmt.Errorf("username given but reading of password failed")
}
plugin.Password = string(passBytes)
}
// if it's still empty bail out
if plugin.Username != "" && plugin.Password == "" {
return nil, nil, fmt.Errorf("username was provided but password is missing")
}
return globalopts, &plugin, nil
}
func init() {
cmdVhost = &cobra.Command{
Use: "vhost",
Short: "Uses VHOST bruteforcing mode",
RunE: runVhost,
}
cmdVhost.Flags().StringP("url", "u", "", "The target URL")
cmdVhost.Flags().StringP("cookies", "c", "", "Cookies to use for the requests")
cmdVhost.Flags().StringP("username", "U", "", "Username for Basic Auth")
cmdVhost.Flags().StringP("password", "P", "", "Password for Basic Auth")
cmdVhost.Flags().StringP("useragent", "a", libgobuster.DefaultUserAgent(), "Set the User-Agent string")
cmdVhost.Flags().StringP("proxy", "p", "", "Proxy to use for requests [http(s)://host:port]")
cmdVhost.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout")
cmdVhost.Flags().BoolP("followredirect", "r", true, "Follow redirects")
cmdVhost.Flags().BoolP("insecuressl", "k", false, "Skip SSL certificate verification")
if err := cmdVhost.MarkFlagRequired("url"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
cmdVhost.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdVhost)
}

View File

@ -17,13 +17,13 @@ import (
type GobusterDir struct {
options *OptionsDir
globalopts *libgobuster.Options
http *httpClient
http *libgobuster.HTTPClient
}
// GetRequest issues a GET request to the target and returns
// the status code, length and an error
func (d *GobusterDir) get(url string) (*int, *int64, error) {
return d.http.makeRequest(url, d.options.Cookies)
return d.http.Get(url, "", d.options.Cookies)
}
// NewGobusterDir creates a new initialized GobusterDir
@ -40,7 +40,18 @@ func NewGobusterDir(cont context.Context, globalopts *libgobuster.Options, opts
options: opts,
globalopts: globalopts,
}
h, err := newHTTPClient(cont, opts)
httpOpts := libgobuster.HTTPOptions{
Proxy: opts.Proxy,
FollowRedirect: opts.FollowRedirect,
InsecureSSL: opts.InsecureSSL,
Timeout: opts.Timeout,
Username: opts.Username,
Password: opts.Password,
UserAgent: opts.UserAgent,
}
h, err := libgobuster.NewHTTPClient(cont, &httpOpts)
if err != nil {
return nil, err
}
@ -68,7 +79,6 @@ func (d *GobusterDir) PreRun() error {
}
if d.options.StatusCodesParsed.Contains(*wildcardResp) {
d.options.IsWildcard = true
log.Printf("[-] Wildcard response found: %s => %d", url, *wildcardResp)
if !d.options.WildcardForced {
return fmt.Errorf("To force processing of Wildcard responses, specify the '--wildcard' switch.")

View File

@ -25,7 +25,6 @@ type OptionsDir struct {
NoStatus bool
InsecureSSL bool
UseSlash bool
IsWildcard bool
WildcardForced bool
}

View File

@ -0,0 +1,172 @@
package gobustervhost
import (
"bufio"
"bytes"
"context"
"fmt"
"net/url"
"strings"
"text/tabwriter"
"github.com/OJ/gobuster/libgobuster"
)
// GobusterVhost is the main type to implement the interface
type GobusterVhost struct {
options *OptionsVhost
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
domain string
baseResponse string
}
// NewGobusterVhost creates a new initialized GobusterDir
func NewGobusterVhost(cont context.Context, globalopts *libgobuster.Options, opts *OptionsVhost) (*GobusterVhost, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterVhost{
options: opts,
globalopts: globalopts,
}
httpOpts := libgobuster.HTTPOptions{
Proxy: opts.Proxy,
FollowRedirect: opts.FollowRedirect,
InsecureSSL: opts.InsecureSSL,
Timeout: opts.Timeout,
Username: opts.Username,
Password: opts.Password,
UserAgent: opts.UserAgent,
}
h, err := libgobuster.NewHTTPClient(cont, &httpOpts)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// PreRun is the pre run implementation of gobusterdir
func (v *GobusterVhost) PreRun() error {
// add trailing slash
if !strings.HasSuffix(v.options.URL, "/") {
v.options.URL = fmt.Sprintf("%s/", v.options.URL)
}
url, err := url.Parse(v.options.URL)
if err != nil {
return fmt.Errorf("invalid url %s: %v", v.options.URL, err)
}
v.domain = url.Host
_, bodyBase, err := v.http.GetBody(v.options.URL, "", v.options.Cookies)
if err != nil {
return fmt.Errorf("unable to connect to %s: %v", v.options.URL, err)
}
v.baseResponse = *bodyBase
return nil
}
// Run is the process implementation of gobusterdir
func (v *GobusterVhost) Run(word string) ([]libgobuster.Result, error) {
subdomain := fmt.Sprintf("%s.%s", word, v.domain)
_, body, err := v.http.GetBody(v.options.URL, subdomain, v.options.Cookies)
var ret []libgobuster.Result
if err != nil {
return ret, err
}
if *body != v.baseResponse {
result := libgobuster.Result{
Entity: subdomain,
}
ret = append(ret, result)
}
return ret, nil
}
// ResultToString is the to string implementation of gobusterdir
func (v *GobusterVhost) ResultToString(r *libgobuster.Result) (*string, error) {
buf := &bytes.Buffer{}
if _, err := fmt.Fprintf(buf, "Found: %s\n", r.Entity); err != nil {
return nil, err
}
s := buf.String()
return &s, nil
}
// GetConfigString returns the string representation of the current config
func (v *GobusterVhost) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := v.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", v.globalopts.Threads); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if v.globalopts.Wordlist != "-" {
wordlist = v.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if v.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %v", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %v", err)
}
return strings.TrimSpace(buffer.String()), nil
}

18
gobustervhost/options.go Normal file
View File

@ -0,0 +1,18 @@
package gobustervhost
import (
"time"
)
// OptionsVhost is the struct to hold all options for this plugin
type OptionsVhost struct {
Password string
URL string
UserAgent string
Username string
Proxy string
Cookies string
Timeout time.Duration
InsecureSSL bool
FollowRedirect bool
}

View File

@ -1,4 +1,4 @@
package gobusterdir
package libgobuster
import (
"context"
@ -9,12 +9,12 @@ import (
"net/http"
"net/url"
"strings"
"time"
"unicode/utf8"
"github.com/OJ/gobuster/v3/libgobuster"
)
type httpClient struct {
// HTTPClient represents a http object
type HTTPClient struct {
client *http.Client
context context.Context
userAgent string
@ -23,10 +23,22 @@ type httpClient struct {
includeLength bool
}
// HTTPOptions provides options to the http client
type HTTPOptions struct {
Proxy string
Username string
Password string
UserAgent string
Timeout time.Duration
FollowRedirect bool
InsecureSSL bool
IncludeLength bool
}
// NewHTTPClient returns a new HTTPClient
func newHTTPClient(c context.Context, opt *OptionsDir) (*httpClient, error) {
func NewHTTPClient(c context.Context, opt *HTTPOptions) (*HTTPClient, error) {
var proxyURLFunc func(*http.Request) (*url.URL, error)
var client httpClient
var client HTTPClient
proxyURLFunc = http.ProxyFromEnvironment
if opt == nil {
@ -67,42 +79,12 @@ func newHTTPClient(c context.Context, opt *OptionsDir) (*httpClient, error) {
return &client, nil
}
// MakeRequest makes a request to the specified url
func (client *httpClient) makeRequest(fullURL, cookie string) (*int, *int64, error) {
req, err := http.NewRequest(http.MethodGet, fullURL, nil)
// Get makes an http request and returns the status, the length and an error
func (client *HTTPClient) Get(fullURL, host, cookie string) (*int, *int64, error) {
resp, err := client.makeRequest(fullURL, host, cookie)
if err != nil {
return nil, nil, err
}
// add the context so we can easily cancel out
req = req.WithContext(client.context)
if cookie != "" {
req.Header.Set("Cookie", cookie)
}
ua := libgobuster.DefaultUserAgent()
if client.userAgent != "" {
ua = client.userAgent
}
req.Header.Set("User-Agent", ua)
if client.username != "" {
req.SetBasicAuth(client.username, client.password)
}
resp, err := client.client.Do(req)
if err != nil {
if ue, ok := err.(*url.Error); ok {
if strings.HasPrefix(ue.Err.Error(), "x509") {
return nil, nil, fmt.Errorf("Invalid certificate: %v", ue.Err)
}
}
return nil, nil, err
}
defer resp.Body.Close()
var length *int64
@ -120,7 +102,7 @@ func (client *httpClient) makeRequest(fullURL, cookie string) (*int, *int64, err
} else {
// DO NOT REMOVE!
// absolutely needed so golang will reuse connections!
_, err = io.Copy(ioutil.Discard, resp.Body)
_, err := io.Copy(ioutil.Discard, resp.Body)
if err != nil {
return nil, nil, err
}
@ -128,3 +110,60 @@ func (client *httpClient) makeRequest(fullURL, cookie string) (*int, *int64, err
return &resp.StatusCode, length, nil
}
// GetBody makes an http request and returns the status and the body
func (client *HTTPClient) GetBody(fullURL, host, cookie string) (*int, *string, error) {
resp, err := client.makeRequest(fullURL, host, cookie)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("could not read body: %v", err)
}
bodyString := string(body)
return &resp.StatusCode, &bodyString, nil
}
func (client *HTTPClient) makeRequest(fullURL, host, cookie string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, fullURL, nil)
if err != nil {
return nil, err
}
// add the context so we can easily cancel out
req = req.WithContext(client.context)
if cookie != "" {
req.Header.Set("Cookie", cookie)
}
if host != "" {
req.Host = host
}
ua := DefaultUserAgent()
if client.userAgent != "" {
ua = client.userAgent
}
req.Header.Set("User-Agent", ua)
if client.username != "" {
req.SetBasicAuth(client.username, client.password)
}
resp, err := client.client.Do(req)
if err != nil {
if ue, ok := err.(*url.Error); ok {
if strings.HasPrefix(ue.Err.Error(), "x509") {
return nil, fmt.Errorf("Invalid certificate: %v", ue.Err)
}
}
return nil, err
}
return resp, nil
}

View File

@ -1,4 +1,4 @@
package gobusterdir
package libgobuster
import (
"context"
@ -19,12 +19,12 @@ func httpServer(t *testing.T, content string) *httptest.Server {
func TestMakeRequest(t *testing.T) {
h := httpServer(t, "test")
defer h.Close()
o := NewOptionsDir()
c, err := newHTTPClient(context.Background(), o)
var o HTTPOptions
c, err := NewHTTPClient(context.Background(), &o)
if err != nil {
t.Fatalf("Got Error: %v", err)
}
a, b, err := c.makeRequest(h.URL, "")
a, b, err := c.Get(h.URL, "", "")
if err != nil {
t.Fatalf("Got Error: %v", err)
}