From 21b23042621125fb45c92f163b0e5fcd98533df8 Mon Sep 17 00:00:00 2001 From: firefart <105281+firefart@users.noreply.github.com> Date: Wed, 30 Aug 2023 01:13:54 +0200 Subject: [PATCH] goodbye cobra --- LICENSE | 10 +- README.md | 1 + cli/cmd/dir.go | 188 ---------------------- cli/cmd/dns.go | 115 -------------- cli/cmd/fuzz.go | 148 ------------------ cli/cmd/gcs.go | 84 ---------- cli/cmd/http.go | 258 ------------------------------- cli/cmd/root.go | 188 ---------------------- cli/cmd/s3.go | 84 ---------- cli/cmd/tftp.go | 80 ---------- cli/cmd/version.go | 27 ---- cli/cmd/vhost.go | 105 ------------- cli/dir/dir.go | 122 +++++++++++++++ cli/{cmd => dir}/dir_test.go | 2 +- cli/dns/dns.go | 75 +++++++++ cli/fuzz/fuzz.go | 107 +++++++++++++ cli/gcs/gcs.go | 61 ++++++++ cli/options.go | 246 +++++++++++++++++++++++++++++ cli/s3/s3.go | 61 ++++++++ cli/tftp/tftp.go | 60 +++++++ cli/vhost/vhost.go | 70 +++++++++ cli/{cmd => vhost}/vhost_test.go | 12 +- go.mod | 7 +- go.sum | 14 +- libgobuster/useragents.go | 2 +- libgobuster/version.go | 20 +++ main.go | 73 +++++++-- 27 files changed, 907 insertions(+), 1313 deletions(-) delete mode 100644 cli/cmd/dir.go delete mode 100644 cli/cmd/dns.go delete mode 100644 cli/cmd/fuzz.go delete mode 100644 cli/cmd/gcs.go delete mode 100644 cli/cmd/http.go delete mode 100644 cli/cmd/root.go delete mode 100644 cli/cmd/s3.go delete mode 100644 cli/cmd/tftp.go delete mode 100644 cli/cmd/version.go delete mode 100644 cli/cmd/vhost.go create mode 100644 cli/dir/dir.go rename cli/{cmd => dir}/dir_test.go (99%) create mode 100644 cli/dns/dns.go create mode 100644 cli/fuzz/fuzz.go create mode 100644 cli/gcs/gcs.go create mode 100644 cli/options.go create mode 100644 cli/s3/s3.go create mode 100644 cli/tftp/tftp.go create mode 100644 cli/vhost/vhost.go rename cli/{cmd => vhost}/vhost_test.go (85%) diff --git a/LICENSE b/LICENSE index e06d208..7a4a3ea 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +179,7 @@ Apache License APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +187,7 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -198,5 +199,4 @@ Apache License distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. - + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md index fd67b83..26079a6 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ All funds that are donated to this project will be donated to charity. A full lo ## 3.7 +- use new cli library that does not rely on global variables - More user friendly error messages - Clean up DNS mode - renamed `show-cname` to `check-cname` in dns mode diff --git a/cli/cmd/dir.go b/cli/cmd/dir.go deleted file mode 100644 index 17209da..0000000 --- a/cli/cmd/dir.go +++ /dev/null @@ -1,188 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "log" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobusterdir" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdDir *cobra.Command - -func runDir(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseDirOptions() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - plugin, err := gobusterdir.NewGobusterDir(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobusterdir: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - var wErr *gobusterdir.ErrWildcard - if errors.As(err, &wErr) { - return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr) - } - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster on %s: %w", pluginopts.URL, err) - } - return nil -} - -func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - - pluginOpts := gobusterdir.NewOptionsDir() - - httpOpts, err := parseCommonHTTPOptions(cmdDir) - if err != nil { - return nil, nil, err - } - pluginOpts.Password = httpOpts.Password - pluginOpts.URL = httpOpts.URL - pluginOpts.UserAgent = httpOpts.UserAgent - pluginOpts.Username = httpOpts.Username - pluginOpts.Proxy = httpOpts.Proxy - pluginOpts.Cookies = httpOpts.Cookies - pluginOpts.Timeout = httpOpts.Timeout - pluginOpts.FollowRedirect = httpOpts.FollowRedirect - pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation - pluginOpts.Headers = httpOpts.Headers - pluginOpts.Method = httpOpts.Method - pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout - pluginOpts.RetryAttempts = httpOpts.RetryAttempts - pluginOpts.TLSCertificate = httpOpts.TLSCertificate - pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders - - pluginOpts.Extensions, err = cmdDir.Flags().GetString("extensions") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) - } - - ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) - } - pluginOpts.ExtensionsParsed = ret - - pluginOpts.ExtensionsFile, err = cmdDir.Flags().GetString("extensions-file") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err) - } - - if pluginOpts.ExtensionsFile != "" { - extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err) - } - pluginOpts.ExtensionsParsed.AddRange(extensions) - } - - // parse normal status codes - pluginOpts.StatusCodes, err = cmdDir.Flags().GetString("status-codes") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err) - } - ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err) - } - pluginOpts.StatusCodesParsed = ret2 - - // blacklist will override the normal status codes - pluginOpts.StatusCodesBlacklist, err = cmdDir.Flags().GetString("status-codes-blacklist") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err) - } - ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err) - } - pluginOpts.StatusCodesBlacklistParsed = ret3 - - if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" { - return nil, nil, fmt.Errorf("status-codes (%q) and status-codes-blacklist (%q) are both set - please set only one. status-codes-blacklist is set by default so you might want to disable it by supplying an empty string.", - pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist) - } - - if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" { - return nil, nil, fmt.Errorf("status-codes and status-codes-blacklist are both not set, please set one") - } - - pluginOpts.UseSlash, err = cmdDir.Flags().GetBool("add-slash") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for add-slash: %w", err) - } - - pluginOpts.Expanded, err = cmdDir.Flags().GetBool("expanded") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for expanded: %w", err) - } - - pluginOpts.NoStatus, err = cmdDir.Flags().GetBool("no-status") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for no-status: %w", err) - } - - pluginOpts.HideLength, err = cmdDir.Flags().GetBool("hide-length") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for hide-length: %w", err) - } - - pluginOpts.DiscoverBackup, err = cmdDir.Flags().GetBool("discover-backup") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for discover-backup: %w", err) - } - - pluginOpts.ExcludeLength, err = cmdDir.Flags().GetString("exclude-length") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - pluginOpts.ExcludeLengthParsed = ret4 - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdDir = &cobra.Command{ - Use: "dir", - Short: "Uses directory/file enumeration mode", - RunE: runDir, - } - - if err := addCommonHTTPOptions(cmdDir); err != nil { - log.Fatalf("%v", err) - } - cmdDir.Flags().StringP("status-codes", "s", "", "Positive status codes (will be overwritten with status-codes-blacklist if set). Can also handle ranges like 200,300-400,404.") - cmdDir.Flags().StringP("status-codes-blacklist", "b", "404", "Negative status codes (will override status-codes if set). Can also handle ranges like 200,300-400,404.") - cmdDir.Flags().StringP("extensions", "x", "", "File extension(s) to search for") - cmdDir.Flags().StringP("extensions-file", "X", "", "Read file extension(s) to search from the file") - cmdDir.Flags().BoolP("expanded", "e", false, "Expanded mode, print full URLs") - cmdDir.Flags().BoolP("no-status", "n", false, "Don't print status codes") - cmdDir.Flags().Bool("hide-length", false, "Hide the length of the body in the output") - cmdDir.Flags().BoolP("add-slash", "f", false, "Append / to each request") - cmdDir.Flags().BoolP("discover-backup", "d", false, "Also search for backup files by appending multiple backup extensions") - cmdDir.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") - - cmdDir.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdDir) -} diff --git a/cli/cmd/dns.go b/cli/cmd/dns.go deleted file mode 100644 index 057bcbc..0000000 --- a/cli/cmd/dns.go +++ /dev/null @@ -1,115 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "log" - "runtime" - "time" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobusterdns" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdDNS *cobra.Command - -func runDNS(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseDNSOptions() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - plugin, err := gobusterdns.NewGobusterDNS(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobusterdns: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - var wErr *gobusterdns.ErrWildcard - if errors.As(err, &wErr) { - return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr) - } - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster on %s: %w", pluginopts.Domain, err) - } - return nil -} - -func parseDNSOptions() (*libgobuster.Options, *gobusterdns.OptionsDNS, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - pluginOpts := gobusterdns.NewOptionsDNS() - - pluginOpts.Domain, err = cmdDNS.Flags().GetString("domain") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for domain: %w", err) - } - - pluginOpts.ShowIPs, err = cmdDNS.Flags().GetBool("show-ips") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for show-ips: %w", err) - } - - pluginOpts.CheckCNAME, err = cmdDNS.Flags().GetBool("check-cname") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for check-cname: %w", err) - } - - pluginOpts.WildcardForced, err = cmdDNS.Flags().GetBool("wildcard") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for wildcard: %w", err) - } - - pluginOpts.Timeout, err = cmdDNS.Flags().GetDuration("timeout") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for timeout: %w", err) - } - - pluginOpts.Resolver, err = cmdDNS.Flags().GetString("resolver") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for resolver: %w", err) - } - - pluginOpts.NoFQDN, err = cmdDNS.Flags().GetBool("no-fqdn") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for no-fqdn: %w", err) - } - - if pluginOpts.Resolver != "" && runtime.GOOS == "windows" { - return nil, nil, fmt.Errorf("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution") - } - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdDNS = &cobra.Command{ - Use: "dns", - Short: "Uses DNS subdomain enumeration mode", - RunE: runDNS, - } - - cmdDNS.Flags().StringP("domain", "d", "", "The target domain") - cmdDNS.Flags().BoolP("show-ips", "i", false, "Show IP addresses of found domains") - cmdDNS.Flags().BoolP("check-cname", "c", false, "Also check CNAME records") - cmdDNS.Flags().DurationP("timeout", "", time.Second, "DNS resolver timeout") - cmdDNS.Flags().BoolP("wildcard", "", false, "Force continued operation when wildcard found") - cmdDNS.Flags().BoolP("no-fqdn", "", false, "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain") - cmdDNS.Flags().StringP("resolver", "r", "", "Use custom DNS server (format server.com or server.com:port)") - if err := cmdDNS.MarkFlagRequired("domain"); err != nil { - log.Fatalf("error on marking flag as required: %v", err) - } - - cmdDNS.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdDNS) -} diff --git a/cli/cmd/fuzz.go b/cli/cmd/fuzz.go deleted file mode 100644 index dc4ea42..0000000 --- a/cli/cmd/fuzz.go +++ /dev/null @@ -1,148 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "log" - "strings" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobusterfuzz" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdFuzz *cobra.Command - -func runFuzz(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseFuzzOptions() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - if !containsFuzzKeyword(*pluginopts) { - return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword) - } - - plugin, err := gobusterfuzz.NewGobusterFuzz(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobusterfuzz: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - var wErr *gobusterfuzz.ErrWildcard - if errors.As(err, &wErr) { - return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr) - } - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster on %s: %w", pluginopts.URL, err) - } - return nil -} - -func parseFuzzOptions() (*libgobuster.Options, *gobusterfuzz.OptionsFuzz, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - - pluginOpts := gobusterfuzz.NewOptionsFuzz() - - httpOpts, err := parseCommonHTTPOptions(cmdFuzz) - if err != nil { - return nil, nil, err - } - pluginOpts.Password = httpOpts.Password - pluginOpts.URL = httpOpts.URL - pluginOpts.UserAgent = httpOpts.UserAgent - pluginOpts.Username = httpOpts.Username - pluginOpts.Proxy = httpOpts.Proxy - pluginOpts.Cookies = httpOpts.Cookies - pluginOpts.Timeout = httpOpts.Timeout - pluginOpts.FollowRedirect = httpOpts.FollowRedirect - pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation - pluginOpts.Headers = httpOpts.Headers - pluginOpts.Method = httpOpts.Method - pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout - pluginOpts.RetryAttempts = httpOpts.RetryAttempts - pluginOpts.TLSCertificate = httpOpts.TLSCertificate - pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders - - // blacklist will override the normal status codes - pluginOpts.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) - } - ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) - } - pluginOpts.ExcludedStatusCodesParsed = ret - - pluginOpts.ExcludeLength, err = cmdFuzz.Flags().GetString("exclude-length") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - pluginOpts.ExcludeLengthParsed = ret2 - - pluginOpts.RequestBody, err = cmdFuzz.Flags().GetString("body") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for body: %w", err) - } - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdFuzz = &cobra.Command{ - Use: "fuzz", - Short: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword), - RunE: runFuzz, - } - - if err := addCommonHTTPOptions(cmdFuzz); err != nil { - log.Fatalf("%v", err) - } - cmdFuzz.Flags().StringP("excludestatuscodes", "b", "", "Excluded status codes. Can also handle ranges like 200,300-400,404.") - cmdFuzz.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") - cmdFuzz.Flags().StringP("body", "B", "", "Request body") - - cmdFuzz.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdFuzz) -} - -func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool { - if strings.Contains(pluginopts.URL, gobusterfuzz.FuzzKeyword) { - return true - } - - if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) { - return true - } - - for _, h := range pluginopts.Headers { - if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) { - return true - } - } - - if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) { - return true - } - - if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) { - return true - } - - return false -} diff --git a/cli/cmd/gcs.go b/cli/cmd/gcs.go deleted file mode 100644 index b1a8789..0000000 --- a/cli/cmd/gcs.go +++ /dev/null @@ -1,84 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobustergcs" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdGCS *cobra.Command - -func runGCS(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseGCSOptions() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - plugin, err := gobustergcs.NewGobusterGCS(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobustergcs: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster: %w", err) - } - return nil -} - -func parseGCSOptions() (*libgobuster.Options, *gobustergcs.OptionsGCS, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - - pluginopts := gobustergcs.NewOptionsGCS() - - httpOpts, err := parseBasicHTTPOptions(cmdGCS) - if err != nil { - return nil, nil, err - } - - pluginopts.UserAgent = httpOpts.UserAgent - pluginopts.Proxy = httpOpts.Proxy - pluginopts.Timeout = httpOpts.Timeout - pluginopts.NoTLSValidation = httpOpts.NoTLSValidation - pluginopts.RetryOnTimeout = httpOpts.RetryOnTimeout - pluginopts.RetryAttempts = httpOpts.RetryAttempts - pluginopts.TLSCertificate = httpOpts.TLSCertificate - - pluginopts.MaxFilesToList, err = cmdGCS.Flags().GetInt("maxfiles") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err) - } - pluginopts.ShowFiles, err = cmdGCS.Flags().GetBool("show-files") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for show-files: %w", err) - } - - return globalopts, pluginopts, nil -} - -// nolint:gochecknoinits -func init() { - cmdGCS = &cobra.Command{ - Use: "gcs", - Short: "Uses gcs bucket enumeration mode", - RunE: runGCS, - } - - addBasicHTTPOptions(cmdGCS) - cmdGCS.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets") - cmdGCS.Flags().BoolP("show-files", "s", true, "show files from found buckets") - - cmdGCS.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdGCS) -} diff --git a/cli/cmd/http.go b/cli/cmd/http.go deleted file mode 100644 index c0747fe..0000000 --- a/cli/cmd/http.go +++ /dev/null @@ -1,258 +0,0 @@ -package cmd - -import ( - "crypto/tls" - "encoding/pem" - "fmt" - "os" - "regexp" - "strconv" - "strings" - "syscall" - "time" - - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" - "golang.org/x/crypto/pkcs12" - "golang.org/x/term" -) - -func addBasicHTTPOptions(cmd *cobra.Command) { - cmd.Flags().StringP("useragent", "a", libgobuster.DefaultUserAgent(), "Set the User-Agent string") - cmd.Flags().BoolP("random-agent", "", false, "Use a random User-Agent string") - cmd.Flags().StringP("proxy", "", "", "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]") - cmd.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout") - cmd.Flags().BoolP("no-tls-validation", "k", false, "Skip TLS certificate verification") - cmd.Flags().BoolP("retry", "", false, "Should retry on request timeout") - cmd.Flags().IntP("retry-attempts", "", 3, "Times to retry on request timeout") - // client certificates, either pem or p12 - cmd.Flags().StringP("client-cert-pem", "", "", "public key in PEM format for optional TLS client certificates") - cmd.Flags().StringP("client-cert-pem-key", "", "", "private key in PEM format for optional TLS client certificates (this key needs to have no password)") - cmd.Flags().StringP("client-cert-p12", "", "", "a p12 file to use for options TLS client certificates") - cmd.Flags().StringP("client-cert-p12-password", "", "", "the password to the p12 file") -} - -func addCommonHTTPOptions(cmd *cobra.Command) error { - addBasicHTTPOptions(cmd) - cmd.Flags().StringP("url", "u", "", "The target URL") - cmd.Flags().StringP("cookies", "c", "", "Cookies to use for the requests") - cmd.Flags().StringP("username", "U", "", "Username for Basic Auth") - cmd.Flags().StringP("password", "P", "", "Password for Basic Auth") - cmd.Flags().BoolP("follow-redirect", "r", false, "Follow redirects") - cmd.Flags().StringArrayP("headers", "H", []string{""}, "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'") - cmd.Flags().BoolP("no-canonicalize-headers", "", false, "Do not canonicalize HTTP header names. If set header names are sent as is.") - cmd.Flags().StringP("method", "m", "GET", "Use the following HTTP method") - - if err := cmd.MarkFlagRequired("url"); err != nil { - return fmt.Errorf("error on marking flag as required: %w", err) - } - - return nil -} - -func parseBasicHTTPOptions(cmd *cobra.Command) (libgobuster.BasicHTTPOptions, error) { - options := libgobuster.BasicHTTPOptions{} - var err error - - options.UserAgent, err = cmd.Flags().GetString("useragent") - if err != nil { - return options, fmt.Errorf("invalid value for useragent: %w", err) - } - randomUA, err := cmd.Flags().GetBool("random-agent") - if err != nil { - return options, fmt.Errorf("invalid value for random-agent: %w", err) - } - if randomUA { - ua, err := libgobuster.GetRandomUserAgent() - if err != nil { - return options, err - } - options.UserAgent = ua - } - - options.Proxy, err = cmd.Flags().GetString("proxy") - if err != nil { - return options, fmt.Errorf("invalid value for proxy: %w", err) - } - - options.Timeout, err = cmd.Flags().GetDuration("timeout") - if err != nil { - return options, fmt.Errorf("invalid value for timeout: %w", err) - } - - options.RetryOnTimeout, err = cmd.Flags().GetBool("retry") - if err != nil { - return options, fmt.Errorf("invalid value for retry: %w", err) - } - - options.RetryAttempts, err = cmd.Flags().GetInt("retry-attempts") - if err != nil { - return options, fmt.Errorf("invalid value for retry-attempts: %w", err) - } - - options.NoTLSValidation, err = cmd.Flags().GetBool("no-tls-validation") - if err != nil { - return options, fmt.Errorf("invalid value for no-tls-validation: %w", err) - } - - pemFile, err := cmd.Flags().GetString("client-cert-pem") - if err != nil { - return options, fmt.Errorf("invalid value for client-cert-pem: %w", err) - } - pemKeyFile, err := cmd.Flags().GetString("client-cert-pem-key") - if err != nil { - return options, fmt.Errorf("invalid value for client-cert-pem-key: %w", err) - } - p12File, err := cmd.Flags().GetString("client-cert-p12") - if err != nil { - return options, fmt.Errorf("invalid value for client-cert-p12: %w", err) - } - p12Pass, err := cmd.Flags().GetString("client-cert-p12-password") - if err != nil { - return options, fmt.Errorf("invalid value for client-cert-p12-password: %w", err) - } - - if pemFile != "" && p12File != "" { - return options, fmt.Errorf("please supply either a pem or a p12, not both") - } - - if pemFile != "" { - cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile) - if err != nil { - return options, fmt.Errorf("could not load supplied pem key: %w", err) - } - options.TLSCertificate = &cert - } else if p12File != "" { - p12Content, err := os.ReadFile(p12File) - if err != nil { - return options, fmt.Errorf("could not read p12 %s: %w", p12File, err) - } - blocks, err := pkcs12.ToPEM(p12Content, p12Pass) - if err != nil { - return options, fmt.Errorf("could not load P12: %w", err) - } - var pemData []byte - for _, b := range blocks { - pemData = append(pemData, pem.EncodeToMemory(b)...) - } - cert, err := tls.X509KeyPair(pemData, pemData) - if err != nil { - return options, fmt.Errorf("could not load certificate from P12: %w", err) - } - options.TLSCertificate = &cert - } - - return options, nil -} - -func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error) { - options := libgobuster.HTTPOptions{} - var err error - - basic, err := parseBasicHTTPOptions(cmd) - if err != nil { - return options, err - } - options.Proxy = basic.Proxy - options.Timeout = basic.Timeout - options.UserAgent = basic.UserAgent - options.NoTLSValidation = basic.NoTLSValidation - options.RetryOnTimeout = basic.RetryOnTimeout - options.RetryAttempts = basic.RetryAttempts - options.TLSCertificate = basic.TLSCertificate - - options.URL, err = cmd.Flags().GetString("url") - if err != nil { - return options, fmt.Errorf("invalid value for url: %w", err) - } - - if !strings.HasPrefix(options.URL, "http") { - // check to see if a port was specified - re := regexp.MustCompile(`^[^/]+:(\d+)`) - match := re.FindStringSubmatch(options.URL) - - if len(match) < 2 { - // no port, default to http on 80 - options.URL = fmt.Sprintf("http://%s", options.URL) - } else { - port, err2 := strconv.Atoi(match[1]) - if err2 != nil || (port != 80 && port != 443) { - return options, fmt.Errorf("url scheme not specified") - } else if port == 80 { - options.URL = fmt.Sprintf("http://%s", options.URL) - } else { - options.URL = fmt.Sprintf("https://%s", options.URL) - } - } - } - - options.Cookies, err = cmd.Flags().GetString("cookies") - if err != nil { - return options, fmt.Errorf("invalid value for cookies: %w", err) - } - - options.Username, err = cmd.Flags().GetString("username") - if err != nil { - return options, fmt.Errorf("invalid value for username: %w", err) - } - - options.Password, err = cmd.Flags().GetString("password") - if err != nil { - return options, fmt.Errorf("invalid value for password: %w", err) - } - - options.FollowRedirect, err = cmd.Flags().GetBool("follow-redirect") - if err != nil { - return options, fmt.Errorf("invalid value for follow-redirect: %w", err) - } - - options.Method, err = cmd.Flags().GetString("method") - if err != nil { - return options, fmt.Errorf("invalid value for method: %w", err) - } - - headers, err := cmd.Flags().GetStringArray("headers") - if err != nil { - return options, fmt.Errorf("invalid value for headers: %w", err) - } - - for _, h := range headers { - keyAndValue := strings.SplitN(h, ":", 2) - if len(keyAndValue) != 2 { - return options, fmt.Errorf("invalid header format for header %q", h) - } - key := strings.TrimSpace(keyAndValue[0]) - value := strings.TrimSpace(keyAndValue[1]) - if len(key) == 0 { - return options, fmt.Errorf("invalid header format for header %q - name is empty", h) - } - header := libgobuster.HTTPHeader{Name: key, Value: value} - options.Headers = append(options.Headers, header) - } - - noCanonHeaders, err := cmd.Flags().GetBool("no-canonicalize-headers") - if err != nil { - return options, fmt.Errorf("invalid value for no-canonicalize-headers: %w", err) - } - options.NoCanonicalizeHeaders = noCanonHeaders - - // Prompt for PW if not provided - if options.Username != "" && options.Password == "" { - fmt.Printf("[?] Auth Password: ") - // please don't remove the int cast here as it is sadly needed on windows :/ - passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert - // 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 options, fmt.Errorf("username given but reading of password failed") - } - options.Password = string(passBytes) - } - // if it's still empty bail out - if options.Username != "" && options.Password == "" { - return options, fmt.Errorf("username was provided but password is missing") - } - - return options, nil -} diff --git a/cli/cmd/root.go b/cli/cmd/root.go deleted file mode 100644 index dd8bbd1..0000000 --- a/cli/cmd/root.go +++ /dev/null @@ -1,188 +0,0 @@ -package cmd - -import ( - "bufio" - "context" - "fmt" - "log" - "os" - "os/signal" - - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/fatih/color" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var rootCmd = &cobra.Command{ - Use: "gobuster", - SilenceUsage: true, -} - -// nolint:gochecknoglobals -var mainContext context.Context - -// Execute is the main cobra method -func Execute() { - var cancel context.CancelFunc - mainContext, cancel = context.WithCancel(context.Background()) - defer cancel() - - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt) - defer func() { - signal.Stop(signalChan) - cancel() - }() - go func() { - select { - case <-signalChan: - // caught CTRL+C - fmt.Println("\n[!] Keyboard interrupt detected, terminating.") - cancel() - case <-mainContext.Done(): - } - }() - - if err := rootCmd.Execute(); err != nil { - // Leaving this in results in the same error appearing twice - // Once before and once after the help output. Not sure if - // this is going to be needed to output other errors that - // aren't automatically outputted. - // 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: %w", err) - } - - if threads <= 0 { - return nil, fmt.Errorf("threads must be bigger than 0") - } - globalopts.Threads = threads - - delay, err := rootCmd.Flags().GetDuration("delay") - if err != nil { - return nil, fmt.Errorf("invalid value for delay: %w", err) - } - - if delay < 0 { - return nil, fmt.Errorf("delay must be positive") - } - globalopts.Delay = delay - - globalopts.Wordlist, err = rootCmd.Flags().GetString("wordlist") - if err != nil { - return nil, fmt.Errorf("invalid value for wordlist: %w", err) - } - - if globalopts.Wordlist == "-" { - // STDIN - } else if _, err2 := os.Stat(globalopts.Wordlist); os.IsNotExist(err2) { - return nil, fmt.Errorf("wordlist file %q does not exist: %w", globalopts.Wordlist, err2) - } - - offset, err := rootCmd.Flags().GetInt("wordlist-offset") - if err != nil { - return nil, fmt.Errorf("invalid value for wordlist-offset: %w", err) - } - - if offset < 0 { - return nil, fmt.Errorf("wordlist-offset must be bigger or equal to 0") - } - globalopts.WordlistOffset = offset - - if globalopts.Wordlist == "-" && globalopts.WordlistOffset > 0 { - return nil, fmt.Errorf("wordlist-offset is not supported when reading from STDIN") - } - - globalopts.PatternFile, err = rootCmd.Flags().GetString("pattern") - if err != nil { - return nil, fmt.Errorf("invalid value for pattern: %w", err) - } - - if globalopts.PatternFile != "" { - if _, err = os.Stat(globalopts.PatternFile); os.IsNotExist(err) { - return nil, fmt.Errorf("pattern file %q does not exist: %w", globalopts.PatternFile, err) - } - patternFile, err := os.Open(globalopts.PatternFile) - if err != nil { - return nil, fmt.Errorf("could not open pattern file %q: %w", globalopts.PatternFile, err) - } - defer patternFile.Close() - - scanner := bufio.NewScanner(patternFile) - for scanner.Scan() { - globalopts.Patterns = append(globalopts.Patterns, scanner.Text()) - } - if err := scanner.Err(); err != nil { - return nil, fmt.Errorf("could not read pattern file %q: %w", globalopts.PatternFile, err) - } - } - - globalopts.OutputFilename, err = rootCmd.Flags().GetString("output") - if err != nil { - return nil, fmt.Errorf("invalid value for output filename: %w", err) - } - - globalopts.Quiet, err = rootCmd.Flags().GetBool("quiet") - if err != nil { - return nil, fmt.Errorf("invalid value for quiet: %w", err) - } - - globalopts.NoProgress, err = rootCmd.Flags().GetBool("no-progress") - if err != nil { - return nil, fmt.Errorf("invalid value for no-progress: %w", err) - } - - globalopts.NoError, err = rootCmd.Flags().GetBool("no-error") - if err != nil { - return nil, fmt.Errorf("invalid value for no-error: %w", err) - } - - noColor, err := rootCmd.Flags().GetBool("no-color") - if err != nil { - return nil, fmt.Errorf("invalid value for no-color: %w", err) - } - if noColor { - color.NoColor = true - } - - globalopts.Debug, err = rootCmd.Flags().GetBool("debug") - if err != nil { - return nil, fmt.Errorf("invalid value for debug: %w", err) - } - - return globalopts, nil -} - -// This has to be called as part of the pre-run for sub commands. Including -// this in the init() function results in the built-in `help` command not -// working as intended. The required flags should only be marked as required -// on the global flags when one of the non-help commands is used. -func configureGlobalOptions() { - if err := rootCmd.MarkPersistentFlagRequired("wordlist"); err != nil { - log.Fatalf("error on marking flag as required: %v", err) - } -} - -// nolint:gochecknoinits -func init() { - rootCmd.PersistentFlags().DurationP("delay", "", 0, "Time each thread waits between requests (e.g. 1500ms)") - rootCmd.PersistentFlags().IntP("threads", "t", 10, "Number of concurrent threads") - rootCmd.PersistentFlags().StringP("wordlist", "w", "", "Path to the wordlist. Set to - to use STDIN.") - rootCmd.PersistentFlags().IntP("wordlist-offset", "", 0, "Resume from a given position in the wordlist (defaults to 0)") - rootCmd.PersistentFlags().StringP("output", "o", "", "Output file to write results to (defaults to stdout)") - rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Don't print the banner and other noise") - rootCmd.PersistentFlags().BoolP("no-progress", "z", false, "Don't display progress") - rootCmd.PersistentFlags().Bool("no-error", false, "Don't display errors") - rootCmd.PersistentFlags().StringP("pattern", "p", "", "File containing replacement patterns") - rootCmd.PersistentFlags().Bool("no-color", false, "Disable color output") - rootCmd.PersistentFlags().Bool("debug", false, "Enable debug output") -} diff --git a/cli/cmd/s3.go b/cli/cmd/s3.go deleted file mode 100644 index 646ca3a..0000000 --- a/cli/cmd/s3.go +++ /dev/null @@ -1,84 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobusters3" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdS3 *cobra.Command - -func runS3(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseS3Options() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - plugin, err := gobusters3.NewGobusterS3(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobusters3: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster: %w", err) - } - return nil -} - -func parseS3Options() (*libgobuster.Options, *gobusters3.OptionsS3, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - - pluginOpts := gobusters3.NewOptionsS3() - - httpOpts, err := parseBasicHTTPOptions(cmdS3) - if err != nil { - return nil, nil, err - } - - pluginOpts.UserAgent = httpOpts.UserAgent - pluginOpts.Proxy = httpOpts.Proxy - pluginOpts.Timeout = httpOpts.Timeout - pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation - pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout - pluginOpts.RetryAttempts = httpOpts.RetryAttempts - pluginOpts.TLSCertificate = httpOpts.TLSCertificate - - pluginOpts.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err) - } - pluginOpts.ShowFiles, err = cmdS3.Flags().GetBool("show-files") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for show-files: %w", err) - } - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdS3 = &cobra.Command{ - Use: "s3", - Short: "Uses aws bucket enumeration mode", - RunE: runS3, - } - - addBasicHTTPOptions(cmdS3) - cmdS3.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets (only shown in verbose mode)") - cmdS3.Flags().BoolP("show-files", "s", true, "show files from found buckets") - - cmdS3.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdS3) -} diff --git a/cli/cmd/tftp.go b/cli/cmd/tftp.go deleted file mode 100644 index 69c627e..0000000 --- a/cli/cmd/tftp.go +++ /dev/null @@ -1,80 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - "strings" - "time" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobustertftp" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdTFTP *cobra.Command - -func runTFTP(cmd *cobra.Command, args []string) error { - globalopts, pluginopts, err := parseTFTPOptions() - if err != nil { - return fmt.Errorf("error on parsing arguments: %w", err) - } - - plugin, err := gobustertftp.NewGobusterTFTP(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobustertftp: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster on %s: %w", pluginopts.Server, err) - } - return nil -} - -func parseTFTPOptions() (*libgobuster.Options, *gobustertftp.OptionsTFTP, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - pluginOpts := gobustertftp.NewOptionsTFTP() - - pluginOpts.Server, err = cmdTFTP.Flags().GetString("server") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for domain: %w", err) - } - - if !strings.Contains(pluginOpts.Server, ":") { - pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server) - } - - pluginOpts.Timeout, err = cmdTFTP.Flags().GetDuration("timeout") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for timeout: %w", err) - } - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdTFTP = &cobra.Command{ - Use: "tftp", - Short: "Uses TFTP enumeration mode", - RunE: runTFTP, - } - - cmdTFTP.Flags().StringP("server", "s", "", "The target TFTP server") - cmdTFTP.Flags().DurationP("timeout", "", time.Second, "TFTP timeout") - if err := cmdTFTP.MarkFlagRequired("server"); err != nil { - log.Fatalf("error on marking flag as required: %v", err) - } - - cmdTFTP.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdTFTP) -} diff --git a/cli/cmd/version.go b/cli/cmd/version.go deleted file mode 100644 index 4a63d50..0000000 --- a/cli/cmd/version.go +++ /dev/null @@ -1,27 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -var cmdVersion *cobra.Command - -func runVersion(cmd *cobra.Command, args []string) error { - fmt.Println(libgobuster.VERSION) - return nil -} - -// nolint:gochecknoinits -func init() { - cmdVersion = &cobra.Command{ - Use: "version", - Short: "shows the current version", - RunE: runVersion, - } - - rootCmd.AddCommand(cmdVersion) -} diff --git a/cli/cmd/vhost.go b/cli/cmd/vhost.go deleted file mode 100644 index 8b7c7d5..0000000 --- a/cli/cmd/vhost.go +++ /dev/null @@ -1,105 +0,0 @@ -package cmd - -import ( - "fmt" - "log" - - "github.com/OJ/gobuster/v3/cli" - "github.com/OJ/gobuster/v3/gobustervhost" - "github.com/OJ/gobuster/v3/libgobuster" - "github.com/spf13/cobra" -) - -// nolint:gochecknoglobals -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: %w", err) - } - - plugin, err := gobustervhost.NewGobusterVhost(globalopts, pluginopts) - if err != nil { - return fmt.Errorf("error on creating gobustervhost: %w", err) - } - - log := libgobuster.NewLogger(globalopts.Debug) - if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { - log.Debugf("%#v", err) - return fmt.Errorf("error on running gobuster on %s: %w", pluginopts.URL, err) - } - return nil -} - -func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, error) { - globalopts, err := parseGlobalOptions() - if err != nil { - return nil, nil, err - } - - pluginOpts := gobustervhost.NewOptionsVhost() - - httpOpts, err := parseCommonHTTPOptions(cmdVhost) - if err != nil { - return nil, nil, err - } - pluginOpts.Password = httpOpts.Password - pluginOpts.URL = httpOpts.URL - pluginOpts.UserAgent = httpOpts.UserAgent - pluginOpts.Username = httpOpts.Username - pluginOpts.Proxy = httpOpts.Proxy - pluginOpts.Cookies = httpOpts.Cookies - pluginOpts.Timeout = httpOpts.Timeout - pluginOpts.FollowRedirect = httpOpts.FollowRedirect - pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation - pluginOpts.Headers = httpOpts.Headers - pluginOpts.Method = httpOpts.Method - pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout - pluginOpts.RetryAttempts = httpOpts.RetryAttempts - pluginOpts.TLSCertificate = httpOpts.TLSCertificate - pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders - - pluginOpts.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for append-domain: %w", err) - } - - pluginOpts.ExcludeLength, err = cmdVhost.Flags().GetString("exclude-length") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) - if err != nil { - return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) - } - pluginOpts.ExcludeLengthParsed = ret - - pluginOpts.Domain, err = cmdVhost.Flags().GetString("domain") - if err != nil { - return nil, nil, fmt.Errorf("invalid value for domain: %w", err) - } - - return globalopts, pluginOpts, nil -} - -// nolint:gochecknoinits -func init() { - cmdVhost = &cobra.Command{ - Use: "vhost", - Short: "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)", - RunE: runVhost, - } - if err := addCommonHTTPOptions(cmdVhost); err != nil { - log.Fatalf("%v", err) - } - cmdVhost.Flags().BoolP("append-domain", "", false, "Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.") - cmdVhost.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") - cmdVhost.Flags().String("domain", "", "the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted") - - cmdVhost.PersistentPreRun = func(cmd *cobra.Command, args []string) { - configureGlobalOptions() - } - - rootCmd.AddCommand(cmdVhost) -} diff --git a/cli/dir/dir.go b/cli/dir/dir.go new file mode 100644 index 0000000..25980ea --- /dev/null +++ b/cli/dir/dir.go @@ -0,0 +1,122 @@ +package dir + +import ( + "errors" + "fmt" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobusterdir" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "dir", + Usage: "Uses directory/file enumeration mode", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, internalcli.CommonHTTPOptions()...) + flags = append(flags, internalcli.GlobalOptions()...) + flags = append(flags, []cli.Flag{ + &cli.StringFlag{Name: "status-codes", Aliases: []string{"s"}, Usage: "Positive status codes (will be overwritten with status-codes-blacklist if set). Can also handle ranges like 200,300-400,404"}, + &cli.StringFlag{Name: "status-codes-blacklist", Aliases: []string{"b"}, Usage: "Negative status codes (will override status-codes if set). Can also handle ranges like 200,300-400,404.", Value: "404"}, + &cli.StringFlag{Name: "extensions", Aliases: []string{"x"}, Usage: "File extension(s) to search for"}, + &cli.StringFlag{Name: "extensions-file", Aliases: []string{"X"}, Usage: "Read file extension(s) to search from the file"}, + &cli.BoolFlag{Name: "expanded", Aliases: []string{"e"}, Value: false, Usage: "Expanded mode, print full URLs"}, + &cli.BoolFlag{Name: "no-status", Aliases: []string{"n"}, Value: false, Usage: "Don't print status codes"}, + &cli.BoolFlag{Name: "hide-length", Value: false, Usage: "Hide the length of the body in the output"}, + &cli.BoolFlag{Name: "add-slash", Aliases: []string{"f"}, Value: false, Usage: "Append / to each request"}, + &cli.BoolFlag{Name: "discover-backup", Value: false, Usage: "Also search for backup files by appending multiple backup extensions"}, + &cli.StringFlag{Name: "exclude-length", Aliases: []string{"l"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"}, + }...) + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobusterdir.NewOptionsDir() + + httpOptions, err := internalcli.ParseCommonHTTPOptions(c) + if err != nil { + return err + } + pluginOpts.HTTPOptions = httpOptions + + pluginOpts.Extensions = c.String("extensions") + ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions) + if err != nil { + return fmt.Errorf("invalid value for extensions: %w", err) + } + pluginOpts.ExtensionsParsed = ret + + pluginOpts.ExtensionsFile = c.String("extensions-file") + if pluginOpts.ExtensionsFile != "" { + extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile) + if err != nil { + return fmt.Errorf("invalid value for extensions file: %w", err) + } + pluginOpts.ExtensionsParsed.AddRange(extensions) + } + + pluginOpts.StatusCodes = c.String("status-codes") + ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes) + if err != nil { + return fmt.Errorf("invalid value for status-codes: %w", err) + } + pluginOpts.StatusCodesParsed = ret2 + + pluginOpts.StatusCodesBlacklist = c.String("status-codes-blacklist") + ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist) + if err != nil { + return fmt.Errorf("invalid value for status-codes-blacklist: %w", err) + } + pluginOpts.StatusCodesBlacklistParsed = ret3 + + if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" { + return fmt.Errorf("status-codes (%q) and status-codes-blacklist (%q) are both set - please set only one. status-codes-blacklist is set by default so you might want to disable it by supplying an empty string.", + pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist) + } + + if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" { + return fmt.Errorf("status-codes and status-codes-blacklist are both not set, please set one") + } + + pluginOpts.UseSlash = c.Bool("add-slash") + pluginOpts.Expanded = c.Bool("expanded") + pluginOpts.NoStatus = c.Bool("no-status") + pluginOpts.HideLength = c.Bool("hide-length") + pluginOpts.DiscoverBackup = c.Bool("discover-backup") + pluginOpts.ExcludeLength = c.String("exclude-length") + ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) + if err != nil { + return fmt.Errorf("invalid value for exclude-length: %w", err) + } + pluginOpts.ExcludeLengthParsed = ret4 + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobusterdir.NewGobusterDir(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobusterdir: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + var wErr *gobusterdir.ErrWildcard + if errors.As(err, &wErr) { + return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr) + } + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err) + } + return nil +} diff --git a/cli/cmd/dir_test.go b/cli/dir/dir_test.go similarity index 99% rename from cli/cmd/dir_test.go rename to cli/dir/dir_test.go index 568fd48..347b539 100644 --- a/cli/cmd/dir_test.go +++ b/cli/dir/dir_test.go @@ -1,4 +1,4 @@ -package cmd +package dir import ( "context" diff --git a/cli/dns/dns.go b/cli/dns/dns.go new file mode 100644 index 0000000..5136065 --- /dev/null +++ b/cli/dns/dns.go @@ -0,0 +1,75 @@ +package dns + +import ( + "errors" + "fmt" + "runtime" + "time" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobusterdns" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "dns", + Usage: "Uses DNS subdomain enumeration mode", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, []cli.Flag{ + &cli.StringFlag{Name: "domain", Aliases: []string{"do"}, Usage: "The target domain", Required: true}, + &cli.BoolFlag{Name: "show-ips", Aliases: []string{"i"}, Value: false, Usage: "Show IP addresses of found domains"}, + &cli.BoolFlag{Name: "check-cname", Aliases: []string{"c"}, Value: false, Usage: "Also check CNAME records"}, + &cli.DurationFlag{Name: "timeout", Value: 1 * time.Second, Usage: "DNS resolver timeout"}, + &cli.BoolFlag{Name: "wildcard", Value: false, Usage: "Force continued operation when wildcard found"}, + &cli.BoolFlag{Name: "no-fqdn", Value: false, Usage: "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain"}, + &cli.StringFlag{Name: "resolver", Usage: "Use custom DNS server (format server.com or server.com:port)"}, + }...) + flags = append(flags, internalcli.GlobalOptions()...) + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobusterdns.NewOptionsDNS() + + pluginOpts.Domain = c.String("domain") + pluginOpts.ShowIPs = c.Bool("show-ips") + pluginOpts.CheckCNAME = c.Bool("check-cname") + pluginOpts.Timeout = c.Duration("timeout") + pluginOpts.WildcardForced = c.Bool("wildcard") + pluginOpts.NoFQDN = c.Bool("no-fqdn") + pluginOpts.Resolver = c.String("resolver") + + if pluginOpts.Resolver != "" && runtime.GOOS == "windows" { + return fmt.Errorf("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution") + } + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobusterdns.NewGobusterDNS(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobusterdns: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + var wErr *gobusterdns.ErrWildcard + if errors.As(err, &wErr) { + return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr) + } + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Domain, err) + } + return nil +} diff --git a/cli/fuzz/fuzz.go b/cli/fuzz/fuzz.go new file mode 100644 index 0000000..a69fd59 --- /dev/null +++ b/cli/fuzz/fuzz.go @@ -0,0 +1,107 @@ +package fuzz + +import ( + "fmt" + "strings" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobusterfuzz" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "fuzz", + Usage: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword), + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, internalcli.CommonHTTPOptions()...) + flags = append(flags, internalcli.GlobalOptions()...) + flags = append(flags, []cli.Flag{ + &cli.StringFlag{Name: "excludestatuscodes", Aliases: []string{"b"}, Usage: "Excluded status codes. Can also handle ranges like 200,300-400,404."}, + &cli.StringFlag{Name: "exclude-length", Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"}, + &cli.StringFlag{Name: "body", Aliases: []string{"B"}, Usage: "Request body"}, + }...) + + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobusterfuzz.NewOptionsFuzz() + + httpOptions, err := internalcli.ParseCommonHTTPOptions(c) + if err != nil { + return err + } + pluginOpts.HTTPOptions = httpOptions + + pluginOpts.ExcludedStatusCodes = c.String("excludestatuscodes") + ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes) + if err != nil { + return fmt.Errorf("invalid value for excludestatuscodes: %w", err) + } + pluginOpts.ExcludedStatusCodesParsed = ret + + pluginOpts.ExcludeLength = c.String("exclude-length") + ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) + if err != nil { + return fmt.Errorf("invalid value for exclude-length: %w", err) + } + pluginOpts.ExcludeLengthParsed = ret2 + + pluginOpts.RequestBody = c.String("body") + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + if !containsFuzzKeyword(*pluginOpts) { + return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword) + } + + plugin, err := gobusterfuzz.NewGobusterFuzz(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobusterfuzz: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err) + } + return nil +} + +func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool { + if strings.Contains(pluginopts.URL, gobusterfuzz.FuzzKeyword) { + return true + } + + if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) { + return true + } + + for _, h := range pluginopts.Headers { + if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) { + return true + } + } + + if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) { + return true + } + + if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) { + return true + } + + return false +} diff --git a/cli/gcs/gcs.go b/cli/gcs/gcs.go new file mode 100644 index 0000000..6968348 --- /dev/null +++ b/cli/gcs/gcs.go @@ -0,0 +1,61 @@ +package gcs + +import ( + "fmt" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobustergcs" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "gcs", + Usage: "Uses gcs bucket enumeration mode", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, []cli.Flag{ + &cli.IntFlag{Name: "maxfiles", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"}, + &cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"}, + }...) + flags = append(flags, internalcli.GlobalOptions()...) + flags = append(flags, internalcli.BasicHTTPOptions()...) + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobustergcs.NewOptionsGCS() + + httpOptions, err := internalcli.ParseBasicHTTPOptions(c) + if err != nil { + return err + } + pluginOpts.BasicHTTPOptions = httpOptions + + pluginOpts.MaxFilesToList = c.Int("maxfiles") + pluginOpts.ShowFiles = c.Bool("show-files") + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobustergcs.NewGobusterGCS(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobustergcs: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster: %w", err) + } + return nil +} diff --git a/cli/options.go b/cli/options.go new file mode 100644 index 0000000..ac734d0 --- /dev/null +++ b/cli/options.go @@ -0,0 +1,246 @@ +package cli + +import ( + "bufio" + "crypto/tls" + "encoding/pem" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/fatih/color" + "github.com/urfave/cli/v2" + "golang.org/x/crypto/pkcs12" + "golang.org/x/term" +) + +func BasicHTTPOptions() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{Name: "useragent", Aliases: []string{"a"}, Value: libgobuster.DefaultUserAgent(), Usage: "Set the User-Agent string"}, + &cli.BoolFlag{Name: "random-agent", Value: false, Usage: "Use a random User-Agent string"}, + &cli.StringFlag{Name: "proxy", Usage: "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]"}, + &cli.DurationFlag{Name: "timeout", Value: 10 * time.Second, Usage: "HTTP Timeout"}, + &cli.BoolFlag{Name: "no-tls-validation", Aliases: []string{"k"}, Value: false, Usage: "Skip TLS certificate verification"}, + &cli.BoolFlag{Name: "retry", Value: false, Usage: "Should retry on request timeout"}, + &cli.IntFlag{Name: "retry-attempts", Value: 3, Usage: "Times to retry on request timeout"}, + &cli.StringFlag{Name: "client-cert-pem", Usage: "public key in PEM format for optional TLS client certificates]"}, + &cli.StringFlag{Name: "client-cert-pem-key", Usage: "private key in PEM format for optional TLS client certificates (this key needs to have no password)"}, + &cli.StringFlag{Name: "client-cert-p12", Usage: "a p12 file to use for options TLS client certificates"}, + &cli.StringFlag{Name: "client-cert-p12-password", Usage: "the password to the p12 file"}, + } +} + +func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions, error) { + var opts libgobuster.BasicHTTPOptions + opts.UserAgent = c.String("useragent") + randomUA := c.Bool("random-agent") + if randomUA { + ua, err := libgobuster.GetRandomUserAgent() + if err != nil { + return opts, err + } + opts.UserAgent = ua + } + opts.Proxy = c.String("proxy") + opts.Timeout = c.Duration("timeout") + opts.NoTLSValidation = c.Bool("no-tls-validation") + opts.RetryOnTimeout = c.Bool("retry") + opts.RetryAttempts = c.Int("retry-attempts") + + pemFile := c.String("client-cert-pem") + pemKeyFile := c.String("client-cert-pem-key") + p12File := c.String("client-cert-p12") + p12Pass := c.String("client-cert-p12-password") + + if pemFile != "" && p12File != "" { + return opts, fmt.Errorf("please supply either a pem or a p12, not both") + } + + if pemFile != "" { + cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile) + if err != nil { + return opts, fmt.Errorf("could not load supplied pem key: %w", err) + } + opts.TLSCertificate = &cert + } else if p12File != "" { + p12Content, err := os.ReadFile(p12File) + if err != nil { + return opts, fmt.Errorf("could not read p12 %s: %w", p12File, err) + } + blocks, err := pkcs12.ToPEM(p12Content, p12Pass) + if err != nil { + return opts, fmt.Errorf("could not load P12: %w", err) + } + var pemData []byte + for _, b := range blocks { + pemData = append(pemData, pem.EncodeToMemory(b)...) + } + cert, err := tls.X509KeyPair(pemData, pemData) + if err != nil { + return opts, fmt.Errorf("could not load certificate from P12: %w", err) + } + opts.TLSCertificate = &cert + } + + return opts, nil +} + +func CommonHTTPOptions() []cli.Flag { + var flags []cli.Flag + flags = append(flags, []cli.Flag{ + &cli.StringFlag{Name: "url", Aliases: []string{"u"}, Usage: "The target URL", Required: true}, + &cli.StringFlag{Name: "cookies", Aliases: []string{"c"}, Usage: "Cookies to use for the requests"}, + &cli.StringFlag{Name: "username", Aliases: []string{"U"}, Usage: "Username for Basic Auth"}, + &cli.StringFlag{Name: "password", Aliases: []string{"P"}, Usage: "Password for Basic Auth"}, + &cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"}, + &cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"}, + &cli.BoolFlag{Name: "no-canonicalize-headers", Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"}, + &cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"}, + }...) + flags = append(flags, BasicHTTPOptions()...) + return flags +} + +func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) { + var opts libgobuster.HTTPOptions + basic, err := ParseBasicHTTPOptions(c) + if err != nil { + return opts, err + } + opts.BasicHTTPOptions = basic + + opts.URL = c.String("url") + if !strings.HasPrefix(opts.URL, "http") { + // check to see if a port was specified + re := regexp.MustCompile(`^[^/]+:(\d+)`) + match := re.FindStringSubmatch(opts.URL) + + if len(match) < 2 { + // no port, default to http on 80 + opts.URL = fmt.Sprintf("http://%s", opts.URL) + } else { + port, err2 := strconv.Atoi(match[1]) + if err2 != nil || (port != 80 && port != 443) { + return opts, fmt.Errorf("url scheme not specified") + } else if port == 80 { + opts.URL = fmt.Sprintf("http://%s", opts.URL) + } else { + opts.URL = fmt.Sprintf("https://%s", opts.URL) + } + } + } + + opts.Cookies = c.String("cookies") + opts.Username = c.String("username") + opts.Password = c.String("password") + + // Prompt for PW if not provided + if opts.Username != "" && opts.Password == "" { + fmt.Printf("[?] Auth Password: ") + // please don't remove the int cast here as it is sadly needed on windows :/ + passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert + // 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 opts, fmt.Errorf("username given but reading of password failed") + } + opts.Password = string(passBytes) + } + // if it's still empty bail out + if opts.Username != "" && opts.Password == "" { + return opts, fmt.Errorf("username was provided but password is missing") + } + + opts.FollowRedirect = c.Bool("follow-redirect") + opts.NoCanonicalizeHeaders = c.Bool("no-canonicalize-headers") + opts.Method = c.String("method") + + for _, h := range c.StringSlice("headers") { + keyAndValue := strings.SplitN(h, ":", 2) + if len(keyAndValue) != 2 { + return opts, fmt.Errorf("invalid header format for header %q", h) + } + key := strings.TrimSpace(keyAndValue[0]) + value := strings.TrimSpace(keyAndValue[1]) + if len(key) == 0 { + return opts, fmt.Errorf("invalid header format for header %q - name is empty", h) + } + header := libgobuster.HTTPHeader{Name: key, Value: value} + opts.Headers = append(opts.Headers, header) + } + + return opts, nil +} + +func GlobalOptions() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{Name: "wordlist", Aliases: []string{"w"}, Usage: "Path to the wordlist. Set to - to use STDIN.", Required: true}, + &cli.DurationFlag{Name: "delay", Usage: "Time each thread waits between requests (e.g. 1500ms)"}, + &cli.IntFlag{Name: "threads", Aliases: []string{"t"}, Value: 10, Usage: "Number of concurrent threads"}, + &cli.IntFlag{Name: "wordlist-offset", Value: 0, Usage: "Resume from a given position in the wordlist"}, + &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output file to write results to (defaults to stdout)"}, + &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Value: false, Usage: "Don't print the banner and other noise"}, + &cli.BoolFlag{Name: "no-progress", Aliases: []string{"z"}, Value: false, Usage: "Don't display progress"}, + &cli.BoolFlag{Name: "no-error", Value: false, Usage: "Don't display errors"}, + &cli.StringFlag{Name: "pattern", Aliases: []string{"p"}, Usage: "File containing replacement patterns"}, + &cli.BoolFlag{Name: "no-color", Value: false, Usage: "Disable color output"}, + &cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"}, + } +} + +func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) { + var opts libgobuster.Options + + opts.Wordlist = c.String("wordlist") + if opts.Wordlist == "-" { + // STDIN + } else if _, err := os.Stat(opts.Wordlist); os.IsNotExist(err) { + return opts, fmt.Errorf("wordlist file %q does not exist: %w", opts.Wordlist, err) + } + + opts.Delay = c.Duration("delay") + opts.Threads = c.Int("threads") + opts.WordlistOffset = c.Int("wordlist-offset") + if opts.Wordlist == "-" && opts.WordlistOffset > 0 { + return opts, fmt.Errorf("wordlist-offset is not supported when reading from STDIN") + } else if opts.WordlistOffset < 0 { + return opts, fmt.Errorf("wordlist-offset must be bigger or equal to 0") + } + + opts.OutputFilename = c.String("output") + opts.Quiet = c.Bool("quiet") + opts.NoProgress = c.Bool("no-progress") + opts.NoError = c.Bool("no-error") + opts.PatternFile = c.String("pattern") + if opts.PatternFile != "" { + if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) { + return opts, fmt.Errorf("pattern file %q does not exist: %w", opts.PatternFile, err) + } + patternFile, err := os.Open(opts.PatternFile) + if err != nil { + return opts, fmt.Errorf("could not open pattern file %q: %w", opts.PatternFile, err) + } + defer patternFile.Close() + + scanner := bufio.NewScanner(patternFile) + for scanner.Scan() { + opts.Patterns = append(opts.Patterns, scanner.Text()) + } + if err := scanner.Err(); err != nil { + return opts, fmt.Errorf("could not read pattern file %q: %w", opts.PatternFile, err) + } + } + + if c.Bool("no-color") { + color.NoColor = true + } + + opts.Debug = c.Bool("debug") + return opts, nil +} diff --git a/cli/s3/s3.go b/cli/s3/s3.go new file mode 100644 index 0000000..ea45f53 --- /dev/null +++ b/cli/s3/s3.go @@ -0,0 +1,61 @@ +package s3 + +import ( + "fmt" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobusters3" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "s3", + Usage: "Uses aws bucket enumeration mode", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, []cli.Flag{ + &cli.IntFlag{Name: "maxfiles", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"}, + &cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"}, + }...) + flags = append(flags, internalcli.GlobalOptions()...) + flags = append(flags, internalcli.BasicHTTPOptions()...) + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobusters3.NewOptionsS3() + + httpOptions, err := internalcli.ParseBasicHTTPOptions(c) + if err != nil { + return err + } + pluginOpts.BasicHTTPOptions = httpOptions + + pluginOpts.MaxFilesToList = c.Int("maxfiles") + pluginOpts.ShowFiles = c.Bool("show-files") + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobusters3.NewGobusterS3(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobusters3: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster: %w", err) + } + return nil +} diff --git a/cli/tftp/tftp.go b/cli/tftp/tftp.go new file mode 100644 index 0000000..bf61e65 --- /dev/null +++ b/cli/tftp/tftp.go @@ -0,0 +1,60 @@ +package tftp + +import ( + "fmt" + "strings" + "time" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobustertftp" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "tftp", + Usage: "Uses TFTP enumeration mode", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, []cli.Flag{ + &cli.StringFlag{Name: "server", Aliases: []string{"s"}, Usage: "The target TFTP server", Required: true}, + &cli.DurationFlag{Name: "timeout", Value: 1 * time.Second, Usage: "TFTP timeout"}, + }...) + flags = append(flags, internalcli.GlobalOptions()...) + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobustertftp.NewOptionsTFTP() + + pluginOpts.Server = c.String("server") + if !strings.Contains(pluginOpts.Server, ":") { + pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server) + } + + pluginOpts.Timeout = c.Duration("timeout") + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobustertftp.NewGobusterTFTP(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobustertftp: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Server, err) + } + return nil +} diff --git a/cli/vhost/vhost.go b/cli/vhost/vhost.go new file mode 100644 index 0000000..76be070 --- /dev/null +++ b/cli/vhost/vhost.go @@ -0,0 +1,70 @@ +package vhost + +import ( + "fmt" + + internalcli "github.com/OJ/gobuster/v3/cli" + "github.com/OJ/gobuster/v3/gobustervhost" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) + +func Command() *cli.Command { + cmd := cli.Command{ + Name: "vhost", + Usage: "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)", + Action: run, + Flags: getFlags(), + } + return &cmd +} + +func getFlags() []cli.Flag { + var flags []cli.Flag + flags = append(flags, internalcli.CommonHTTPOptions()...) + flags = append(flags, internalcli.GlobalOptions()...) + flags = append(flags, []cli.Flag{ + &cli.BoolFlag{Name: "append-domain", Value: false, Usage: "Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist."}, + &cli.StringFlag{Name: "exclude-length", Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"}, + &cli.StringFlag{Name: "domain", Usage: "the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted"}, + }...) + + return flags +} + +func run(c *cli.Context) error { + pluginOpts := gobustervhost.NewOptionsVhost() + + httpOptions, err := internalcli.ParseCommonHTTPOptions(c) + if err != nil { + return err + } + pluginOpts.HTTPOptions = httpOptions + + pluginOpts.AppendDomain = c.Bool("append-domain") + pluginOpts.ExcludeLength = c.String("exclude-length") + ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) + if err != nil { + return fmt.Errorf("invalid value for exclude-length: %w", err) + } + pluginOpts.ExcludeLengthParsed = ret + + pluginOpts.Domain = c.String("domain") + + globalOpts, err := internalcli.ParseGlobalOptions(c) + if err != nil { + return err + } + + plugin, err := gobustervhost.NewGobusterVhost(&globalOpts, pluginOpts) + if err != nil { + return fmt.Errorf("error on creating gobustervhost: %w", err) + } + + log := libgobuster.NewLogger(globalOpts.Debug) + if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil { + log.Debugf("%#v", err) + return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err) + } + return nil +} diff --git a/cli/cmd/vhost_test.go b/cli/vhost/vhost_test.go similarity index 85% rename from cli/cmd/vhost_test.go rename to cli/vhost/vhost_test.go index 722c873..7dc53fb 100644 --- a/cli/cmd/vhost_test.go +++ b/cli/vhost/vhost_test.go @@ -1,8 +1,10 @@ -package cmd +package vhost import ( "context" "fmt" + "net/http" + "net/http/httptest" "os" "testing" "time" @@ -12,6 +14,14 @@ import ( "github.com/OJ/gobuster/v3/libgobuster" ) +func httpServer(b *testing.B, content string) *httptest.Server { + b.Helper() + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, content) + })) + return ts +} + func BenchmarkVhostMode(b *testing.B) { h := httpServer(b, "test") defer h.Close() diff --git a/go.mod b/go.mod index 5d169da..c269a9a 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,17 @@ require ( github.com/fatih/color v1.15.0 github.com/google/uuid v1.3.1 github.com/pin/tftp/v3 v3.0.0 - github.com/spf13/cobra v1.7.0 + github.com/urfave/cli/v2 v2.25.7 golang.org/x/crypto v0.12.0 golang.org/x/term v0.11.0 ) require ( - github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect ) diff --git a/go.sum b/go.sum index 4f5a434..f573f12 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,9 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -12,11 +11,12 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pin/tftp/v3 v3.0.0 h1:o9cQpmWBSbgiaYXuN+qJAB12XBIv4dT7OuOONucn2l0= github.com/pin/tftp/v3 v3.0.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= @@ -31,5 +31,3 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/libgobuster/useragents.go b/libgobuster/useragents.go index fbbe473..997f0a8 100644 --- a/libgobuster/useragents.go +++ b/libgobuster/useragents.go @@ -4672,5 +4672,5 @@ func GetRandomUserAgent() (string, error) { if err != nil { return "", err } - return userAgents[n.Int64()], err + return userAgents[n.Int64()], nil } diff --git a/libgobuster/version.go b/libgobuster/version.go index c8823d5..62b20ab 100644 --- a/libgobuster/version.go +++ b/libgobuster/version.go @@ -1,6 +1,26 @@ package libgobuster +import ( + "fmt" + "runtime/debug" +) + const ( // VERSION contains the current gobuster version VERSION = "3.7" ) + +func GetVersion() string { + version := VERSION + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" { + version = fmt.Sprintf("%s Revision %s", version, setting.Value) + } + if setting.Key == "vcs.time" { + version = fmt.Sprintf("%s from %s", version, setting.Value) + } + } + } + return version +} diff --git a/main.go b/main.go index 751c5fe..b122e72 100644 --- a/main.go +++ b/main.go @@ -1,23 +1,62 @@ package main -import "github.com/OJ/gobuster/v3/cli/cmd" +import ( + "fmt" + "log" + "os" + "runtime/debug" -//---------------------------------------------------- -// Gobuster -- by OJ Reeves -// -// A crap attempt at building something that resembles -// dirbuster or dirb using Go. The goal was to build -// a tool that would help learn Go and to actually do -// something useful. The idea of having this compile -// to native code is also appealing. -// -// Run: gobuster -h -// -// Please see THANKS file for contributors. -// Please see LICENSE file for license details. -// -//---------------------------------------------------- + "github.com/OJ/gobuster/v3/cli/dir" + "github.com/OJ/gobuster/v3/cli/dns" + "github.com/OJ/gobuster/v3/cli/fuzz" + "github.com/OJ/gobuster/v3/cli/gcs" + "github.com/OJ/gobuster/v3/cli/s3" + "github.com/OJ/gobuster/v3/cli/tftp" + "github.com/OJ/gobuster/v3/cli/vhost" + "github.com/OJ/gobuster/v3/libgobuster" + "github.com/urfave/cli/v2" +) func main() { - cmd.Execute() + // log := logrus.New() + // log.SetOutput(os.Stdout) + // log.SetLevel(logrus.InfoLevel) + + cli.VersionPrinter = func(cCtx *cli.Context) { + fmt.Printf("gobuster version %s\n", libgobuster.VERSION) + if info, ok := debug.ReadBuildInfo(); ok { + fmt.Printf("Build info:\n") + fmt.Printf("%s", info) + } + } + + app := &cli.App{ + Name: "gobuster", + UsageText: "gobuster command [command options]", + Authors: []*cli.Author{ + { + Name: "Christian Mehlmauer", + Email: "firefart@gmail.com", + }, + { + Name: "OJ Reeves", + }, + }, + Version: libgobuster.GetVersion(), + } + + app.Commands = []*cli.Command{ + dir.Command(), + vhost.Command(), + dns.Command(), + fuzz.Command(), + tftp.Command(), + s3.Command(), + gcs.Command(), + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } }