mirror of
https://github.com/OJ/gobuster.git
synced 2025-07-23 11:24:16 +02:00
327 lines
12 KiB
Go
327 lines
12 KiB
Go
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
|
|
}
|