Files
gobuster/cli/options.go
T
2023-08-30 01:13:54 +02:00

247 lines
9.2 KiB
Go

package cli
import (
"bufio"
"crypto/tls"
"encoding/pem"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
"golang.org/x/crypto/pkcs12"
"golang.org/x/term"
)
func BasicHTTPOptions() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "useragent", Aliases: []string{"a"}, Value: libgobuster.DefaultUserAgent(), Usage: "Set the User-Agent string"},
&cli.BoolFlag{Name: "random-agent", Value: false, Usage: "Use a random User-Agent string"},
&cli.StringFlag{Name: "proxy", Usage: "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]"},
&cli.DurationFlag{Name: "timeout", Value: 10 * time.Second, Usage: "HTTP Timeout"},
&cli.BoolFlag{Name: "no-tls-validation", Aliases: []string{"k"}, Value: false, Usage: "Skip TLS certificate verification"},
&cli.BoolFlag{Name: "retry", Value: false, Usage: "Should retry on request timeout"},
&cli.IntFlag{Name: "retry-attempts", Value: 3, Usage: "Times to retry on request timeout"},
&cli.StringFlag{Name: "client-cert-pem", Usage: "public key in PEM format for optional TLS client certificates]"},
&cli.StringFlag{Name: "client-cert-pem-key", Usage: "private key in PEM format for optional TLS client certificates (this key needs to have no password)"},
&cli.StringFlag{Name: "client-cert-p12", Usage: "a p12 file to use for options TLS client certificates"},
&cli.StringFlag{Name: "client-cert-p12-password", Usage: "the password to the p12 file"},
}
}
func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions, error) {
var opts libgobuster.BasicHTTPOptions
opts.UserAgent = c.String("useragent")
randomUA := c.Bool("random-agent")
if randomUA {
ua, err := libgobuster.GetRandomUserAgent()
if err != nil {
return opts, err
}
opts.UserAgent = ua
}
opts.Proxy = c.String("proxy")
opts.Timeout = c.Duration("timeout")
opts.NoTLSValidation = c.Bool("no-tls-validation")
opts.RetryOnTimeout = c.Bool("retry")
opts.RetryAttempts = c.Int("retry-attempts")
pemFile := c.String("client-cert-pem")
pemKeyFile := c.String("client-cert-pem-key")
p12File := c.String("client-cert-p12")
p12Pass := c.String("client-cert-p12-password")
if pemFile != "" && p12File != "" {
return opts, fmt.Errorf("please supply either a pem or a p12, not both")
}
if pemFile != "" {
cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile)
if err != nil {
return opts, fmt.Errorf("could not load supplied pem key: %w", err)
}
opts.TLSCertificate = &cert
} else if p12File != "" {
p12Content, err := os.ReadFile(p12File)
if err != nil {
return opts, fmt.Errorf("could not read p12 %s: %w", p12File, err)
}
blocks, err := pkcs12.ToPEM(p12Content, p12Pass)
if err != nil {
return opts, fmt.Errorf("could not load P12: %w", err)
}
var pemData []byte
for _, b := range blocks {
pemData = append(pemData, pem.EncodeToMemory(b)...)
}
cert, err := tls.X509KeyPair(pemData, pemData)
if err != nil {
return opts, fmt.Errorf("could not load certificate from P12: %w", err)
}
opts.TLSCertificate = &cert
}
return opts, nil
}
func CommonHTTPOptions() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "url", Aliases: []string{"u"}, Usage: "The target URL", Required: true},
&cli.StringFlag{Name: "cookies", Aliases: []string{"c"}, Usage: "Cookies to use for the requests"},
&cli.StringFlag{Name: "username", Aliases: []string{"U"}, Usage: "Username for Basic Auth"},
&cli.StringFlag{Name: "password", Aliases: []string{"P"}, Usage: "Password for Basic Auth"},
&cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"},
&cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"},
&cli.BoolFlag{Name: "no-canonicalize-headers", Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"},
&cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"},
}...)
flags = append(flags, BasicHTTPOptions()...)
return flags
}
func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) {
var opts libgobuster.HTTPOptions
basic, err := ParseBasicHTTPOptions(c)
if err != nil {
return opts, err
}
opts.BasicHTTPOptions = basic
opts.URL = c.String("url")
if !strings.HasPrefix(opts.URL, "http") {
// check to see if a port was specified
re := regexp.MustCompile(`^[^/]+:(\d+)`)
match := re.FindStringSubmatch(opts.URL)
if len(match) < 2 {
// no port, default to http on 80
opts.URL = fmt.Sprintf("http://%s", opts.URL)
} else {
port, err2 := strconv.Atoi(match[1])
if err2 != nil || (port != 80 && port != 443) {
return opts, fmt.Errorf("url scheme not specified")
} else if port == 80 {
opts.URL = fmt.Sprintf("http://%s", opts.URL)
} else {
opts.URL = fmt.Sprintf("https://%s", opts.URL)
}
}
}
opts.Cookies = c.String("cookies")
opts.Username = c.String("username")
opts.Password = c.String("password")
// Prompt for PW if not provided
if opts.Username != "" && opts.Password == "" {
fmt.Printf("[?] Auth Password: ")
// please don't remove the int cast here as it is sadly needed on windows :/
passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
// print a newline to simulate the newline that was entered
// this means that formatting/printing after doesn't look bad.
fmt.Println("")
if err != nil {
return opts, fmt.Errorf("username given but reading of password failed")
}
opts.Password = string(passBytes)
}
// if it's still empty bail out
if opts.Username != "" && opts.Password == "" {
return opts, fmt.Errorf("username was provided but password is missing")
}
opts.FollowRedirect = c.Bool("follow-redirect")
opts.NoCanonicalizeHeaders = c.Bool("no-canonicalize-headers")
opts.Method = c.String("method")
for _, h := range c.StringSlice("headers") {
keyAndValue := strings.SplitN(h, ":", 2)
if len(keyAndValue) != 2 {
return opts, fmt.Errorf("invalid header format for header %q", h)
}
key := strings.TrimSpace(keyAndValue[0])
value := strings.TrimSpace(keyAndValue[1])
if len(key) == 0 {
return opts, fmt.Errorf("invalid header format for header %q - name is empty", h)
}
header := libgobuster.HTTPHeader{Name: key, Value: value}
opts.Headers = append(opts.Headers, header)
}
return opts, nil
}
func GlobalOptions() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "wordlist", Aliases: []string{"w"}, Usage: "Path to the wordlist. Set to - to use STDIN.", Required: true},
&cli.DurationFlag{Name: "delay", Usage: "Time each thread waits between requests (e.g. 1500ms)"},
&cli.IntFlag{Name: "threads", Aliases: []string{"t"}, Value: 10, Usage: "Number of concurrent threads"},
&cli.IntFlag{Name: "wordlist-offset", Value: 0, Usage: "Resume from a given position in the wordlist"},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output file to write results to (defaults to stdout)"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Value: false, Usage: "Don't print the banner and other noise"},
&cli.BoolFlag{Name: "no-progress", Aliases: []string{"z"}, Value: false, Usage: "Don't display progress"},
&cli.BoolFlag{Name: "no-error", Value: false, Usage: "Don't display errors"},
&cli.StringFlag{Name: "pattern", Aliases: []string{"p"}, Usage: "File containing replacement patterns"},
&cli.BoolFlag{Name: "no-color", Value: false, Usage: "Disable color output"},
&cli.BoolFlag{Name: "debug", Aliases: []string{"d"}, Value: false, Usage: "enable debug output"},
}
}
func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) {
var opts libgobuster.Options
opts.Wordlist = c.String("wordlist")
if opts.Wordlist == "-" {
// STDIN
} else if _, err := os.Stat(opts.Wordlist); os.IsNotExist(err) {
return opts, fmt.Errorf("wordlist file %q does not exist: %w", opts.Wordlist, err)
}
opts.Delay = c.Duration("delay")
opts.Threads = c.Int("threads")
opts.WordlistOffset = c.Int("wordlist-offset")
if opts.Wordlist == "-" && opts.WordlistOffset > 0 {
return opts, fmt.Errorf("wordlist-offset is not supported when reading from STDIN")
} else if opts.WordlistOffset < 0 {
return opts, fmt.Errorf("wordlist-offset must be bigger or equal to 0")
}
opts.OutputFilename = c.String("output")
opts.Quiet = c.Bool("quiet")
opts.NoProgress = c.Bool("no-progress")
opts.NoError = c.Bool("no-error")
opts.PatternFile = c.String("pattern")
if opts.PatternFile != "" {
if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) {
return opts, fmt.Errorf("pattern file %q does not exist: %w", opts.PatternFile, err)
}
patternFile, err := os.Open(opts.PatternFile)
if err != nil {
return opts, fmt.Errorf("could not open pattern file %q: %w", opts.PatternFile, err)
}
defer patternFile.Close()
scanner := bufio.NewScanner(patternFile)
for scanner.Scan() {
opts.Patterns = append(opts.Patterns, scanner.Text())
}
if err := scanner.Err(); err != nil {
return opts, fmt.Errorf("could not read pattern file %q: %w", opts.PatternFile, err)
}
}
if c.Bool("no-color") {
color.NoColor = true
}
opts.Debug = c.Bool("debug")
return opts, nil
}