diff --git a/cli/cmd/dir.go b/cli/cmd/dir.go new file mode 100644 index 0000000..3dc4a3c --- /dev/null +++ b/cli/cmd/dir.go @@ -0,0 +1,188 @@ +package cmd + +import ( + "errors" + "fmt" + "log" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobusterdir" + "git.sual.in/casual/gobuster-lib/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: %w", 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/dir_test.go b/cli/cmd/dir_test.go new file mode 100644 index 0000000..7a0f41b --- /dev/null +++ b/cli/cmd/dir_test.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobusterdir" + "git.sual.in/casual/gobuster-lib/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 BenchmarkDirMode(b *testing.B) { + h := httpServer(b, "test") + defer h.Close() + + pluginopts := gobusterdir.NewOptionsDir() + pluginopts.URL = h.URL + pluginopts.Timeout = 10 * time.Second + + pluginopts.Extensions = ".php,.csv" + tmpExt, err := libgobuster.ParseExtensions(pluginopts.Extensions) + if err != nil { + b.Fatalf("could not parse extensions: %v", err) + } + pluginopts.ExtensionsParsed = tmpExt + + pluginopts.StatusCodes = "200,204,301,302,307,401,403" + tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes) + if err != nil { + b.Fatalf("could not parse status codes: %v", err) + } + pluginopts.StatusCodesParsed = tmpStat + + wordlist, err := os.CreateTemp("", "") + if err != nil { + b.Fatalf("could not create tempfile: %v", err) + } + defer os.Remove(wordlist.Name()) + for w := 0; w < 1000; w++ { + _, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w)) + } + wordlist.Close() + + globalopts := libgobuster.Options{ + Threads: 10, + Wordlist: wordlist.Name(), + NoProgress: true, + } + + ctx := context.Background() + oldStdout := os.Stdout + oldStderr := os.Stderr + defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr) + devnull, err := os.Open(os.DevNull) + if err != nil { + b.Fatalf("could not get devnull %v", err) + } + defer devnull.Close() + log := libgobuster.NewLogger(false) + + // Run the real benchmark + for x := 0; x < b.N; x++ { + os.Stdout = devnull + os.Stderr = devnull + plugin, err := gobusterdir.NewGobusterDir(&globalopts, pluginopts) + if err != nil { + b.Fatalf("error on creating gobusterdir: %v", err) + } + + if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil { + b.Fatalf("error on running gobuster: %v", err) + } + os.Stdout = oldStdout + os.Stderr = oldStderr + } +} diff --git a/cli/cmd/dns.go b/cli/cmd/dns.go new file mode 100644 index 0000000..d1579c1 --- /dev/null +++ b/cli/cmd/dns.go @@ -0,0 +1,115 @@ +package cmd + +import ( + "errors" + "fmt" + "log" + "runtime" + "time" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobusterdns" + "git.sual.in/casual/gobuster-lib/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: %w", 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.ShowCNAME, err = cmdDNS.Flags().GetBool("show-cname") + if err != nil { + return nil, nil, fmt.Errorf("invalid value for show-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") + cmdDNS.Flags().BoolP("show-cname", "c", false, "Show CNAME records (cannot be used with '-i' option)") + 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 new file mode 100644 index 0000000..0e9d1d2 --- /dev/null +++ b/cli/cmd/fuzz.go @@ -0,0 +1,148 @@ +package cmd + +import ( + "errors" + "fmt" + "log" + "strings" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobusterfuzz" + "git.sual.in/casual/gobuster-lib/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: %w", 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 new file mode 100644 index 0000000..43f846e --- /dev/null +++ b/cli/cmd/gcs.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobustergcs" + "git.sual.in/casual/gobuster-lib/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) + } + + 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 (only shown in verbose mode)") + + cmdGCS.PersistentPreRun = func(cmd *cobra.Command, args []string) { + configureGlobalOptions() + } + + rootCmd.AddCommand(cmdGCS) +} diff --git a/cli/cmd/http.go b/cli/cmd/http.go new file mode 100644 index 0000000..ddfe966 --- /dev/null +++ b/cli/cmd/http.go @@ -0,0 +1,258 @@ +package cmd + +import ( + "crypto/tls" + "encoding/pem" + "fmt" + "os" + "regexp" + "strconv" + "strings" + "syscall" + "time" + + "git.sual.in/casual/gobuster-lib/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 new file mode 100644 index 0000000..1534fae --- /dev/null +++ b/cli/cmd/root.go @@ -0,0 +1,194 @@ +package cmd + +import ( + "bufio" + "context" + "fmt" + "log" + "os" + "os/signal" + + "git.sual.in/casual/gobuster-lib/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.Verbose, err = rootCmd.Flags().GetBool("verbose") + if err != nil { + return nil, fmt.Errorf("invalid value for verbose: %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("verbose", "v", false, "Verbose output (errors)") + 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 new file mode 100644 index 0000000..a2935de --- /dev/null +++ b/cli/cmd/s3.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "fmt" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobusters3" + "git.sual.in/casual/gobuster-lib/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) + } + + 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.PersistentPreRun = func(cmd *cobra.Command, args []string) { + configureGlobalOptions() + } + + rootCmd.AddCommand(cmdS3) +} diff --git a/cli/cmd/tftp.go b/cli/cmd/tftp.go new file mode 100644 index 0000000..cb8f52e --- /dev/null +++ b/cli/cmd/tftp.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "fmt" + "log" + "strings" + "time" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobustertftp" + "git.sual.in/casual/gobuster-lib/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: %w", 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 new file mode 100644 index 0000000..8d55edf --- /dev/null +++ b/cli/cmd/version.go @@ -0,0 +1,27 @@ +package cmd + +import ( + "fmt" + + "git.sual.in/casual/gobuster-lib/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 new file mode 100644 index 0000000..1ea6dd6 --- /dev/null +++ b/cli/cmd/vhost.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "fmt" + "log" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobustervhost" + "git.sual.in/casual/gobuster-lib/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: %w", 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/cmd/vhost_test.go b/cli/cmd/vhost_test.go new file mode 100644 index 0000000..ebc2037 --- /dev/null +++ b/cli/cmd/vhost_test.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "git.sual.in/casual/gobuster-lib/cli" + "git.sual.in/casual/gobuster-lib/gobustervhost" + "git.sual.in/casual/gobuster-lib/libgobuster" +) + +func BenchmarkVhostMode(b *testing.B) { + h := httpServer(b, "test") + defer h.Close() + + pluginopts := gobustervhost.NewOptionsVhost() + pluginopts.URL = h.URL + pluginopts.Timeout = 10 * time.Second + + wordlist, err := os.CreateTemp("", "") + if err != nil { + b.Fatalf("could not create tempfile: %v", err) + } + defer os.Remove(wordlist.Name()) + for w := 0; w < 1000; w++ { + _, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w)) + } + wordlist.Close() + + globalopts := libgobuster.Options{ + Threads: 10, + Wordlist: wordlist.Name(), + NoProgress: true, + } + + ctx := context.Background() + oldStdout := os.Stdout + oldStderr := os.Stderr + defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr) + devnull, err := os.Open(os.DevNull) + if err != nil { + b.Fatalf("could not get devnull %v", err) + } + defer devnull.Close() + log := libgobuster.NewLogger(false) + + // Run the real benchmark + for x := 0; x < b.N; x++ { + os.Stdout = devnull + os.Stderr = devnull + plugin, err := gobustervhost.NewGobusterVhost(&globalopts, pluginopts) + if err != nil { + b.Fatalf("error on creating gobusterdir: %v", err) + } + + if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil { + b.Fatalf("error on running gobuster: %v", err) + } + os.Stdout = oldStdout + os.Stderr = oldStderr + } +} diff --git a/go.mod b/go.mod index fa779d7..3dd0619 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.sual.in/casual/gobusterlib +module git.sual.in/casual/gobuster-lib go 1.22.5 @@ -7,11 +7,16 @@ require ( github.com/fatih/color v1.17.0 github.com/google/uuid v1.6.0 github.com/pin/tftp/v3 v3.1.0 + github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.26.0 + golang.org/x/term v0.23.0 ) require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.25.0 // indirect ) diff --git a/go.sum b/go.sum index 9a1f3d7..649c3c7 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,12 @@ git.sual.in/casual/gobuster-lib/libgobuster v0.0.0-20240904201007-8210f5ee7e12 h1:+CnxZE3aMK45ZGtoSNv/5J2VSL2igm0/Iytvbxcatog= git.sual.in/casual/gobuster-lib/libgobuster v0.0.0-20240904201007-8210f5ee7e12/go.mod h1:bkuQXxQgSQ+tO2Qs6PiRKRLXd8g5izddsxraLjHzrD8= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/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= @@ -11,7 +14,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c= github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= @@ -20,4 +30,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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/go.mod b/libgobuster/go.mod new file mode 100644 index 0000000..c64f9bf --- /dev/null +++ b/libgobuster/go.mod @@ -0,0 +1,11 @@ +module git.sual.in/casual/gobuster-lib/libgobuster + +go 1.22.5 + +require github.com/fatih/color v1.17.0 + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/libgobuster/go.sum b/libgobuster/go.sum new file mode 100644 index 0000000..4ddf511 --- /dev/null +++ b/libgobuster/go.sum @@ -0,0 +1,11 @@ +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +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= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go.bak b/main.go similarity index 100% rename from main.go.bak rename to main.go