package cli import ( "bufio" "crypto/tls" "errors" "fmt" "net" "net/url" "os" "regexp" "strconv" "strings" "syscall" "time" "github.com/OJ/gobuster/v3/libgobuster" "github.com/fatih/color" "github.com/urfave/cli/v2" "golang.org/x/term" "software.sslmate.com/src/go-pkcs12" ) func BasicHTTPOptions() []cli.Flag { return []cli.Flag{ &cli.StringFlag{Name: "useragent", Aliases: []string{"a"}, Value: libgobuster.DefaultUserAgent(), Usage: "Set the User-Agent string"}, &cli.BoolFlag{Name: "random-agent", Aliases: []string{"rua"}, Value: false, Usage: "Use a random User-Agent string"}, &cli.StringFlag{Name: "proxy", Usage: "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]"}, &cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 10 * time.Second, Usage: "HTTP Timeout"}, &cli.BoolFlag{Name: "no-tls-validation", Aliases: []string{"k"}, Value: false, Usage: "Skip TLS certificate verification"}, &cli.BoolFlag{Name: "retry", Value: false, Usage: "Should retry on request timeout"}, &cli.IntFlag{Name: "retry-attempts", Aliases: []string{"ra"}, Value: 3, Usage: "Times to retry on request timeout"}, &cli.StringFlag{Name: "client-cert-pem", Aliases: []string{"ccp"}, Usage: "public key in PEM format for optional TLS client certificates]"}, &cli.StringFlag{Name: "client-cert-pem-key", Aliases: []string{"ccpk"}, Usage: "private key in PEM format for optional TLS client certificates (this key needs to have no password)"}, &cli.StringFlag{Name: "client-cert-p12", Aliases: []string{"ccp12"}, Usage: "a p12 file to use for options TLS client certificates"}, &cli.StringFlag{Name: "client-cert-p12-password", Aliases: []string{"ccp12p"}, Usage: "the password to the p12 file"}, &cli.BoolFlag{Name: "tls-renegotiation", Value: false, Usage: "Enable TLS renegotiation"}, &cli.StringFlag{Name: "interface", Aliases: []string{"iface"}, Usage: "specify network interface to use. Can't be used with local-ip"}, &cli.StringFlag{Name: "local-ip", Usage: "specify local ip of network interface to use. Can't be used with interface"}, } } func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions, error) { var opts libgobuster.BasicHTTPOptions opts.UserAgent = c.String("useragent") randomUA := c.Bool("random-agent") if randomUA { ua, err := libgobuster.GetRandomUserAgent() if err != nil { return opts, err } opts.UserAgent = ua } opts.Proxy = c.String("proxy") opts.Timeout = c.Duration("timeout") opts.NoTLSValidation = c.Bool("no-tls-validation") opts.RetryOnTimeout = c.Bool("retry") opts.RetryAttempts = c.Int("retry-attempts") pemFile := c.String("client-cert-pem") pemKeyFile := c.String("client-cert-pem-key") p12File := c.String("client-cert-p12") p12Pass := c.String("client-cert-p12-password") if pemFile != "" && p12File != "" { return opts, errors.New("please supply either a pem or a p12, not both") } if pemFile != "" { cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile) if err != nil { return opts, fmt.Errorf("could not load supplied pem key: %w", err) } opts.TLSCertificate = &cert } else if p12File != "" { p12Content, err := os.ReadFile(p12File) if err != nil { return opts, fmt.Errorf("could not read p12 %s: %w", p12File, err) } privKey, pubKey, _, err := pkcs12.DecodeChain(p12Content, p12Pass) if err != nil { return opts, fmt.Errorf("could not load P12: %w", err) } opts.TLSCertificate = &tls.Certificate{ Certificate: [][]byte{pubKey.Raw}, PrivateKey: privKey, } } opts.TLSRenegotiation = c.Bool("tls-renegotiation") iface := c.String("interface") localIP := c.String("local-ip") if iface != "" && localIP != "" { return opts, errors.New("can not set both interface and local-ip") } switch { case iface != "": a, err := getLocalAddrFromInterface(iface) if err != nil { return opts, err } opts.LocalAddr = a case localIP != "": if !strings.Contains(localIP, ":") { localIP = fmt.Sprintf("%s:0", localIP) } a, err := net.ResolveIPAddr("ip", localIP) if err != nil { return opts, err } localTCPAddr := net.TCPAddr{ IP: a.IP, } opts.LocalAddr = &localTCPAddr } return opts, nil } func CommonHTTPOptions() []cli.Flag { var flags []cli.Flag flags = append(flags, []cli.Flag{ &cli.StringFlag{Name: "url", Aliases: []string{"u"}, Usage: "The target URL", Required: true}, &cli.StringFlag{Name: "cookies", Aliases: []string{"c"}, Usage: "Cookies to use for the requests"}, &cli.StringFlag{Name: "username", Aliases: []string{"U"}, Usage: "Username for Basic Auth"}, &cli.StringFlag{Name: "password", Aliases: []string{"P"}, Usage: "Password for Basic Auth"}, &cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"}, &cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"}, &cli.BoolFlag{Name: "no-canonicalize-headers", Aliases: []string{"nch"}, Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"}, &cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"}, }...) flags = append(flags, BasicHTTPOptions()...) return flags } func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) { var opts libgobuster.HTTPOptions basic, err := ParseBasicHTTPOptions(c) if err != nil { return opts, err } opts.BasicHTTPOptions = basic urlInput := c.String("url") if !strings.HasPrefix(urlInput, "http") { // check to see if a port was specified re := regexp.MustCompile(`^[^/]+:(\d+)`) match := re.FindStringSubmatch(urlInput) if len(match) < 2 { // no port, default to http on 80 urlInput = fmt.Sprintf("http://%s", urlInput) } else { port, err2 := strconv.Atoi(match[1]) switch { case err2 != nil || (port != 80 && port != 443): return opts, errors.New("url scheme not specified") case port == 80: urlInput = fmt.Sprintf("http://%s", urlInput) default: urlInput = fmt.Sprintf("https://%s", urlInput) } } } if opts.URL, err = url.Parse(urlInput); err != nil { return opts, fmt.Errorf("url %q is not valid: %w", urlInput, err) } opts.Cookies = c.String("cookies") opts.Username = c.String("username") opts.Password = c.String("password") // Prompt for PW if not provided if opts.Username != "" && opts.Password == "" { fmt.Printf("[?] Auth Password: ") // nolint:forbidigo // 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("") // nolint:forbidigo if err != nil { return opts, errors.New("username given but reading of password failed") } opts.Password = string(passBytes) } // if it's still empty bail out if opts.Username != "" && opts.Password == "" { return opts, errors.New("username was provided but password is missing") } opts.FollowRedirect = c.Bool("follow-redirect") opts.NoCanonicalizeHeaders = c.Bool("no-canonicalize-headers") opts.Method = c.String("method") for _, h := range c.StringSlice("headers") { keyAndValue := strings.SplitN(h, ":", 2) if len(keyAndValue) != 2 { return opts, fmt.Errorf("invalid header format for header %q", h) } key := strings.TrimSpace(keyAndValue[0]) value := strings.TrimSpace(keyAndValue[1]) if len(key) == 0 { return opts, fmt.Errorf("invalid header format for header %q - name is empty", h) } header := libgobuster.HTTPHeader{Name: key, Value: value} opts.Headers = append(opts.Headers, header) } return opts, nil } func GlobalOptions() []cli.Flag { return []cli.Flag{ &cli.StringFlag{Name: "wordlist", Aliases: []string{"w"}, Usage: "Path to the wordlist. Set to - to use STDIN.", Required: true}, &cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "Time each thread waits between requests (e.g. 1500ms)"}, &cli.IntFlag{Name: "threads", Aliases: []string{"t"}, Value: 10, Usage: "Number of concurrent threads"}, &cli.IntFlag{Name: "wordlist-offset", Aliases: []string{"wo"}, Value: 0, Usage: "Resume from a given position in the wordlist"}, &cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output file to write results to (defaults to stdout)"}, &cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Value: false, Usage: "Don't print the banner and other noise"}, &cli.BoolFlag{Name: "no-progress", Aliases: []string{"np"}, Value: false, Usage: "Don't display progress"}, &cli.BoolFlag{Name: "no-error", Aliases: []string{"ne"}, Value: false, Usage: "Don't display errors"}, &cli.StringFlag{Name: "pattern", Aliases: []string{"p"}, Usage: "File containing replacement patterns"}, &cli.StringFlag{Name: "discover-pattern", Aliases: []string{"pd"}, Usage: "File containing replacement patterns applied to successful guesses"}, &cli.BoolFlag{Name: "no-color", Aliases: []string{"nc"}, Value: false, Usage: "Disable color output"}, &cli.BoolFlag{Name: "debug", Value: false, Usage: "enable debug output"}, } } func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) { var opts libgobuster.Options opts.Wordlist = c.String("wordlist") if opts.Wordlist == "-" { // nolint:revive // STDIN } else if _, err := os.Stat(opts.Wordlist); os.IsNotExist(err) { return opts, fmt.Errorf("wordlist file %q does not exist: %w", opts.Wordlist, err) } opts.Delay = c.Duration("delay") opts.Threads = c.Int("threads") opts.WordlistOffset = c.Int("wordlist-offset") if opts.Wordlist == "-" && opts.WordlistOffset > 0 { return opts, errors.New("wordlist-offset is not supported when reading from STDIN") } else if opts.WordlistOffset < 0 { return opts, errors.New("wordlist-offset must be bigger or equal to 0") } opts.OutputFilename = c.String("output") opts.Quiet = c.Bool("quiet") opts.NoProgress = c.Bool("no-progress") opts.NoError = c.Bool("no-error") opts.PatternFile = c.String("pattern") if opts.PatternFile != "" { if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) { return opts, fmt.Errorf("pattern file %q does not exist: %w", opts.PatternFile, err) } patternFile, err := os.Open(opts.PatternFile) if err != nil { return opts, fmt.Errorf("could not open pattern file %q: %w", opts.PatternFile, err) } defer patternFile.Close() scanner := bufio.NewScanner(patternFile) for scanner.Scan() { opts.Patterns = append(opts.Patterns, scanner.Text()) } if err := scanner.Err(); err != nil { return opts, fmt.Errorf("could not read pattern file %q: %w", opts.PatternFile, err) } } opts.DiscoverPatternFile = c.String("discover-pattern") if opts.DiscoverPatternFile != "" { if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) { return opts, fmt.Errorf("discover pattern file %q does not exist: %w", opts.DiscoverPatternFile, err) } discoverPatternFile, err := os.Open(opts.DiscoverPatternFile) if err != nil { return opts, fmt.Errorf("could not open discover pattern file %q: %w", opts.DiscoverPatternFile, err) } defer discoverPatternFile.Close() scanner := bufio.NewScanner(discoverPatternFile) for scanner.Scan() { opts.DiscoverPatterns = append(opts.DiscoverPatterns, scanner.Text()) } if err := scanner.Err(); err != nil { return opts, fmt.Errorf("could not read discover pattern file %q: %w", opts.DiscoverPatternFile, err) } } if c.Bool("no-color") { color.NoColor = true } opts.Debug = c.Bool("debug") return opts, nil } func getLocalAddrFromInterface(iface string) (*net.TCPAddr, error) { i, err := net.InterfaceByName(iface) if err != nil { return nil, fmt.Errorf("could not get interface %s: %w", iface, err) } addrs, err := i.Addrs() if err != nil { return nil, fmt.Errorf("could not get local addrs for iface %s: %w", i.Name, err) } if len(addrs) == 0 { return nil, fmt.Errorf("no ip addresses on interface %s", iface) } tmp, ok := addrs[0].(*net.IPNet) if !ok { return nil, fmt.Errorf("could not get ipnet address from interface %s", iface) } tcpAddr := &net.TCPAddr{ IP: tmp.IP, } return tcpAddr, err }