393 lines
10 KiB
Go
393 lines
10 KiB
Go
|
package gobusterdir
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
"text/tabwriter"
|
||
|
"unicode/utf8"
|
||
|
|
||
|
"git.sual.in/casual/gobuster-lib/libgobuster"
|
||
|
"github.com/google/uuid"
|
||
|
)
|
||
|
|
||
|
// nolint:gochecknoglobals
|
||
|
var (
|
||
|
backupExtensions = []string{"~", ".bak", ".bak2", ".old", ".1"}
|
||
|
backupDotExtensions = []string{".swp"}
|
||
|
)
|
||
|
|
||
|
// ErrWildcard is returned if a wildcard response is found
|
||
|
type ErrWildcard struct {
|
||
|
url string
|
||
|
statusCode int
|
||
|
length int64
|
||
|
}
|
||
|
|
||
|
// Error is the implementation of the error interface
|
||
|
func (e *ErrWildcard) Error() string {
|
||
|
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d (Length: %d)", e.url, e.statusCode, e.length)
|
||
|
}
|
||
|
|
||
|
// GobusterDir is the main type to implement the interface
|
||
|
type GobusterDir struct {
|
||
|
options *OptionsDir
|
||
|
globalopts *libgobuster.Options
|
||
|
http *libgobuster.HTTPClient
|
||
|
}
|
||
|
|
||
|
// NewGobusterDir creates a new initialized GobusterDir
|
||
|
func NewGobusterDir(globalopts *libgobuster.Options, opts *OptionsDir) (*GobusterDir, error) {
|
||
|
if globalopts == nil {
|
||
|
return nil, fmt.Errorf("please provide valid global options")
|
||
|
}
|
||
|
|
||
|
if opts == nil {
|
||
|
return nil, fmt.Errorf("please provide valid plugin options")
|
||
|
}
|
||
|
|
||
|
g := GobusterDir{
|
||
|
options: opts,
|
||
|
globalopts: globalopts,
|
||
|
}
|
||
|
|
||
|
basicOptions := libgobuster.BasicHTTPOptions{
|
||
|
Proxy: opts.Proxy,
|
||
|
Timeout: opts.Timeout,
|
||
|
UserAgent: opts.UserAgent,
|
||
|
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,
|
||
|
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
|
||
|
Cookies: opts.Cookies,
|
||
|
Method: opts.Method,
|
||
|
}
|
||
|
|
||
|
h, err := libgobuster.NewHTTPClient(&httpOpts)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
g.http = h
|
||
|
|
||
|
return &g, nil
|
||
|
}
|
||
|
|
||
|
// Name should return the name of the plugin
|
||
|
func (d *GobusterDir) Name() string {
|
||
|
return "directory enumeration"
|
||
|
}
|
||
|
|
||
|
// PreRun is the pre run implementation of gobusterdir
|
||
|
func (d *GobusterDir) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
|
||
|
// add trailing slash
|
||
|
if !strings.HasSuffix(d.options.URL, "/") {
|
||
|
d.options.URL = fmt.Sprintf("%s/", d.options.URL)
|
||
|
}
|
||
|
|
||
|
_, _, _, _, err := d.http.Request(ctx, d.options.URL, libgobuster.RequestOptions{})
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("unable to connect to %s: %w", d.options.URL, err)
|
||
|
}
|
||
|
|
||
|
guid := uuid.New()
|
||
|
url := fmt.Sprintf("%s%s", d.options.URL, guid)
|
||
|
if d.options.UseSlash {
|
||
|
url = fmt.Sprintf("%s/", url)
|
||
|
}
|
||
|
|
||
|
wildcardResp, wildcardLength, _, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if d.options.ExcludeLengthParsed.Contains(int(wildcardLength)) {
|
||
|
// we are done and ignore the request as the length is excluded
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if d.options.StatusCodesBlacklistParsed.Length() > 0 {
|
||
|
if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
|
||
|
return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
|
||
|
}
|
||
|
} else if d.options.StatusCodesParsed.Length() > 0 {
|
||
|
if d.options.StatusCodesParsed.Contains(wildcardResp) {
|
||
|
return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
|
||
|
}
|
||
|
} else {
|
||
|
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func getBackupFilenames(word string) []string {
|
||
|
ret := make([]string, len(backupExtensions)+len(backupDotExtensions))
|
||
|
i := 0
|
||
|
for _, b := range backupExtensions {
|
||
|
ret[i] = fmt.Sprintf("%s%s", word, b)
|
||
|
i++
|
||
|
}
|
||
|
for _, b := range backupDotExtensions {
|
||
|
ret[i] = fmt.Sprintf(".%s%s", word, b)
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
func (d *GobusterDir) AdditionalWords(word string) []string {
|
||
|
var words []string
|
||
|
// build list of urls to check
|
||
|
// 1: No extension
|
||
|
// 2: With extension
|
||
|
// 3: backupextension
|
||
|
if d.options.DiscoverBackup {
|
||
|
words = append(words, getBackupFilenames(word)...)
|
||
|
}
|
||
|
for ext := range d.options.ExtensionsParsed.Set {
|
||
|
filename := fmt.Sprintf("%s.%s", word, ext)
|
||
|
words = append(words, filename)
|
||
|
if d.options.DiscoverBackup {
|
||
|
words = append(words, getBackupFilenames(filename)...)
|
||
|
}
|
||
|
}
|
||
|
return words
|
||
|
}
|
||
|
|
||
|
// ProcessWord is the process implementation of gobusterdir
|
||
|
func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
|
||
|
suffix := ""
|
||
|
if d.options.UseSlash {
|
||
|
suffix = "/"
|
||
|
}
|
||
|
entity := fmt.Sprintf("%s%s", word, suffix)
|
||
|
|
||
|
// make sure the url ends with a slash
|
||
|
if !strings.HasSuffix(d.options.URL, "/") {
|
||
|
d.options.URL = fmt.Sprintf("%s/", d.options.URL)
|
||
|
}
|
||
|
// prevent double slashes by removing leading /
|
||
|
if strings.HasPrefix(entity, "/") {
|
||
|
// get size of first rune and trim it
|
||
|
_, i := utf8.DecodeRuneInString(entity)
|
||
|
entity = entity[i:]
|
||
|
}
|
||
|
url := fmt.Sprintf("%s%s", d.options.URL, entity)
|
||
|
|
||
|
tries := 1
|
||
|
if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
|
||
|
// add it so it will be the overall max requests
|
||
|
tries += d.options.RetryAttempts
|
||
|
}
|
||
|
|
||
|
var statusCode int
|
||
|
var size int64
|
||
|
var header http.Header
|
||
|
for i := 1; i <= tries; i++ {
|
||
|
var err error
|
||
|
statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.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
|
||
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
|
||
|
continue
|
||
|
} else if strings.Contains(err.Error(), "invalid control character in URL") {
|
||
|
// put error in error chan so it's printed out and ignore it
|
||
|
// so gobuster will not quit
|
||
|
progress.ErrorChan <- err
|
||
|
continue
|
||
|
} else {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if statusCode != 0 {
|
||
|
resultStatus := false
|
||
|
|
||
|
if d.options.StatusCodesBlacklistParsed.Length() > 0 {
|
||
|
if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
|
||
|
resultStatus = true
|
||
|
}
|
||
|
} else if d.options.StatusCodesParsed.Length() > 0 {
|
||
|
if d.options.StatusCodesParsed.Contains(statusCode) {
|
||
|
resultStatus = true
|
||
|
}
|
||
|
} else {
|
||
|
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
|
||
|
}
|
||
|
|
||
|
if (resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size))) || d.globalopts.Verbose {
|
||
|
progress.ResultChan <- Result{
|
||
|
URL: d.options.URL,
|
||
|
Path: entity,
|
||
|
Verbose: d.globalopts.Verbose,
|
||
|
Expanded: d.options.Expanded,
|
||
|
NoStatus: d.options.NoStatus,
|
||
|
HideLength: d.options.HideLength,
|
||
|
Found: resultStatus,
|
||
|
Header: header,
|
||
|
StatusCode: statusCode,
|
||
|
Size: size,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// GetConfigString returns the string representation of the current config
|
||
|
func (d *GobusterDir) GetConfigString() (string, error) {
|
||
|
var buffer bytes.Buffer
|
||
|
bw := bufio.NewWriter(&buffer)
|
||
|
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
|
||
|
o := d.options
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if d.globalopts.Delay > 0 {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
wordlist := "stdin (pipe)"
|
||
|
if d.globalopts.Wordlist != "-" {
|
||
|
wordlist = d.globalopts.Wordlist
|
||
|
}
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if d.globalopts.PatternFile != "" {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.StatusCodesBlacklistParsed.Length() > 0 {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Negative Status codes:\t%s\n", o.StatusCodesBlacklistParsed.Stringify()); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
} else if o.StatusCodesParsed.Length() > 0 {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Status codes:\t%s\n", o.StatusCodesParsed.Stringify()); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(o.ExcludeLength) > 0 {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.Proxy != "" {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.Cookies != "" {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.UserAgent != "" {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.HideLength {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Show length:\tfalse\n"); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.Username != "" {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.FollowRedirect {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.Expanded {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Expanded:\ttrue\n"); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if o.NoStatus {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] No status:\ttrue\n"); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if d.globalopts.Verbose {
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
if err := tw.Flush(); err != nil {
|
||
|
return "", fmt.Errorf("error on tostring: %w", err)
|
||
|
}
|
||
|
|
||
|
if err := bw.Flush(); err != nil {
|
||
|
return "", fmt.Errorf("error on tostring: %w", err)
|
||
|
}
|
||
|
|
||
|
return strings.TrimSpace(buffer.String()), nil
|
||
|
}
|