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

first implementation of cobra

This commit is contained in:
Christian Mehlmauer 2018-09-19 19:13:22 +02:00
parent 5ca1584c16
commit 042f4f8fd7
No known key found for this signature in database
GPG Key ID: DCF54A05D6E62591
23 changed files with 1090 additions and 674 deletions

223
cli/cmd/dir.go Normal file
View File

@ -0,0 +1,223 @@
package cmd
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/OJ/gobuster/cli"
"github.com/OJ/gobuster/gobusterdir"
"github.com/OJ/gobuster/libgobuster"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
)
var cmdDir *cobra.Command
func runDir(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseDir()
if err != nil {
return fmt.Errorf("error on parsing arguments: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
plugin, err := gobusterdir.NewGobusterDir(ctx, globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobusterdir: %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 parseDir() (*libgobuster.Options, *gobusterdir.OptionsDir, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
plugin := gobusterdir.NewOptionsDir()
url, err := cmdDir.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)
}
}
}
statuscodes, err := cmdDir.Flags().GetString("statuscodes")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for statuscodes: %v", err)
}
plugin.StatusCodes = statuscodes
if err3 := plugin.ParseStatusCodes(); err3 != nil {
return nil, nil, fmt.Errorf("invalid value for statuscodes: %v", err3)
}
cookies, err := cmdDir.Flags().GetString("cookies")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for cookies: %v", err)
}
plugin.Cookies = cookies
username, err := cmdDir.Flags().GetString("username")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for username: %v", err)
}
plugin.Username = username
password, err := cmdDir.Flags().GetString("password")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for password: %v", err)
}
plugin.Password = password
extensions, err := cmdDir.Flags().GetString("extensions")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for extensions: %v", err)
}
plugin.Extensions = extensions
if extensions != "" {
if err2 := plugin.ParseExtensions(); err2 != nil {
return nil, nil, fmt.Errorf("invalid value for extensions: %v", err2)
}
}
useragent, err := cmdDir.Flags().GetString("useragent")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for useragent: %v", err)
}
plugin.UserAgent = useragent
proxy, err := cmdDir.Flags().GetString("proxy")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for proxy: %v", err)
}
plugin.Proxy = proxy
timeout, err := cmdDir.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
expanded, err := cmdDir.Flags().GetBool("expanded")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for expanded: %v", err)
}
plugin.Expanded = expanded
nostatus, err := cmdDir.Flags().GetBool("nostatus")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for nostatus: %v", err)
}
plugin.NoStatus = nostatus
includelength, err := cmdDir.Flags().GetBool("includelength")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for includelength: %v", err)
}
plugin.IncludeLength = includelength
insecuressl, err := cmdDir.Flags().GetBool("insecuressl")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for insecuressl: %v", err)
}
plugin.InsecureSSL = insecuressl
wildcard, err := cmdDir.Flags().GetBool("wildcard")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for wildcard: %v", err)
}
plugin.WildcardForced = wildcard
// Prompt for PW if not provided
if plugin.Username != "" && plugin.Password == "" {
fmt.Printf("[?] Auth Password: ")
passBytes, err := terminal.ReadPassword(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() {
cmdDir = &cobra.Command{
Use: "dir",
Short: "uses dir mode",
RunE: runDir,
}
cmdDir.Flags().StringP("url", "u", "", "The target URL")
cmdDir.Flags().StringP("statuscodes", "s", "200,204,301,302,307,403", "Positive status codes")
cmdDir.Flags().StringP("cookies", "c", "", "Cookies to use for the requests")
cmdDir.Flags().StringP("username", "U", "", "Username for Basic Auth")
cmdDir.Flags().StringP("password", "P", "", "Password for Basic Auth")
cmdDir.Flags().StringP("extensions", "x", "", "File extension(s) to search for")
cmdDir.Flags().StringP("useragent", "a", libgobuster.DefaultUserAgent(), "Set the User-Agent string")
cmdDir.Flags().StringP("proxy", "p", "", "Proxy to use for requests [http(s)://host:port]")
cmdDir.Flags().DurationP("timeout", "d", 10*time.Second, "HTTP Timeout in seconds")
cmdDir.Flags().BoolP("followredirect", "r", false, "Follow redirects")
cmdDir.Flags().BoolP("expanded", "e", false, "Expanded mode, print full URLs")
cmdDir.Flags().BoolP("nostatus", "n", false, "Don't print status codes")
cmdDir.Flags().BoolP("includelength", "l", false, "Include the length of the body in the output")
cmdDir.Flags().BoolP("insecuressl", "k", false, "Skip SSL certificate verification")
cmdDir.Flags().BoolP("wildcard", "", false, "Force continued operation when wildcard found")
if err := cmdDir.MarkFlagRequired("url"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
rootCmd.AddCommand(cmdDir)
}

97
cli/cmd/dns.go Normal file
View File

@ -0,0 +1,97 @@
package cmd
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"github.com/OJ/gobuster/cli"
"github.com/OJ/gobuster/gobusterdns"
"github.com/OJ/gobuster/libgobuster"
"github.com/spf13/cobra"
)
var cmdDNS *cobra.Command
func runDNS(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseDNS()
if err != nil {
return fmt.Errorf("error on parsing arguments: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
plugin, err := gobusterdns.NewGobusterDNS(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("Error on creating gobusterdns: %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 parseDNS() (*libgobuster.Options, *gobusterdns.OptionsDNS, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
plugin := gobusterdns.NewOptionsDNS()
domain, err := cmdDNS.Flags().GetString("domain")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for domain: %v", err)
}
plugin.Domain = domain
showips, err := cmdDNS.Flags().GetBool("showips")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for showips: %v", err)
}
plugin.ShowIPs = showips
showcname, err := cmdDNS.Flags().GetBool("showcname")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for showcname: %v", err)
}
plugin.ShowCNAME = showcname
wildcard, err := cmdDNS.Flags().GetBool("wildcard")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for wildcard: %v", err)
}
plugin.WildcardForced = wildcard
return globalopts, plugin, nil
}
func init() {
cmdDNS = &cobra.Command{
Use: "dns",
Short: "uses dns mode",
RunE: runDNS,
}
cmdDNS.Flags().StringP("domain", "d", "", "The target domain")
cmdDNS.Flags().BoolP("showips", "i", false, "Show IP addresses")
cmdDNS.Flags().BoolP("showcname", "c", false, "Show CNAME records (cannot be used with '-i' option)")
cmdDNS.Flags().BoolP("wildcard", "", false, "Force continued operation when wildcard found")
if err := cmdDNS.MarkFlagRequired("domain"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
rootCmd.AddCommand(cmdDNS)
}

84
cli/cmd/root.go Normal file
View File

@ -0,0 +1,84 @@
package cmd
import (
"fmt"
"log"
"os"
"github.com/OJ/gobuster/libgobuster"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gobuster",
}
// Execute is the main cobra method
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func parseGlobalOptions() (*libgobuster.Options, error) {
globalopts := libgobuster.NewOptions()
threads, err := rootCmd.Flags().GetInt("threads")
if err != nil {
return nil, fmt.Errorf("invalid value for threads: %v", err)
}
if threads <= 0 {
return nil, fmt.Errorf("threads must be bigger than 0")
}
globalopts.Threads = threads
wordlist, err := rootCmd.Flags().GetString("wordlist")
if err != nil {
return nil, fmt.Errorf("invalid value for wordlist: %v", err)
}
if wordlist == "-" {
// STDIN
} else if _, err2 := os.Stat(wordlist); os.IsNotExist(err2) {
return nil, fmt.Errorf("wordlist file %q does not exist: %v", wordlist, err2)
}
globalopts.Wordlist = wordlist
outputfilename, err := rootCmd.Flags().GetString("output")
if err != nil {
return nil, fmt.Errorf("invalid value for output filename: %v", err)
}
globalopts.OutputFilename = outputfilename
verbose, err := rootCmd.Flags().GetBool("verbose")
if err != nil {
return nil, fmt.Errorf("invalid value for verbose: %v", err)
}
globalopts.Verbose = verbose
quiet, err := rootCmd.Flags().GetBool("quiet")
if err != nil {
return nil, fmt.Errorf("invalid value for quiet: %v", err)
}
globalopts.Quiet = quiet
noprogress, err := rootCmd.Flags().GetBool("noprogress")
if err != nil {
return nil, fmt.Errorf("invalid value for noprogress: %v", err)
}
globalopts.NoProgress = noprogress
return globalopts, nil
}
func init() {
rootCmd.PersistentFlags().IntP("threads", "t", 10, "Number of concurrent threads")
rootCmd.PersistentFlags().StringP("wordlist", "w", "", "Path to the wordlist")
rootCmd.PersistentFlags().StringP("output", "o", "", "Output file to write results to (defaults to stdout)")
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output (errors)")
rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Don't print the banner and other noise")
rootCmd.PersistentFlags().BoolP("noprogress", "", false, "Don't display progress")
if err := rootCmd.MarkPersistentFlagRequired("wordlist"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
}

142
cli/gobuster.go Normal file
View File

@ -0,0 +1,142 @@
package cli
import (
"context"
"fmt"
"log"
"os"
"strings"
"sync"
"time"
"github.com/OJ/gobuster/libgobuster"
)
func ruler() {
fmt.Println("=====================================================")
}
func banner() {
fmt.Printf("Gobuster v%s OJ Reeves (@TheColonial)\n", libgobuster.VERSION)
}
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
defer wg.Done()
var f *os.File
var err error
if filename != "" {
f, err = os.Create(filename)
if err != nil {
log.Fatalf("error on creating output file: %v", err)
}
}
for r := range g.Results() {
s, err := r.ToString(g)
if err != nil {
log.Fatal(err)
}
if s != "" {
g.ClearProgress()
s = strings.TrimSpace(s)
fmt.Println(s)
if f != nil {
err = writeToFile(f, s)
if err != nil {
log.Fatalf("error on writing output file: %v", err)
}
}
}
}
}
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for e := range g.Errors() {
if !g.Opts.Quiet {
g.ClearProgress()
log.Printf("[!] %v", e)
}
}
}
func progressWorker(c context.Context, g *libgobuster.Gobuster) {
tick := time.NewTicker(1 * time.Second)
for {
select {
case <-tick.C:
g.PrintProgress()
case <-c.Done():
return
}
}
}
func writeToFile(f *os.File, output string) error {
_, err := f.WriteString(fmt.Sprintf("%s\n", output))
if err != nil {
return fmt.Errorf("[!] Unable to write to file %v", err)
}
return nil
}
// Gobuster is the main entry point for the CLI
func Gobuster(prevCtx context.Context, opts *libgobuster.Options, plugin libgobuster.GobusterPlugin) error {
// Sanity checks
if opts == nil {
return fmt.Errorf("please provide valid options")
}
if plugin == nil {
return fmt.Errorf("please provide a valid plugin")
}
ctx, cancel := context.WithCancel(prevCtx)
defer cancel()
gobuster, err := libgobuster.NewGobuster(ctx, opts, plugin)
if err != nil {
log.Fatalf("[!] %v", err)
}
if !opts.Quiet {
fmt.Println("")
ruler()
banner()
ruler()
c, err := gobuster.GetConfigString()
if err != nil {
log.Fatalf("error on creating config string: %v", err)
}
fmt.Println(c)
ruler()
log.Println("Starting gobuster")
ruler()
}
var wg sync.WaitGroup
wg.Add(2)
go errorWorker(gobuster, &wg)
go resultWorker(gobuster, opts.OutputFilename, &wg)
if !opts.Quiet && !opts.NoProgress {
go progressWorker(ctx, gobuster)
}
if err := gobuster.Start(); err != nil {
log.Printf("[!] %v", err)
} else {
// call cancel func to free ressources and stop progressFunc
cancel()
// wait for all output funcs to finish
wg.Wait()
}
if !opts.Quiet {
gobuster.ClearProgress()
ruler()
log.Println("Finished")
ruler()
}
return nil
}

View File

@ -2,52 +2,90 @@ package gobusterdir
import (
"bytes"
"context"
"fmt"
"log"
"strings"
"github.com/OJ/gobuster/libgobuster"
"github.com/google/uuid"
)
// GobusterDir is the main type to implement the interface
type GobusterDir struct{}
type GobusterDir struct {
options *OptionsDir
globalopts *libgobuster.Options
http *httpClient
}
// Setup is the setup implementation of gobusterdir
func (d GobusterDir) Setup(g *libgobuster.Gobuster) error {
_, _, err := g.GetRequest(g.Opts.URL)
// 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)
}
// NewGobusterDir creates a new initialized GobusterDir
func NewGobusterDir(cont context.Context, globalopts *libgobuster.Options, opts *OptionsDir) (*GobusterDir, 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 := GobusterDir{
options: opts,
globalopts: globalopts,
}
h, err := newHTTPClient(cont, opts)
if err != nil {
return fmt.Errorf("unable to connect to %s: %v", g.Opts.URL, err)
return nil, err
}
g.http = h
return &g, nil
}
// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun() error {
// add trailing slash
if !strings.HasSuffix(d.options.URL, "/") {
d.options.URL = fmt.Sprintf("%s/", d.options.URL)
}
_, _, err := d.get(d.options.URL)
if err != nil {
return fmt.Errorf("unable to connect to %s: %v", d.options.URL, err)
}
guid := uuid.New()
url := fmt.Sprintf("%s%s", g.Opts.URL, guid)
wildcardResp, _, err := g.GetRequest(url)
url := fmt.Sprintf("%s%s", d.options.URL, guid)
wildcardResp, _, err := d.get(url)
if err != nil {
return err
}
if g.Opts.StatusCodesParsed.Contains(*wildcardResp) {
g.IsWildcard = true
if d.options.StatusCodesParsed.Contains(*wildcardResp) {
d.options.IsWildcard = true
log.Printf("[-] Wildcard response found: %s => %d", url, *wildcardResp)
if !g.Opts.WildcardForced {
return fmt.Errorf("To force processing of Wildcard responses, specify the '-fw' switch.")
if !d.options.WildcardForced {
return fmt.Errorf("To force processing of Wildcard responses, specify the '--wildcard' switch.")
}
}
return nil
}
// Process is the process implementation of gobusterdir
func (d GobusterDir) Process(g *libgobuster.Gobuster, word string) ([]libgobuster.Result, error) {
// Run is the process implementation of gobusterdir
func (d *GobusterDir) Run(word string) ([]libgobuster.Result, error) {
suffix := ""
if g.Opts.UseSlash {
if d.options.UseSlash {
suffix = "/"
}
// Try the DIR first
url := fmt.Sprintf("%s%s%s", g.Opts.URL, word, suffix)
dirResp, dirSize, err := g.GetRequest(url)
url := fmt.Sprintf("%s%s%s", d.options.URL, word, suffix)
dirResp, dirSize, err := d.get(url)
if err != nil {
return nil, err
}
@ -61,10 +99,10 @@ func (d GobusterDir) Process(g *libgobuster.Gobuster, word string) ([]libgobuste
}
// Follow up with files using each ext.
for ext := range g.Opts.ExtensionsParsed.Set {
for ext := range d.options.ExtensionsParsed.Set {
file := fmt.Sprintf("%s.%s", word, ext)
url = fmt.Sprintf("%s%s", g.Opts.URL, file)
fileResp, fileSize, err := g.GetRequest(url)
url = fmt.Sprintf("%s%s", d.options.URL, file)
fileResp, fileSize, err := d.get(url)
if err != nil {
return nil, err
}
@ -81,12 +119,12 @@ func (d GobusterDir) Process(g *libgobuster.Gobuster, word string) ([]libgobuste
}
// ResultToString is the to string implementation of gobusterdir
func (d GobusterDir) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Result) (*string, error) {
func (d *GobusterDir) ResultToString(r *libgobuster.Result) (*string, error) {
buf := &bytes.Buffer{}
// Prefix if we're in verbose mode
if g.Opts.Verbose {
if g.Opts.StatusCodesParsed.Contains(r.Status) {
if d.globalopts.Verbose {
if d.options.StatusCodesParsed.Contains(r.Status) {
if _, err := fmt.Fprintf(buf, "Found: "); err != nil {
return nil, err
}
@ -97,9 +135,9 @@ func (d GobusterDir) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Resu
}
}
if g.Opts.StatusCodesParsed.Contains(r.Status) || g.Opts.Verbose {
if g.Opts.Expanded {
if _, err := fmt.Fprintf(buf, g.Opts.URL); err != nil {
if d.options.StatusCodesParsed.Contains(r.Status) || d.globalopts.Verbose {
if d.options.Expanded {
if _, err := fmt.Fprintf(buf, d.options.URL); err != nil {
return nil, err
}
} else {
@ -111,7 +149,7 @@ func (d GobusterDir) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Resu
return nil, err
}
if !g.Opts.NoStatus {
if !d.options.NoStatus {
if _, err := fmt.Fprintf(buf, " (Status: %d)", r.Status); err != nil {
return nil, err
}
@ -129,3 +167,99 @@ func (d GobusterDir) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Resu
s := buf.String()
return &s, nil
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDir) GetConfigString() (string, error) {
buf := &bytes.Buffer{}
o := d.options
if _, err := fmt.Fprintf(buf, "[+] Url : %s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(buf, "[+] Threads : %d\n", d.globalopts.Threads); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(buf, "[+] Wordlist : %s\n", wordlist); err != nil {
return "", err
}
if _, err := fmt.Fprintf(buf, "[+] Status codes : %s\n", o.StatusCodesParsed.Stringify()); err != nil {
return "", err
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(buf, "[+] Proxy : %s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(buf, "[+] Cookies : %s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(buf, "[+] User Agent : %s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.IncludeLength {
if _, err := fmt.Fprintf(buf, "[+] Show length : true\n"); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(buf, "[+] Auth User : %s\n", o.Username); err != nil {
return "", err
}
}
if o.Extensions != "" {
if _, err := fmt.Fprintf(buf, "[+] Extensions : %s\n", o.ExtensionsParsed.Stringify()); err != nil {
return "", err
}
}
if o.UseSlash {
if _, err := fmt.Fprintf(buf, "[+] Add Slash : true\n"); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(buf, "[+] Follow Redir : true\n"); err != nil {
return "", err
}
}
if o.Expanded {
if _, err := fmt.Fprintf(buf, "[+] Expanded : true\n"); err != nil {
return "", err
}
}
if o.NoStatus {
if _, err := fmt.Fprintf(buf, "[+] No status : true\n"); err != nil {
return "", err
}
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(buf, "[+] Verbose : true\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(buf, "[+] Timeout : %s\n", o.Timeout.String()); err != nil {
return "", err
}
return strings.TrimSpace(buf.String()), nil
}

39
gobusterdir/helper.go Normal file
View File

@ -0,0 +1,39 @@
package gobusterdir
import (
"fmt"
"strconv"
"strings"
)
// ParseExtensions parses the extensions provided as a comma seperated list
func (opt *OptionsDir) ParseExtensions() error {
if opt.Extensions == "" {
return fmt.Errorf("invalid extension string provided")
}
exts := strings.Split(opt.Extensions, ",")
for _, e := range exts {
e = strings.TrimSpace(e)
// remove leading . from extensions
opt.ExtensionsParsed.Add(strings.TrimPrefix(e, "."))
}
return nil
}
// ParseStatusCodes parses the status codes provided as a comma seperated list
func (opt *OptionsDir) ParseStatusCodes() error {
if opt.StatusCodes == "" {
return fmt.Errorf("invalid status code string provided")
}
for _, c := range strings.Split(opt.StatusCodes, ",") {
c = strings.TrimSpace(c)
i, err := strconv.Atoi(c)
if err != nil {
return fmt.Errorf("invalid status code given: %s", c)
}
opt.StatusCodesParsed.Add(i)
}
return nil
}

View File

@ -0,0 +1,73 @@
package gobusterdir
import (
"reflect"
"testing"
"github.com/OJ/gobuster/libgobuster"
)
func TestParseExtensions(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
Extensions string
expectedExtensions libgobuster.StringSet
expectedError string
}{
{"Valid extensions", "php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", libgobuster.NewStringSet(), "invalid extension string provided"},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
o := NewOptionsDir()
o.Extensions = x.Extensions
err := o.ParseExtensions()
if x.expectedError != "" {
if err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedExtensions, o.ExtensionsParsed) {
t.Fatalf("Expected %v but got %v", x.expectedExtensions, o.ExtensionsParsed)
}
})
}
}
func TestParseStatusCodes(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
stringCodes string
expectedCodes libgobuster.IntSet
expectedError string
}{
{"Valid codes", "200,100,202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", libgobuster.NewIntSet(), "invalid status code given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", libgobuster.NewIntSet(), "invalid status code given: 2000000000000000000000000000000"},
{"Empty string", "", libgobuster.NewIntSet(), "invalid status code string provided"},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
o := NewOptionsDir()
o.StatusCodes = x.stringCodes
err := o.ParseStatusCodes()
if x.expectedError != "" {
if err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedCodes, o.StatusCodesParsed) {
t.Fatalf("Expected %v but got %v", x.expectedCodes, o.StatusCodesParsed)
}
})
}
}

View File

@ -1,4 +1,4 @@
package libgobuster
package gobusterdir
import (
"context"
@ -10,6 +10,8 @@ import (
"net/url"
"strings"
"unicode/utf8"
"github.com/OJ/gobuster/libgobuster"
)
type httpClient struct {
@ -22,7 +24,7 @@ type httpClient struct {
}
// NewHTTPClient returns a new HTTPClient
func newHTTPClient(c context.Context, opt *Options) (*httpClient, error) {
func newHTTPClient(c context.Context, opt *OptionsDir) (*httpClient, error) {
var proxyURLFunc func(*http.Request) (*url.URL, error)
var client httpClient
proxyURLFunc = http.ProxyFromEnvironment
@ -80,7 +82,7 @@ func (client *httpClient) makeRequest(fullURL, cookie string) (*int, *int64, err
req.Header.Set("Cookie", cookie)
}
ua := fmt.Sprintf("gobuster %s", VERSION)
ua := libgobuster.DefaultUserAgent()
if client.userAgent != "" {
ua = client.userAgent
}

View File

@ -1,4 +1,4 @@
package libgobuster
package gobusterdir
import (
"context"
@ -19,7 +19,7 @@ func httpServer(t *testing.T, content string) *httptest.Server {
func TestMakeRequest(t *testing.T) {
h := httpServer(t, "test")
defer h.Close()
o := NewOptions()
o := NewOptionsDir()
c, err := newHTTPClient(context.Background(), o)
if err != nil {
t.Fatalf("Got Error: %v", err)

38
gobusterdir/options.go Normal file
View File

@ -0,0 +1,38 @@
package gobusterdir
import (
"time"
"github.com/OJ/gobuster/libgobuster"
)
// OptionsDir is the struct to hold all options for this plugin
type OptionsDir struct {
Extensions string
ExtensionsParsed libgobuster.StringSet
Password string
StatusCodes string
StatusCodesParsed libgobuster.IntSet
URL string
UserAgent string
Username string
Proxy string
Cookies string
Timeout time.Duration
FollowRedirect bool
IncludeLength bool
Expanded bool
NoStatus bool
InsecureSSL bool
UseSlash bool
IsWildcard bool
WildcardForced bool
}
// NewOptionsDir returns a new initialized OptionsDir
func NewOptionsDir() *OptionsDir {
return &OptionsDir{
StatusCodesParsed: libgobuster.NewIntSet(),
ExtensionsParsed: libgobuster.NewStringSet(),
}
}

View File

@ -0,0 +1,16 @@
package gobusterdir
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptionsDir()
if o.StatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
if o.ExtensionsParsed.Set == nil {
t.Fatal("ExtensionsParsed not initialized")
}
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"log"
"net"
"strings"
"github.com/OJ/gobuster/libgobuster"
@ -11,55 +12,78 @@ import (
)
// GobusterDNS is the main type to implement the interface
type GobusterDNS struct{}
type GobusterDNS struct {
globalopts *libgobuster.Options
options *OptionsDNS
isWildcard bool
wildcardIps libgobuster.StringSet
}
// Setup is the setup implementation of gobusterdns
func (d GobusterDNS) Setup(g *libgobuster.Gobuster) error {
// NewGobusterDNS creates a new initialized GobusterDNS
func NewGobusterDNS(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, 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 := GobusterDNS{
options: opts,
globalopts: globalopts,
wildcardIps: libgobuster.NewStringSet(),
}
return &g, nil
}
// PreRun is the pre run implementation of gobusterdns
func (d *GobusterDNS) PreRun() error {
// Resolve a subdomain sthat probably shouldn't exist
guid := uuid.New()
wildcardIps, err := g.DNSLookup(fmt.Sprintf("%s.%s", guid, g.Opts.URL))
wildcardIps, err := dnsLookup(fmt.Sprintf("%s.%s", guid, d.options.Domain))
if err == nil {
g.IsWildcard = true
g.WildcardIps.AddRange(wildcardIps)
log.Printf("[-] Wildcard DNS found. IP address(es): %s", g.WildcardIps.Stringify())
if !g.Opts.WildcardForced {
return fmt.Errorf("To force processing of Wildcard DNS, specify the '-fw' switch.")
d.isWildcard = true
d.wildcardIps.AddRange(wildcardIps)
log.Printf("[-] Wildcard DNS found. IP address(es): %s", d.wildcardIps.Stringify())
if !d.options.WildcardForced {
return fmt.Errorf("To force processing of Wildcard DNS, specify the '--wildcard' switch.")
}
}
if !g.Opts.Quiet {
if !d.globalopts.Quiet {
// Provide a warning if the base domain doesn't resolve (in case of typo)
_, err = g.DNSLookup(g.Opts.URL)
_, err = dnsLookup(d.options.Domain)
if err != nil {
// Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.py.to` does!
log.Printf("[-] Unable to validate base domain: %s", g.Opts.URL)
log.Printf("[-] Unable to validate base domain: %s", d.options.Domain)
}
}
return nil
}
// Process is the process implementation of gobusterdns
func (d GobusterDNS) Process(g *libgobuster.Gobuster, word string) ([]libgobuster.Result, error) {
subdomain := fmt.Sprintf("%s.%s", word, g.Opts.URL)
ips, err := g.DNSLookup(subdomain)
// Run is the process implementation of gobusterdns
func (d *GobusterDNS) Run(word string) ([]libgobuster.Result, error) {
subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain)
ips, err := dnsLookup(subdomain)
var ret []libgobuster.Result
if err == nil {
if !g.IsWildcard || !g.WildcardIps.ContainsAny(ips) {
if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) {
result := libgobuster.Result{
Entity: subdomain,
}
if g.Opts.ShowIPs {
if d.options.ShowIPs {
result.Extra = strings.Join(ips, ", ")
} else if g.Opts.ShowCNAME {
cname, err := g.DNSLookupCname(subdomain)
} else if d.options.ShowCNAME {
cname, err := dnsLookupCname(subdomain)
if err == nil {
result.Extra = cname
}
}
ret = append(ret, result)
}
} else if g.Opts.Verbose {
} else if d.globalopts.Verbose {
ret = append(ret, libgobuster.Result{
Entity: subdomain,
Status: 404,
@ -69,18 +93,18 @@ func (d GobusterDNS) Process(g *libgobuster.Gobuster, word string) ([]libgobuste
}
// ResultToString is the to string implementation of gobusterdns
func (d GobusterDNS) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Result) (*string, error) {
func (d *GobusterDNS) ResultToString(r *libgobuster.Result) (*string, error) {
buf := &bytes.Buffer{}
if r.Status == 404 {
if _, err := fmt.Fprintf(buf, "Missing: %s\n", r.Entity); err != nil {
return nil, err
}
} else if g.Opts.ShowIPs {
} else if d.options.ShowIPs {
if _, err := fmt.Fprintf(buf, "Found: %s [%s]\n", r.Entity, r.Extra); err != nil {
return nil, err
}
} else if g.Opts.ShowCNAME {
} else if d.options.ShowCNAME {
if _, err := fmt.Fprintf(buf, "Found: %s [%s]\n", r.Entity, r.Extra); err != nil {
return nil, err
}
@ -93,3 +117,57 @@ func (d GobusterDNS) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Resu
s := buf.String()
return &s, nil
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDNS) GetConfigString() (string, error) {
buf := &bytes.Buffer{}
o := d.options
if _, err := fmt.Fprintf(buf, "[+] Domain : %s\n", o.Domain); err != nil {
return "", err
}
if _, err := fmt.Fprintf(buf, "[+] Threads : %d\n", d.globalopts.Threads); err != nil {
return "", err
}
if o.ShowCNAME {
if _, err := fmt.Fprintf(buf, "[+] Show CNAME : true\n"); err != nil {
return "", err
}
}
if o.ShowIPs {
if _, err := fmt.Fprintf(buf, "[+] Show IPs : true\n"); err != nil {
return "", err
}
}
if o.WildcardForced {
if _, err := fmt.Fprintf(buf, "[+] Wildcard forced : true\n"); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(buf, "[+] Wordlist : %s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(buf, "[+] Verbose : true\n"); err != nil {
return "", err
}
}
return strings.TrimSpace(buf.String()), nil
}
func dnsLookup(domain string) ([]string, error) {
return net.LookupHost(domain)
}
func dnsLookupCname(domain string) (string, error) {
return net.LookupCNAME(domain)
}

14
gobusterdns/options.go Normal file
View File

@ -0,0 +1,14 @@
package gobusterdns
// OptionsDNS holds all options for the dns plugin
type OptionsDNS struct {
Domain string
ShowIPs bool
ShowCNAME bool
WildcardForced bool
}
// NewOptionsDNS returns a new initialized OptionsDNS
func NewOptionsDNS() *OptionsDNS {
return &OptionsDNS{}
}

View File

@ -0,0 +1,12 @@
package gobusterdns
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptionsDNS()
if o.WildcardIps.Set == nil {
t.Fatal("WildcardIps not initialized")
}
}

View File

@ -8,40 +8,43 @@ import (
"strings"
)
type intSet struct {
// IntSet is a set of Ints
type IntSet struct {
Set map[int]bool
}
type stringSet struct {
// StringSet is a set of Strings
type StringSet struct {
Set map[string]bool
}
func newStringSet() stringSet {
return stringSet{Set: map[string]bool{}}
// NewStringSet creates a new initialized StringSet
func NewStringSet() StringSet {
return StringSet{Set: map[string]bool{}}
}
// Add an element to a set
func (set *stringSet) Add(s string) bool {
func (set *StringSet) Add(s string) bool {
_, found := set.Set[s]
set.Set[s] = true
return !found
}
// Add a list of elements to a set
func (set *stringSet) AddRange(ss []string) {
// AddRange adds a list of elements to a set
func (set *StringSet) AddRange(ss []string) {
for _, s := range ss {
set.Set[s] = true
}
}
// Test if an element is in a set
func (set *stringSet) Contains(s string) bool {
// Contains tests if an element is in a set
func (set *StringSet) Contains(s string) bool {
_, found := set.Set[s]
return found
}
// Check if any of the elements exist
func (set *stringSet) ContainsAny(ss []string) bool {
// ContainsAny checks if any of the elements exist
func (set *StringSet) ContainsAny(ss []string) bool {
for _, s := range ss {
if set.Set[s] {
return true
@ -50,8 +53,13 @@ func (set *stringSet) ContainsAny(ss []string) bool {
return false
}
// Length returns the length of the Set
func (set *StringSet) Length() int {
return len(set.Set)
}
// Stringify the set
func (set *stringSet) Stringify() string {
func (set *StringSet) Stringify() string {
values := []string{}
for s := range set.Set {
values = append(values, s)
@ -59,25 +67,26 @@ func (set *stringSet) Stringify() string {
return strings.Join(values, ",")
}
func newIntSet() intSet {
return intSet{Set: map[int]bool{}}
// NewIntSet creates a new initialized IntSet
func NewIntSet() IntSet {
return IntSet{Set: map[int]bool{}}
}
// Add an element to a set
func (set *intSet) Add(i int) bool {
// Add adds an element to a set
func (set *IntSet) Add(i int) bool {
_, found := set.Set[i]
set.Set[i] = true
return !found
}
// Test if an element is in a set
func (set *intSet) Contains(i int) bool {
// Contains tests if an element is in a set
func (set *IntSet) Contains(i int) bool {
_, found := set.Set[i]
return found
}
// Stringify the set
func (set *intSet) Stringify() string {
func (set *IntSet) Stringify() string {
values := []int{}
for s := range set.Set {
values = append(values, s)
@ -88,6 +97,11 @@ func (set *intSet) Stringify() string {
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(values)), delim), "[]")
}
// Length returns the length of the Set
func (set *IntSet) Length() int {
return len(set.Set)
}
func lineCounter(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
count := 1
@ -106,3 +120,8 @@ func lineCounter(r io.Reader) (int, error) {
}
}
}
// DefaultUserAgent returns the default user agent to use in HTTP requests
func DefaultUserAgent() string {
return fmt.Sprintf("gobuster %s", VERSION)
}

View File

@ -7,19 +7,19 @@ import (
)
func TestNewStringSet(t *testing.T) {
if newStringSet().Set == nil {
if NewStringSet().Set == nil {
t.Fatal("newStringSet returned nil Set")
}
}
func TestNewIntSet(t *testing.T) {
if newIntSet().Set == nil {
if NewIntSet().Set == nil {
t.Fatal("newIntSet returned nil Set")
}
}
func TestStringSetAdd(t *testing.T) {
x := newStringSet()
x := NewStringSet()
x.Add("test")
if len(x.Set) != 1 {
t.Fatalf("Unexptected size. Should have 1 Got %v", len(x.Set))
@ -27,7 +27,7 @@ func TestStringSetAdd(t *testing.T) {
}
func TestStringSetAddDouble(t *testing.T) {
x := newStringSet()
x := NewStringSet()
x.Add("test")
x.Add("test")
if len(x.Set) != 1 {
@ -36,7 +36,7 @@ func TestStringSetAddDouble(t *testing.T) {
}
func TestStringSetAddRange(t *testing.T) {
x := newStringSet()
x := NewStringSet()
x.AddRange([]string{"asdf", "ghjk"})
if len(x.Set) != 2 {
t.Fatalf("Unexptected size. Should have 2 Got %d", len(x.Set))
@ -44,7 +44,7 @@ func TestStringSetAddRange(t *testing.T) {
}
func TestStringSetAddRangeDouble(t *testing.T) {
x := newStringSet()
x := NewStringSet()
x.AddRange([]string{"asdf", "ghjk", "asdf", "ghjk"})
if len(x.Set) != 2 {
t.Fatalf("Unexptected size. Should have 2 Got %d", len(x.Set))
@ -52,7 +52,7 @@ func TestStringSetAddRangeDouble(t *testing.T) {
}
func TestStringSetContains(t *testing.T) {
x := newStringSet()
x := NewStringSet()
v := []string{"asdf", "ghjk", "1234", "5678"}
x.AddRange(v)
for _, y := range v {
@ -63,7 +63,7 @@ func TestStringSetContains(t *testing.T) {
}
func TestStringSetContainsAny(t *testing.T) {
x := newStringSet()
x := NewStringSet()
v := []string{"asdf", "ghjk", "1234", "5678"}
x.AddRange(v)
if !x.ContainsAny(v) {
@ -77,7 +77,7 @@ func TestStringSetContainsAny(t *testing.T) {
}
func TestStringSetStringify(t *testing.T) {
x := newStringSet()
x := NewStringSet()
v := []string{"asdf", "ghjk", "1234", "5678"}
x.AddRange(v)
z := x.Stringify()
@ -90,7 +90,7 @@ func TestStringSetStringify(t *testing.T) {
}
func TestIntSetAdd(t *testing.T) {
x := newIntSet()
x := NewIntSet()
x.Add(1)
if len(x.Set) != 1 {
t.Fatalf("Unexptected size. Should have 1 Got %d", len(x.Set))
@ -98,7 +98,7 @@ func TestIntSetAdd(t *testing.T) {
}
func TestIntSetAddDouble(t *testing.T) {
x := newIntSet()
x := NewIntSet()
x.Add(1)
x.Add(1)
if len(x.Set) != 1 {
@ -107,7 +107,7 @@ func TestIntSetAddDouble(t *testing.T) {
}
func TestIntSetContains(t *testing.T) {
x := newIntSet()
x := NewIntSet()
v := []int{1, 2, 3, 4}
for _, y := range v {
x.Add(y)
@ -120,7 +120,7 @@ func TestIntSetContains(t *testing.T) {
}
func TestIntSetStringify(t *testing.T) {
x := newIntSet()
x := NewIntSet()
v := []int{1, 3, 2, 4}
expected := "1,2,3,4"
for _, y := range v {

View File

@ -0,0 +1,9 @@
package libgobuster
// GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface {
PreRun() error
Run(string) ([]Result, error)
ResultToString(*Result) (*string, error)
GetConfigString() (string, error)
}

View File

@ -2,20 +2,13 @@ package libgobuster
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"os"
"strings"
"sync"
)
const (
// VERSION contains the current gobuster version
VERSION = "2.0.1"
)
// SetupFunc is the "setup" function prototype for implementations
type SetupFunc func(*Gobuster) error
@ -28,46 +21,22 @@ type ResultToStringFunc func(*Gobuster, *Result) (*string, error)
// Gobuster is the main object when creating a new run
type Gobuster struct {
Opts *Options
http *httpClient
WildcardIps stringSet
context context.Context
requestsExpected int
requestsIssued int
mu *sync.RWMutex
plugin GobusterPlugin
IsWildcard bool
resultChan chan Result
errorChan chan error
}
// GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface {
Setup(*Gobuster) error
Process(*Gobuster, string) ([]Result, error)
ResultToString(*Gobuster, *Result) (*string, error)
}
// NewGobuster returns a new Gobuster object
func NewGobuster(c context.Context, opts *Options, plugin GobusterPlugin) (*Gobuster, error) {
// validate given options
multiErr := opts.validate()
if multiErr != nil {
return nil, multiErr
}
var g Gobuster
g.WildcardIps = newStringSet()
g.context = c
g.Opts = opts
h, err := newHTTPClient(c, opts)
if err != nil {
return nil, err
}
g.http = h
g.plugin = plugin
g.mu = new(sync.RWMutex)
g.context = c
g.resultChan = make(chan Result)
g.errorChan = make(chan error)
@ -109,22 +78,6 @@ func (g *Gobuster) ClearProgress() {
fmt.Fprint(os.Stderr, resetTerminal())
}
// GetRequest issues a GET request to the target and returns
// the status code, length and an error
func (g *Gobuster) GetRequest(url string) (*int, *int64, error) {
return g.http.makeRequest(url, g.Opts.Cookies)
}
// DNSLookup looks up a domain via system default DNS servers
func (g *Gobuster) DNSLookup(domain string) ([]string, error) {
return net.LookupHost(domain)
}
// DNSLookupCname looks up a CNAME record via system default DNS servers
func (g *Gobuster) DNSLookupCname(domain string) (string, error) {
return net.LookupCNAME(domain)
}
func (g *Gobuster) worker(wordChan <-chan string, wg *sync.WaitGroup) {
defer wg.Done()
for {
@ -138,7 +91,7 @@ func (g *Gobuster) worker(wordChan <-chan string, wg *sync.WaitGroup) {
}
g.incrementRequests()
// Mode-specific processing
res, err := g.plugin.Process(g, word)
res, err := g.plugin.Run(word)
if err != nil {
// do not exit and continue
g.errorChan <- err
@ -182,7 +135,7 @@ func (g *Gobuster) getWordlist() (*bufio.Scanner, error) {
// Start the busting of the website with the given
// set of settings from the command line.
func (g *Gobuster) Start() error {
if err := g.plugin.Setup(g); err != nil {
if err := g.plugin.PreRun(); err != nil {
return err
}
@ -224,101 +177,5 @@ Scan:
// GetConfigString returns the current config as a printable string
func (g *Gobuster) GetConfigString() (string, error) {
buf := &bytes.Buffer{}
o := g.Opts
if _, err := fmt.Fprintf(buf, "[+] Mode : %s\n", o.Mode); err != nil {
return "", err
}
if _, err := fmt.Fprintf(buf, "[+] Url/Domain : %s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(buf, "[+] Threads : %d\n", o.Threads); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if o.Wordlist != "-" {
wordlist = o.Wordlist
}
if _, err := fmt.Fprintf(buf, "[+] Wordlist : %s\n", wordlist); err != nil {
return "", err
}
if o.Mode == ModeDir {
if _, err := fmt.Fprintf(buf, "[+] Status codes : %s\n", o.StatusCodesParsed.Stringify()); err != nil {
return "", err
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(buf, "[+] Proxy : %s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(buf, "[+] Cookies : %s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(buf, "[+] User Agent : %s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.IncludeLength {
if _, err := fmt.Fprintf(buf, "[+] Show length : true\n"); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(buf, "[+] Auth User : %s\n", o.Username); err != nil {
return "", err
}
}
if len(o.Extensions) > 0 {
if _, err := fmt.Fprintf(buf, "[+] Extensions : %s\n", o.ExtensionsParsed.Stringify()); err != nil {
return "", err
}
}
if o.UseSlash {
if _, err := fmt.Fprintf(buf, "[+] Add Slash : true\n"); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(buf, "[+] Follow Redir : true\n"); err != nil {
return "", err
}
}
if o.Expanded {
if _, err := fmt.Fprintf(buf, "[+] Expanded : true\n"); err != nil {
return "", err
}
}
if o.NoStatus {
if _, err := fmt.Fprintf(buf, "[+] No status : true\n"); err != nil {
return "", err
}
}
if o.Verbose {
if _, err := fmt.Fprintf(buf, "[+] Verbose : true\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(buf, "[+] Timeout : %s\n", o.Timeout.String()); err != nil {
return "", err
}
}
return strings.TrimSpace(buf.String()), nil
return g.plugin.GetConfigString()
}

View File

@ -1,170 +1,18 @@
package libgobuster
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
multierror "github.com/hashicorp/go-multierror"
)
const (
// ModeDir represents -m dir
ModeDir = "dir"
// ModeDNS represents -m dns
ModeDNS = "dns"
)
// Options helds all options that can be passed to libgobuster
type Options struct {
Extensions string
ExtensionsParsed stringSet
Mode string
Password string
StatusCodes string
StatusCodesParsed intSet
Threads int
URL string
UserAgent string
Username string
Wordlist string
Proxy string
Cookies string
Timeout time.Duration
FollowRedirect bool
IncludeLength bool
NoStatus bool
NoProgress bool
Expanded bool
Quiet bool
ShowIPs bool
ShowCNAME bool
InsecureSSL bool
WildcardForced bool
Verbose bool
UseSlash bool
Threads int
Wordlist string
OutputFilename string
NoStatus bool
NoProgress bool
Quiet bool
WildcardForced bool
Verbose bool
}
// NewOptions returns a new initialized Options object
func NewOptions() *Options {
return &Options{
StatusCodesParsed: newIntSet(),
ExtensionsParsed: newStringSet(),
}
}
// Validate validates the given options
func (opt *Options) validate() *multierror.Error {
var errorList *multierror.Error
if strings.ToLower(opt.Mode) != ModeDir && strings.ToLower(opt.Mode) != ModeDNS {
errorList = multierror.Append(errorList, fmt.Errorf("Mode (-m): Invalid value: %s", opt.Mode))
}
if opt.Threads < 0 {
errorList = multierror.Append(errorList, fmt.Errorf("Threads (-t): Invalid value: %d", opt.Threads))
}
if opt.Wordlist == "" {
errorList = multierror.Append(errorList, fmt.Errorf("WordList (-w): Must be specified (use `-w -` for stdin)"))
} else if opt.Wordlist == "-" {
// STDIN
} else if _, err := os.Stat(opt.Wordlist); os.IsNotExist(err) {
errorList = multierror.Append(errorList, fmt.Errorf("Wordlist (-w): File does not exist: %s", opt.Wordlist))
}
if opt.URL == "" {
errorList = multierror.Append(errorList, fmt.Errorf("Url/Domain (-u): Must be specified"))
}
if opt.StatusCodes != "" {
if err := opt.parseStatusCodes(); err != nil {
errorList = multierror.Append(errorList, err)
}
}
if opt.Extensions != "" {
if err := opt.parseExtensions(); err != nil {
errorList = multierror.Append(errorList, err)
}
}
if opt.Mode == ModeDir {
if !strings.HasSuffix(opt.URL, "/") {
opt.URL = fmt.Sprintf("%s/", opt.URL)
}
if err := opt.validateDirMode(); err != nil {
errorList = multierror.Append(errorList, err)
}
}
return errorList
}
// ParseExtensions parses the extensions provided as a comma seperated list
func (opt *Options) parseExtensions() error {
if opt.Extensions == "" {
return fmt.Errorf("invalid extension string provided")
}
exts := strings.Split(opt.Extensions, ",")
for _, e := range exts {
e = strings.TrimSpace(e)
// remove leading . from extensions
opt.ExtensionsParsed.Add(strings.TrimPrefix(e, "."))
}
return nil
}
// ParseStatusCodes parses the status codes provided as a comma seperated list
func (opt *Options) parseStatusCodes() error {
if opt.StatusCodes == "" {
return fmt.Errorf("invalid status code string provided")
}
for _, c := range strings.Split(opt.StatusCodes, ",") {
c = strings.TrimSpace(c)
i, err := strconv.Atoi(c)
if err != nil {
return fmt.Errorf("invalid status code given: %s", c)
}
opt.StatusCodesParsed.Add(i)
}
return nil
}
func (opt *Options) validateDirMode() error {
// bail out if we are not in dir mode
if opt.Mode != ModeDir {
return nil
}
if !strings.HasPrefix(opt.URL, "http") {
// check to see if a port was specified
re := regexp.MustCompile(`^[^/]+:(\d+)`)
match := re.FindStringSubmatch(opt.URL)
if len(match) < 2 {
// no port, default to http on 80
opt.URL = fmt.Sprintf("http://%s", opt.URL)
} else {
port, err := strconv.Atoi(match[1])
if err != nil || (port != 80 && port != 443) {
return fmt.Errorf("url scheme not specified")
} else if port == 80 {
opt.URL = fmt.Sprintf("http://%s", opt.URL)
} else {
opt.URL = fmt.Sprintf("https://%s", opt.URL)
}
}
}
if opt.Username != "" && opt.Password == "" {
return fmt.Errorf("username was provided but password is missing")
}
return nil
return &Options{}
}

View File

@ -1,84 +0,0 @@
package libgobuster
import (
"reflect"
"testing"
)
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptions()
if o.StatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
if o.ExtensionsParsed.Set == nil {
t.Fatal("ExtensionsParsed not initialized")
}
}
func TestParseExtensions(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
Extensions string
expectedExtensions stringSet
expectedError string
}{
{"Valid extensions", "php,asp,txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", newStringSet(), "invalid extension string provided"},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
o := NewOptions()
o.Extensions = x.Extensions
err := o.parseExtensions()
if x.expectedError != "" {
if err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedExtensions, o.ExtensionsParsed) {
t.Fatalf("Expected %v but got %v", x.expectedExtensions, o.ExtensionsParsed)
}
})
}
}
func TestParseStatusCodes(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
stringCodes string
expectedCodes intSet
expectedError string
}{
{"Valid codes", "200,100,202", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", newIntSet(), "invalid status code given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", newIntSet(), "invalid status code given: 2000000000000000000000000000000"},
{"Empty string", "", newIntSet(), "invalid status code string provided"},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
o := NewOptions()
o.StatusCodes = x.stringCodes
err := o.parseStatusCodes()
if x.expectedError != "" {
if err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedCodes, o.StatusCodesParsed) {
t.Fatalf("Expected %v but got %v", x.expectedCodes, o.StatusCodesParsed)
}
})
}
}

View File

@ -10,7 +10,7 @@ type Result struct {
// ToString converts the Result to it's textual representation
func (r *Result) ToString(g *Gobuster) (string, error) {
s, err := g.plugin.ResultToString(g, r)
s, err := g.plugin.ResultToString(r)
if err != nil {
return "", err
}

6
libgobuster/version.go Normal file
View File

@ -0,0 +1,6 @@
package libgobuster
const (
// VERSION contains the current gobuster version
VERSION = "2.0.1"
)

195
main.go
View File

@ -17,200 +17,9 @@ package main
//----------------------------------------------------
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/OJ/gobuster/gobusterdir"
"github.com/OJ/gobuster/gobusterdns"
"github.com/OJ/gobuster/libgobuster"
"golang.org/x/crypto/ssh/terminal"
"github.com/OJ/gobuster/cli/cmd"
)
func ruler() {
fmt.Println("=====================================================")
}
func banner() {
fmt.Printf("Gobuster v%s OJ Reeves (@TheColonial)\n", libgobuster.VERSION)
}
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
defer wg.Done()
var f *os.File
var err error
if filename != "" {
f, err = os.Create(filename)
if err != nil {
log.Fatalf("error on creating output file: %v", err)
}
}
for r := range g.Results() {
s, err := r.ToString(g)
if err != nil {
log.Fatal(err)
}
if s != "" {
g.ClearProgress()
s = strings.TrimSpace(s)
fmt.Println(s)
if f != nil {
err = writeToFile(f, s)
if err != nil {
log.Fatalf("error on writing output file: %v", err)
}
}
}
}
}
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for e := range g.Errors() {
if !g.Opts.Quiet {
g.ClearProgress()
log.Printf("[!] %v", e)
}
}
}
func progressWorker(c context.Context, g *libgobuster.Gobuster) {
tick := time.NewTicker(1 * time.Second)
for {
select {
case <-tick.C:
g.PrintProgress()
case <-c.Done():
return
}
}
}
func writeToFile(f *os.File, output string) error {
_, err := f.WriteString(fmt.Sprintf("%s\n", output))
if err != nil {
return fmt.Errorf("[!] Unable to write to file %v", err)
}
return nil
}
func main() {
var outputFilename string
o := libgobuster.NewOptions()
flag.IntVar(&o.Threads, "t", 10, "Number of concurrent threads")
flag.StringVar(&o.Mode, "m", "dir", "Directory/File mode (dir) or DNS mode (dns)")
flag.StringVar(&o.Wordlist, "w", "", "Path to the wordlist")
flag.StringVar(&o.StatusCodes, "s", "200,204,301,302,307,403", "Positive status codes (dir mode only)")
flag.StringVar(&outputFilename, "o", "", "Output file to write results to (defaults to stdout)")
flag.StringVar(&o.URL, "u", "", "The target URL or Domain")
flag.StringVar(&o.Cookies, "c", "", "Cookies to use for the requests (dir mode only)")
flag.StringVar(&o.Username, "U", "", "Username for Basic Auth (dir mode only)")
flag.StringVar(&o.Password, "P", "", "Password for Basic Auth (dir mode only)")
flag.StringVar(&o.Extensions, "x", "", "File extension(s) to search for (dir mode only)")
flag.StringVar(&o.UserAgent, "a", "", "Set the User-Agent string (dir mode only)")
flag.StringVar(&o.Proxy, "p", "", "Proxy to use for requests [http(s)://host:port] (dir mode only)")
flag.DurationVar(&o.Timeout, "to", 10*time.Second, "HTTP Timeout in seconds (dir mode only)")
flag.BoolVar(&o.Verbose, "v", false, "Verbose output (errors)")
flag.BoolVar(&o.ShowIPs, "i", false, "Show IP addresses (dns mode only)")
flag.BoolVar(&o.ShowCNAME, "cn", false, "Show CNAME records (dns mode only, cannot be used with '-i' option)")
flag.BoolVar(&o.FollowRedirect, "r", false, "Follow redirects")
flag.BoolVar(&o.Quiet, "q", false, "Don't print the banner and other noise")
flag.BoolVar(&o.Expanded, "e", false, "Expanded mode, print full URLs")
flag.BoolVar(&o.NoStatus, "n", false, "Don't print status codes")
flag.BoolVar(&o.IncludeLength, "l", false, "Include the length of the body in the output (dir mode only)")
flag.BoolVar(&o.UseSlash, "f", false, "Append a forward-slash to each directory request (dir mode only)")
flag.BoolVar(&o.WildcardForced, "fw", false, "Force continued operation when wildcard found")
flag.BoolVar(&o.InsecureSSL, "k", false, "Skip SSL certificate verification")
flag.BoolVar(&o.NoProgress, "np", false, "Don't display progress")
flag.Parse()
// Prompt for PW if not provided
if o.Username != "" && o.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 {
log.Fatal("[!] Auth username given but reading of password failed")
}
o.Password = string(passBytes)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var plugin libgobuster.GobusterPlugin
switch o.Mode {
case libgobuster.ModeDir:
plugin = gobusterdir.GobusterDir{}
case libgobuster.ModeDNS:
plugin = gobusterdns.GobusterDNS{}
}
gobuster, err := libgobuster.NewGobuster(ctx, o, plugin)
if err != nil {
log.Fatalf("[!] %v", err)
}
if !o.Quiet {
fmt.Println("")
ruler()
banner()
ruler()
c, err := gobuster.GetConfigString()
if err != nil {
log.Fatalf("error on creating config string: %v", err)
}
fmt.Println(c)
ruler()
log.Println("Starting gobuster")
ruler()
}
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
go func() {
for range signalChan {
// caught CTRL+C
if !gobuster.Opts.Quiet {
fmt.Println("\n[!] Keyboard interrupt detected, terminating.")
}
cancel()
}
}()
var wg sync.WaitGroup
wg.Add(2)
go errorWorker(gobuster, &wg)
go resultWorker(gobuster, outputFilename, &wg)
if !o.Quiet && !o.NoProgress {
go progressWorker(ctx, gobuster)
}
if err := gobuster.Start(); err != nil {
log.Printf("[!] %v", err)
} else {
// call cancel func to free ressources and stop progressFunc
cancel()
// wait for all output funcs to finish
wg.Wait()
}
if !o.Quiet {
gobuster.ClearProgress()
ruler()
log.Println("Finished")
ruler()
}
cmd.Execute()
}