diff --git a/README.md b/README.md index 8adfd03..dcb8d21 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,13 @@ All funds that are donated to this project will be donated to charity. A full lo # Changes +## 3.3 + +- Support TLS client certificates / mtl +- support loading extensions from file +- support fuzzing POST body, HTTP headers and basic auth +- new option to not canonicalize header names + ## 3.2 - Use go 1.19 @@ -479,7 +486,7 @@ https://buffered.io/categories ### Options ```text -Uses VHOST enumeration mode (you most probably want to use the IP adress as the URL parameter +Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter) Usage: gobuster vhost [flags] diff --git a/cli/cmd/dir.go b/cli/cmd/dir.go index 61157db..6e174a0 100644 --- a/cli/cmd/dir.go +++ b/cli/cmd/dir.go @@ -42,98 +42,114 @@ func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) { return nil, nil, err } - plugin := gobusterdir.NewOptionsDir() + pluginOpts := gobusterdir.NewOptionsDir() httpOpts, err := parseCommonHTTPOptions(cmdDir) if err != nil { return nil, nil, err } - plugin.Password = httpOpts.Password - plugin.URL = httpOpts.URL - plugin.UserAgent = httpOpts.UserAgent - plugin.Username = httpOpts.Username - plugin.Proxy = httpOpts.Proxy - plugin.Cookies = httpOpts.Cookies - plugin.Timeout = httpOpts.Timeout - plugin.FollowRedirect = httpOpts.FollowRedirect - plugin.NoTLSValidation = httpOpts.NoTLSValidation - plugin.Headers = httpOpts.Headers - plugin.Method = httpOpts.Method - plugin.RetryOnTimeout = httpOpts.RetryOnTimeout - plugin.RetryAttempts = httpOpts.RetryAttempts + 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 - plugin.Extensions, err = cmdDir.Flags().GetString("extensions") + pluginOpts.Extensions, err = cmdDir.Flags().GetString("extensions") if err != nil { return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) } - ret, err := helper.ParseExtensions(plugin.Extensions) + + ret, err := helper.ParseExtensions(pluginOpts.Extensions) if err != nil { return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) } - plugin.ExtensionsParsed = ret + 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 := helper.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 - plugin.StatusCodes, err = cmdDir.Flags().GetString("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 := helper.ParseCommaSeparatedInt(plugin.StatusCodes) + ret2, err := helper.ParseCommaSeparatedInt(pluginOpts.StatusCodes) if err != nil { return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err) } - plugin.StatusCodesParsed = ret2 + pluginOpts.StatusCodesParsed = ret2 // blacklist will override the normal status codes - plugin.StatusCodesBlacklist, err = cmdDir.Flags().GetString("status-codes-blacklist") + 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 := helper.ParseCommaSeparatedInt(plugin.StatusCodesBlacklist) + ret3, err := helper.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist) if err != nil { return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err) } - plugin.StatusCodesBlacklistParsed = ret3 + pluginOpts.StatusCodesBlacklistParsed = ret3 - if plugin.StatusCodes != "" && plugin.StatusCodesBlacklist != "" { + 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.", - plugin.StatusCodes, plugin.StatusCodesBlacklist) + pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist) } - if plugin.StatusCodes == "" && plugin.StatusCodesBlacklist == "" { + if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" { return nil, nil, fmt.Errorf("status-codes and status-codes-blacklist are both not set, please set one") } - plugin.UseSlash, err = cmdDir.Flags().GetBool("add-slash") + pluginOpts.UseSlash, err = cmdDir.Flags().GetBool("add-slash") if err != nil { return nil, nil, fmt.Errorf("invalid value for add-slash: %w", err) } - plugin.Expanded, err = cmdDir.Flags().GetBool("expanded") + pluginOpts.Expanded, err = cmdDir.Flags().GetBool("expanded") if err != nil { return nil, nil, fmt.Errorf("invalid value for expanded: %w", err) } - plugin.NoStatus, err = cmdDir.Flags().GetBool("no-status") + pluginOpts.NoStatus, err = cmdDir.Flags().GetBool("no-status") if err != nil { return nil, nil, fmt.Errorf("invalid value for no-status: %w", err) } - plugin.HideLength, err = cmdDir.Flags().GetBool("hide-length") + pluginOpts.HideLength, err = cmdDir.Flags().GetBool("hide-length") if err != nil { return nil, nil, fmt.Errorf("invalid value for hide-length: %w", err) } - plugin.DiscoverBackup, err = cmdDir.Flags().GetBool("discover-backup") + pluginOpts.DiscoverBackup, err = cmdDir.Flags().GetBool("discover-backup") if err != nil { return nil, nil, fmt.Errorf("invalid value for discover-backup: %w", err) } - plugin.ExcludeLength, err = cmdDir.Flags().GetIntSlice("exclude-length") + pluginOpts.ExcludeLength, err = cmdDir.Flags().GetIntSlice("exclude-length") if err != nil { return nil, nil, fmt.Errorf("invalid value for excludelength: %w", err) } - return globalopts, plugin, nil + return globalopts, pluginOpts, nil } // nolint:gochecknoinits @@ -150,6 +166,7 @@ func init() { cmdDir.Flags().StringP("status-codes", "s", "", "Positive status codes (will be overwritten with status-codes-blacklist if set)") cmdDir.Flags().StringP("status-codes-blacklist", "b", "404", "Negative status codes (will override status-codes if set)") 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") diff --git a/cli/cmd/dns.go b/cli/cmd/dns.go index c0810ad..9116db9 100644 --- a/cli/cmd/dns.go +++ b/cli/cmd/dns.go @@ -42,43 +42,43 @@ func parseDNSOptions() (*libgobuster.Options, *gobusterdns.OptionsDNS, error) { if err != nil { return nil, nil, err } - plugin := gobusterdns.NewOptionsDNS() + pluginOpts := gobusterdns.NewOptionsDNS() - plugin.Domain, err = cmdDNS.Flags().GetString("domain") + pluginOpts.Domain, err = cmdDNS.Flags().GetString("domain") if err != nil { return nil, nil, fmt.Errorf("invalid value for domain: %w", err) } - plugin.ShowIPs, err = cmdDNS.Flags().GetBool("show-ips") + pluginOpts.ShowIPs, err = cmdDNS.Flags().GetBool("show-ips") if err != nil { return nil, nil, fmt.Errorf("invalid value for show-ips: %w", err) } - plugin.ShowCNAME, err = cmdDNS.Flags().GetBool("show-cname") + pluginOpts.ShowCNAME, err = cmdDNS.Flags().GetBool("show-cname") if err != nil { return nil, nil, fmt.Errorf("invalid value for show-cname: %w", err) } - plugin.WildcardForced, err = cmdDNS.Flags().GetBool("wildcard") + pluginOpts.WildcardForced, err = cmdDNS.Flags().GetBool("wildcard") if err != nil { return nil, nil, fmt.Errorf("invalid value for wildcard: %w", err) } - plugin.Timeout, err = cmdDNS.Flags().GetDuration("timeout") + pluginOpts.Timeout, err = cmdDNS.Flags().GetDuration("timeout") if err != nil { return nil, nil, fmt.Errorf("invalid value for timeout: %w", err) } - plugin.Resolver, err = cmdDNS.Flags().GetString("resolver") + pluginOpts.Resolver, err = cmdDNS.Flags().GetString("resolver") if err != nil { return nil, nil, fmt.Errorf("invalid value for resolver: %w", err) } - if plugin.Resolver != "" && runtime.GOOS == "windows" { + 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, plugin, nil + return globalopts, pluginOpts, nil } // nolint:gochecknoinits diff --git a/cli/cmd/fuzz.go b/cli/cmd/fuzz.go index d746e6e..de996ae 100644 --- a/cli/cmd/fuzz.go +++ b/cli/cmd/fuzz.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "log" + "strings" "github.com/OJ/gobuster/v3/cli" "github.com/OJ/gobuster/v3/gobusterfuzz" @@ -21,6 +22,10 @@ func runFuzz(cmd *cobra.Command, args []string) error { 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) @@ -42,50 +47,57 @@ func parseFuzzOptions() (*libgobuster.Options, *gobusterfuzz.OptionsFuzz, error) return nil, nil, err } - plugin := gobusterfuzz.NewOptionsFuzz() + pluginOpts := gobusterfuzz.NewOptionsFuzz() httpOpts, err := parseCommonHTTPOptions(cmdFuzz) if err != nil { return nil, nil, err } - plugin.Password = httpOpts.Password - plugin.URL = httpOpts.URL - plugin.UserAgent = httpOpts.UserAgent - plugin.Username = httpOpts.Username - plugin.Proxy = httpOpts.Proxy - plugin.Cookies = httpOpts.Cookies - plugin.Timeout = httpOpts.Timeout - plugin.FollowRedirect = httpOpts.FollowRedirect - plugin.NoTLSValidation = httpOpts.NoTLSValidation - plugin.Headers = httpOpts.Headers - plugin.Method = httpOpts.Method - plugin.RetryOnTimeout = httpOpts.RetryOnTimeout - plugin.RetryAttempts = httpOpts.RetryAttempts + 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 - plugin.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes") + pluginOpts.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes") if err != nil { return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) } - ret, err := helper.ParseCommaSeparatedInt(plugin.ExcludedStatusCodes) + ret, err := helper.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes) if err != nil { return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) } - plugin.ExcludedStatusCodesParsed = ret + pluginOpts.ExcludedStatusCodesParsed = ret - plugin.ExcludeLength, err = cmdFuzz.Flags().GetIntSlice("exclude-length") + pluginOpts.ExcludeLength, err = cmdFuzz.Flags().GetIntSlice("exclude-length") if err != nil { return nil, nil, fmt.Errorf("invalid value for excludelength: %w", err) } - return globalopts, plugin, nil + 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: "Uses fuzzing mode", + Short: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword), RunE: runFuzz, } @@ -94,6 +106,7 @@ func init() { } cmdFuzz.Flags().StringP("excludestatuscodes", "b", "", "Negative status codes (will override statuscodes if set)") cmdFuzz.Flags().IntSlice("exclude-length", []int{}, "exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.") + cmdFuzz.Flags().StringP("body", "B", "", "Request body") cmdFuzz.PersistentPreRun = func(cmd *cobra.Command, args []string) { configureGlobalOptions() @@ -101,3 +114,29 @@ func init() { 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 index 9be0cda..1b7512e 100644 --- a/cli/cmd/gcs.go +++ b/cli/cmd/gcs.go @@ -48,6 +48,7 @@ func parseGCSOptions() (*libgobuster.Options, *gobustergcs.OptionsGCS, error) { 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 { diff --git a/cli/cmd/http.go b/cli/cmd/http.go index 65c3ecb..4f8c1f8 100644 --- a/cli/cmd/http.go +++ b/cli/cmd/http.go @@ -1,7 +1,10 @@ package cmd import ( + "crypto/tls" + "encoding/pem" "fmt" + "os" "regexp" "strconv" "strings" @@ -11,6 +14,7 @@ import ( "github.com/OJ/gobuster/v3/helper" "github.com/OJ/gobuster/v3/libgobuster" "github.com/spf13/cobra" + "golang.org/x/crypto/pkcs12" "golang.org/x/term" ) @@ -22,6 +26,11 @@ func addBasicHTTPOptions(cmd *cobra.Command) { 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 { @@ -32,6 +41,7 @@ func addCommonHTTPOptions(cmd *cobra.Command) error { 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 { @@ -85,6 +95,54 @@ func parseBasicHTTPOptions(cmd *cobra.Command) (libgobuster.BasicHTTPOptions, er 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 } @@ -102,6 +160,7 @@ func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error) 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 { @@ -172,6 +231,12 @@ func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error) 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: ") diff --git a/cli/cmd/s3.go b/cli/cmd/s3.go index 5477b88..c8aaccc 100644 --- a/cli/cmd/s3.go +++ b/cli/cmd/s3.go @@ -35,26 +35,27 @@ func parseS3Options() (*libgobuster.Options, *gobusters3.OptionsS3, error) { return nil, nil, err } - plugin := gobusters3.NewOptionsS3() + pluginOpts := gobusters3.NewOptionsS3() httpOpts, err := parseBasicHTTPOptions(cmdS3) if err != nil { return nil, nil, err } - plugin.UserAgent = httpOpts.UserAgent - plugin.Proxy = httpOpts.Proxy - plugin.Timeout = httpOpts.Timeout - plugin.NoTLSValidation = httpOpts.NoTLSValidation - plugin.RetryOnTimeout = httpOpts.RetryOnTimeout - plugin.RetryAttempts = httpOpts.RetryAttempts + 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 - plugin.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles") + pluginOpts.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles") if err != nil { return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err) } - return globalopts, plugin, nil + return globalopts, pluginOpts, nil } // nolint:gochecknoinits diff --git a/cli/cmd/vhost.go b/cli/cmd/vhost.go index ac047bd..bc7be05 100644 --- a/cli/cmd/vhost.go +++ b/cli/cmd/vhost.go @@ -35,49 +35,52 @@ func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, err if err != nil { return nil, nil, err } - var plugin gobustervhost.OptionsVhost + + pluginOpts := gobustervhost.NewOptionsVhost() httpOpts, err := parseCommonHTTPOptions(cmdVhost) if err != nil { return nil, nil, err } - plugin.Password = httpOpts.Password - plugin.URL = httpOpts.URL - plugin.UserAgent = httpOpts.UserAgent - plugin.Username = httpOpts.Username - plugin.Proxy = httpOpts.Proxy - plugin.Cookies = httpOpts.Cookies - plugin.Timeout = httpOpts.Timeout - plugin.FollowRedirect = httpOpts.FollowRedirect - plugin.NoTLSValidation = httpOpts.NoTLSValidation - plugin.Headers = httpOpts.Headers - plugin.Method = httpOpts.Method - plugin.RetryOnTimeout = httpOpts.RetryOnTimeout - plugin.RetryAttempts = httpOpts.RetryAttempts + 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 - plugin.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain") + pluginOpts.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain") if err != nil { return nil, nil, fmt.Errorf("invalid value for append-domain: %w", err) } - plugin.ExcludeLength, err = cmdVhost.Flags().GetIntSlice("exclude-length") + pluginOpts.ExcludeLength, err = cmdVhost.Flags().GetIntSlice("exclude-length") if err != nil { return nil, nil, fmt.Errorf("invalid value for excludelength: %w", err) } - plugin.Domain, err = cmdVhost.Flags().GetString("domain") + pluginOpts.Domain, err = cmdVhost.Flags().GetString("domain") if err != nil { return nil, nil, fmt.Errorf("invalid value for domain: %w", err) } - return globalopts, &plugin, nil + 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 adress as the URL parameter", + 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 { diff --git a/cli/cmd/vhost_test.go b/cli/cmd/vhost_test.go index 92fc5a2..9f6a2ea 100644 --- a/cli/cmd/vhost_test.go +++ b/cli/cmd/vhost_test.go @@ -18,7 +18,7 @@ func BenchmarkVhostMode(b *testing.B) { h := httpServer(b, "test") defer h.Close() - pluginopts := gobustervhost.OptionsVhost{} + pluginopts := gobustervhost.NewOptionsVhost() pluginopts.URL = h.URL pluginopts.Timeout = 10 * time.Second @@ -54,7 +54,7 @@ func BenchmarkVhostMode(b *testing.B) { for x := 0; x < b.N; x++ { os.Stdout = devnull os.Stderr = devnull - plugin, err := gobustervhost.NewGobusterVhost(&globalopts, &pluginopts) + plugin, err := gobustervhost.NewGobusterVhost(&globalopts, pluginopts) if err != nil { b.Fatalf("error on creating gobusterdir: %v", err) } diff --git a/go.mod b/go.mod index 187f783..47a38df 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,9 @@ go 1.19 require ( github.com/fatih/color v1.13.0 github.com/google/uuid v1.3.0 - github.com/spf13/cobra v1.6.0 - golang.org/x/term v0.0.0-20220919170432-7a66f970e087 + github.com/spf13/cobra v1.6.1 + golang.org/x/crypto v0.1.0 + golang.org/x/term v0.1.0 ) require ( @@ -14,5 +15,5 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect + golang.org/x/sys v0.1.0 // indirect ) diff --git a/go.sum b/go.sum index ebb4eaa..521e1a5 100644 --- a/go.sum +++ b/go.sum @@ -13,17 +13,19 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI= -github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= 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.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM= -golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w= -golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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/gobusterdir/gobusterdir.go b/gobusterdir/gobusterdir.go index a823f38..6599990 100644 --- a/gobusterdir/gobusterdir.go +++ b/gobusterdir/gobusterdir.go @@ -62,16 +62,18 @@ func NewGobusterDir(globalopts *libgobuster.Options, opts *OptionsDir) (*Gobuste NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, + TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ - BasicHTTPOptions: basicOptions, - FollowRedirect: opts.FollowRedirect, - Username: opts.Username, - Password: opts.Password, - Headers: opts.Headers, - Cookies: opts.Cookies, - Method: opts.Method, + BasicHTTPOptions: basicOptions, + FollowRedirect: opts.FollowRedirect, + Username: opts.Username, + Password: opts.Password, + Headers: opts.Headers, + NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, + Cookies: opts.Cookies, + Method: opts.Method, } h, err := libgobuster.NewHTTPClient(&httpOpts) @@ -321,12 +323,18 @@ func (d *GobusterDir) GetConfigString() (string, error) { } } - if o.Extensions != "" { + if o.Extensions != "" || o.ExtensionsFile != "" { if _, err := fmt.Fprintf(tw, "[+] Extensions:\t%s\n", o.ExtensionsParsed.Stringify()); err != nil { return "", err } } + if o.ExtensionsFile != "" { + if _, err := fmt.Fprintf(tw, "[+] Extensions file:\t%s\n", o.ExtensionsFile); err != nil { + return "", err + } + } + if o.UseSlash { if _, err := fmt.Fprintf(tw, "[+] Add Slash:\ttrue\n"); err != nil { return "", err diff --git a/gobusterdir/options.go b/gobusterdir/options.go index 93e821e..b0d700c 100644 --- a/gobusterdir/options.go +++ b/gobusterdir/options.go @@ -9,6 +9,7 @@ type OptionsDir struct { libgobuster.HTTPOptions Extensions string ExtensionsParsed libgobuster.Set[string] + ExtensionsFile string StatusCodes string StatusCodesParsed libgobuster.Set[int] StatusCodesBlacklist string diff --git a/gobusterfuzz/gobusterfuzz.go b/gobusterfuzz/gobusterfuzz.go index b0dca52..1082525 100644 --- a/gobusterfuzz/gobusterfuzz.go +++ b/gobusterfuzz/gobusterfuzz.go @@ -13,6 +13,8 @@ import ( "github.com/OJ/gobuster/v3/libgobuster" ) +const FuzzKeyword = "FUZZ" + // ErrWildcard is returned if a wildcard response is found type ErrWildcard struct { url string @@ -53,16 +55,18 @@ func NewGobusterFuzz(globalopts *libgobuster.Options, opts *OptionsFuzz) (*Gobus NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, + TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ - BasicHTTPOptions: basicOptions, - FollowRedirect: opts.FollowRedirect, - Username: opts.Username, - Password: opts.Password, - Headers: opts.Headers, - Cookies: opts.Cookies, - Method: opts.Method, + BasicHTTPOptions: basicOptions, + FollowRedirect: opts.FollowRedirect, + Username: opts.Username, + Password: opts.Password, + Headers: opts.Headers, + NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, + Cookies: opts.Cookies, + Method: opts.Method, } h, err := libgobuster.NewHTTPClient(&httpOpts) @@ -85,7 +89,31 @@ func (d *GobusterFuzz) PreRun(ctx context.Context) error { // ProcessWord is the process implementation of gobusterfuzz func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { - url := strings.ReplaceAll(d.options.URL, "FUZZ", word) + url := strings.ReplaceAll(d.options.URL, FuzzKeyword, word) + + requestOptions := libgobuster.RequestOptions{} + + if len(d.options.Headers) > 0 { + requestOptions.ModifiedHeaders = make([]libgobuster.HTTPHeader, len(d.options.Headers)) + for i := range d.options.Headers { + requestOptions.ModifiedHeaders[i] = libgobuster.HTTPHeader{ + Name: strings.ReplaceAll(d.options.Headers[i].Name, FuzzKeyword, word), + Value: strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word), + } + } + } + + if d.options.RequestBody != "" { + data := strings.ReplaceAll(d.options.RequestBody, FuzzKeyword, word) + buffer := strings.NewReader(data) + requestOptions.Body = buffer + } + + // fuzzing of basic auth + if strings.Contains(d.options.Username, FuzzKeyword) || strings.Contains(d.options.Password, FuzzKeyword) { + requestOptions.UpdatedBasicAuthUsername = strings.ReplaceAll(d.options.Username, FuzzKeyword, word) + requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word) + } tries := 1 if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 { @@ -97,7 +125,7 @@ func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *l var size int64 for i := 1; i <= tries; i++ { var err error - statusCode, size, _, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{}) + statusCode, size, _, _, err = d.http.Request(ctx, url, requestOptions) if err != nil { // check if it's a timeout and if we should try again and try again // otherwise the timeout error is raised diff --git a/gobusterfuzz/options.go b/gobusterfuzz/options.go index 2ecb865..de91b60 100644 --- a/gobusterfuzz/options.go +++ b/gobusterfuzz/options.go @@ -10,6 +10,7 @@ type OptionsFuzz struct { ExcludedStatusCodes string ExcludedStatusCodesParsed libgobuster.Set[int] ExcludeLength []int + RequestBody string } // NewOptionsFuzz returns a new initialized OptionsFuzz diff --git a/gobustergcs/gobustersgcs.go b/gobustergcs/gobustersgcs.go index 06e0aff..8705622 100644 --- a/gobustergcs/gobustersgcs.go +++ b/gobustergcs/gobustersgcs.go @@ -45,6 +45,7 @@ func NewGobusterGCS(globalopts *libgobuster.Options, opts *OptionsGCS) (*Gobuste NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, + TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ diff --git a/gobusters3/gobusters3.go b/gobusters3/gobusters3.go index bbfcdc4..4c37181 100644 --- a/gobusters3/gobusters3.go +++ b/gobusters3/gobusters3.go @@ -45,6 +45,7 @@ func NewGobusterS3(globalopts *libgobuster.Options, opts *OptionsS3) (*GobusterS NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, + TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ diff --git a/gobustervhost/gobustervhost.go b/gobustervhost/gobustervhost.go index 04bee58..166ba9f 100644 --- a/gobustervhost/gobustervhost.go +++ b/gobustervhost/gobustervhost.go @@ -48,16 +48,18 @@ func NewGobusterVhost(globalopts *libgobuster.Options, opts *OptionsVhost) (*Gob NoTLSValidation: opts.NoTLSValidation, RetryOnTimeout: opts.RetryOnTimeout, RetryAttempts: opts.RetryAttempts, + TLSCertificate: opts.TLSCertificate, } httpOpts := libgobuster.HTTPOptions{ - BasicHTTPOptions: basicOptions, - FollowRedirect: opts.FollowRedirect, - Username: opts.Username, - Password: opts.Password, - Headers: opts.Headers, - Cookies: opts.Cookies, - Method: opts.Method, + BasicHTTPOptions: basicOptions, + FollowRedirect: opts.FollowRedirect, + Username: opts.Username, + Password: opts.Password, + Headers: opts.Headers, + NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, + Cookies: opts.Cookies, + Method: opts.Method, } h, err := libgobuster.NewHTTPClient(&httpOpts) diff --git a/gobustervhost/options.go b/gobustervhost/options.go index 009873e..50db56e 100644 --- a/gobustervhost/options.go +++ b/gobustervhost/options.go @@ -11,3 +11,8 @@ type OptionsVhost struct { ExcludeLength []int Domain string } + +// NewOptionsVhost returns a new initialized OptionsVhost +func NewOptionsVhost() *OptionsVhost { + return &OptionsVhost{} +} diff --git a/helper/helper.go b/helper/helper.go index e43ff35..6cc6429 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -1,7 +1,9 @@ package helper import ( + "bufio" "fmt" + "os" "strconv" "strings" @@ -24,6 +26,30 @@ func ParseExtensions(extensions string) (libgobuster.Set[string], error) { return ret, nil } +func ParseExtensionsFile(file string) ([]string, error) { + var ret []string + + stream, err := os.Open(file) + if err != nil { + return ret, err + } + defer stream.Close() + + scanner := bufio.NewScanner(stream) + for scanner.Scan() { + e := scanner.Text() + e = strings.TrimSpace(e) + // remove leading . from extensions + ret = append(ret, (strings.TrimPrefix(e, "."))) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return ret, nil +} + // ParseCommaSeparatedInt parses the status codes provided as a comma separated list func ParseCommaSeparatedInt(inputString string) (libgobuster.Set[int], error) { ret := libgobuster.NewSet[int]() diff --git a/libgobuster/http.go b/libgobuster/http.go index 8ea791d..8b8648c 100644 --- a/libgobuster/http.go +++ b/libgobuster/http.go @@ -19,22 +19,26 @@ type HTTPHeader struct { // HTTPClient represents a http object type HTTPClient struct { - client *http.Client - userAgent string - defaultUserAgent string - username string - password string - headers []HTTPHeader - cookies string - method string - host string + client *http.Client + userAgent string + defaultUserAgent string + username string + password string + headers []HTTPHeader + noCanonicalizeHeaders bool + cookies string + method string + host string } // RequestOptions is used to pass options to a single individual request type RequestOptions struct { - Host string - Body io.Reader - ReturnBody bool + Host string + Body io.Reader + ReturnBody bool + ModifiedHeaders []HTTPHeader + UpdatedBasicAuthUsername string + UpdatedBasicAuthPassword string } // NewHTTPClient returns a new HTTPClient @@ -64,6 +68,13 @@ func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) { redirectFunc = nil } + tlsConfig := tls.Config{ + InsecureSkipVerify: opt.NoTLSValidation, + } + if opt.TLSCertificate != nil { + tlsConfig.Certificates = []tls.Certificate{*opt.TLSCertificate} + } + client.client = &http.Client{ Timeout: opt.Timeout, CheckRedirect: redirectFunc, @@ -71,15 +82,14 @@ func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) { Proxy: proxyURLFunc, MaxIdleConns: 100, MaxIdleConnsPerHost: 100, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: opt.NoTLSValidation, - }, + TLSClientConfig: &tlsConfig, }} client.username = opt.Username client.password = opt.Password client.userAgent = opt.UserAgent client.defaultUserAgent = DefaultUserAgent() client.headers = opt.Headers + client.noCanonicalizeHeaders = opt.NoCanonicalizeHeaders client.cookies = opt.Cookies client.method = opt.Method if client.method == "" { @@ -98,7 +108,7 @@ func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) { // Request makes an http request and returns the status, the content length, the headers, the body and an error // if you want the body returned set the corresponding property inside RequestOptions func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts RequestOptions) (int, int64, http.Header, []byte, error) { - resp, err := client.makeRequest(ctx, fullURL, opts.Host, opts.Body) + resp, err := client.makeRequest(ctx, fullURL, opts) if err != nil { // ignore context canceled errors if errors.Is(ctx.Err(), context.Canceled) { @@ -128,8 +138,8 @@ func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts Requ return resp.StatusCode, length, resp.Header, body, nil } -func (client *HTTPClient) makeRequest(ctx context.Context, fullURL, host string, data io.Reader) (*http.Response, error) { - req, err := http.NewRequest(client.method, fullURL, data) +func (client *HTTPClient) makeRequest(ctx context.Context, fullURL string, opts RequestOptions) (*http.Response, error) { + req, err := http.NewRequest(client.method, fullURL, opts.Body) if err != nil { return nil, err } @@ -142,8 +152,8 @@ func (client *HTTPClient) makeRequest(ctx context.Context, fullURL, host string, } // Use host for VHOST mode on a per request basis, otherwise the one provided from headers - if host != "" { - req.Host = host + if opts.Host != "" { + req.Host = opts.Host } else if client.host != "" { req.Host = client.host } @@ -155,11 +165,31 @@ func (client *HTTPClient) makeRequest(ctx context.Context, fullURL, host string, } // add custom headers - for _, h := range client.headers { - req.Header.Set(h.Name, h.Value) + // if ModifiedHeaders are supplied use those, otherwise use the original ones + // currently only relevant on fuzzing + if len(opts.ModifiedHeaders) > 0 { + for _, h := range opts.ModifiedHeaders { + if client.noCanonicalizeHeaders { + // https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang + req.Header[h.Name] = []string{h.Value} + } else { + req.Header.Set(h.Name, h.Value) + } + } + } else { + for _, h := range client.headers { + if client.noCanonicalizeHeaders { + // https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang + req.Header[h.Name] = []string{h.Value} + } else { + req.Header.Set(h.Name, h.Value) + } + } } - if client.username != "" { + if opts.UpdatedBasicAuthUsername != "" { + req.SetBasicAuth(opts.UpdatedBasicAuthUsername, opts.UpdatedBasicAuthPassword) + } else if client.username != "" { req.SetBasicAuth(client.username, client.password) } diff --git a/libgobuster/libgobuster.go b/libgobuster/libgobuster.go index ad4bedd..e188ebc 100644 --- a/libgobuster/libgobuster.go +++ b/libgobuster/libgobuster.go @@ -175,6 +175,11 @@ Scan: } close(wordChan) workerGroup.Wait() + + if err := scanner.Err(); err != nil { + return err + } + return nil } diff --git a/libgobuster/options_http.go b/libgobuster/options_http.go index 359f552..602d17d 100644 --- a/libgobuster/options_http.go +++ b/libgobuster/options_http.go @@ -1,6 +1,7 @@ package libgobuster import ( + "crypto/tls" "time" ) @@ -12,16 +13,18 @@ type BasicHTTPOptions struct { Timeout time.Duration RetryOnTimeout bool RetryAttempts int + TLSCertificate *tls.Certificate } // HTTPOptions is the struct to pass in all http options to Gobuster type HTTPOptions struct { BasicHTTPOptions - Password string - URL string - Username string - Cookies string - Headers []HTTPHeader - FollowRedirect bool - Method string + Password string + URL string + Username string + Cookies string + Headers []HTTPHeader + NoCanonicalizeHeaders bool + FollowRedirect bool + Method string } diff --git a/libgobuster/version.go b/libgobuster/version.go index d88d3e7..f0e3c1f 100644 --- a/libgobuster/version.go +++ b/libgobuster/version.go @@ -2,5 +2,5 @@ package libgobuster const ( // VERSION contains the current gobuster version - VERSION = "3.2.1" + VERSION = "3.3" )