Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
576 changes: 141 additions & 435 deletions README.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions cli/dir/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func getFlags() []cli.Flag {
&cli.BoolFlag{Name: "hide-length", Aliases: []string{"hl"}, Value: false, Usage: "Hide the length of the body in the output"},
&cli.BoolFlag{Name: "add-slash", Aliases: []string{"f"}, Value: false, Usage: "Append / to each request"},
&cli.BoolFlag{Name: "discover-backup", Aliases: []string{"db"}, Value: false, Usage: "Upon finding a file search for backup files by appending multiple backup extensions"},
&cli.BoolFlag{Name: "recursive", Usage: "Recursively scan discovered directories"},
&cli.IntFlag{Name: "recursion-depth", Value: 5, Usage: "Maximum recursion depth (0 for unlimited)"},
&cli.IntFlag{Name: "recursion-max-targets", Value: 1000, Usage: "Maximum number of discovered targets (0 for unlimited)"},
&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
&cli.BoolFlag{Name: "force", Value: false, Usage: "Continue even if the prechecks fail. Please only use this if you know what you are doing, it can lead to unexpected results."},
&cli.StringFlag{Name: "regex", Aliases: []string{"re"}, Usage: "Use regex to filter the results, by inspecting the content of the response body. When using this option be sure to set the status-codes and status-codes-blacklist options accordingly. The regex check is done after the status code checks. Only responses matching the regex will be displayed."},
Expand All @@ -48,6 +51,15 @@ func run(c *cli.Context) error {
if err != nil {
return err
}
globalOpts.Recursion = c.Bool("recursive")
globalOpts.RecursionDepth = c.Int("recursion-depth")
globalOpts.RecursionMaxTargets = c.Int("recursion-max-targets")
if globalOpts.RecursionDepth < 0 {
return errors.New("recursion-depth must be greater than or equal to 0")
}
Comment thread
Copilot marked this conversation as resolved.
if globalOpts.RecursionMaxTargets < 0 {
return errors.New("recursion-max-targets must be greater than or equal to 0")
}
log := libgobuster.NewLogger(globalOpts.Debug)

pluginOpts := gobusterdir.NewOptions()
Expand Down
58 changes: 57 additions & 1 deletion gobusterdir/gobusterdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -49,6 +50,7 @@ type GobusterDir struct {
options *OptionsDir
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
rootURL *url.URL
}

// New creates a new initialized GobusterDir
Expand All @@ -65,6 +67,10 @@ func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.
options: opts,
globalopts: globalopts,
}
if opts.URL != nil {
rootURL := *opts.URL
g.rootURL = &rootURL
}

basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Expand Down Expand Up @@ -99,6 +105,28 @@ func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.
return &g, nil
}

// SetTarget changes the base URL between recursive scans. The orchestrator
// calls this only after all workers for the previous target have stopped.
func (d *GobusterDir) SetTarget(target string) error {
u, err := url.Parse(target)
if err != nil {
return err
}
if d.rootURL == nil {
return errors.New("initial URL is not set")
}
if !strings.EqualFold(u.Scheme, d.rootURL.Scheme) || !strings.EqualFold(u.Host, d.rootURL.Host) {
return errors.New("recursive target must have the same scheme and host as the initial URL")
}
u.RawQuery = ""
u.Fragment = ""
if !strings.HasSuffix(u.Path, "/") {
u.Path += "/"
}
d.options.URL = u
return nil
}

// Name should return the name of the plugin
func (d *GobusterDir) Name() string {
return "directory enumeration"
Expand Down Expand Up @@ -336,7 +364,11 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
}

if resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size)) {
path := fmt.Sprintf("%-20s", entity)
displayPath := entity
if d.globalopts.Recursion {
displayPath = fmt.Sprintf("%s%s", d.options.URL.Path, entity)
}
path := fmt.Sprintf("%-20s", displayPath)
if d.options.Expanded {
// expanded mode should show the full url
path = url.String()
Expand All @@ -348,6 +380,15 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
StatusCode: -1,
Size: -1,
}
if d.globalopts.Recursion && d.isDirectoryCandidate(word) {
recursionURL := url
if !strings.HasSuffix(recursionURL.Path, "/") {
recursionURL.Path += "/"
}
recursionURL.RawQuery = ""
recursionURL.Fragment = ""
r.recursionTarget = recursionURL.String()
}
if !d.options.NoStatus {
r.StatusCode = statusCode
}
Expand All @@ -361,6 +402,15 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
return nil, nil // nolint:nilnil
}

func (d *GobusterDir) isDirectoryCandidate(word string) bool {
for ext := range d.options.ExtensionsParsed.Set {
if strings.HasSuffix(word, "."+ext) {
return false
}
}
return true
}

// GetConfigString returns the string representation of the current config
func (d *GobusterDir) GetConfigString() (string, error) {
var buffer bytes.Buffer
Expand All @@ -385,6 +435,12 @@ func (d *GobusterDir) GetConfigString() (string, error) {
}
}

if d.globalopts.Recursion {
if _, err := fmt.Fprintf(tw, "[+] Recursion:\tenabled (depth %d, max targets %d)\n", d.globalopts.RecursionDepth, d.globalopts.RecursionMaxTargets); err != nil {
return "", err
}
}

wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
Expand Down
15 changes: 11 additions & 4 deletions gobusterdir/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ var (

// Result represents a single result
type Result struct {
Path string
Header http.Header
StatusCode int
Size int64
Path string
Header http.Header
StatusCode int
Size int64
recursionTarget string
}

// RecursiveTarget returns the next URL to scan, or an empty string when this
// result represents a file rather than a directory candidate.
func (r Result) RecursiveTarget() string {
return r.recursionTarget
}

// ResultToString converts the Result to its textual representation
Expand Down
15 changes: 15 additions & 0 deletions libgobuster/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,18 @@ type GobusterPlugin interface {
type Result interface {
ResultToString() (string, error)
}

// RecursiveResult is implemented by results which can seed another scan.
// An empty target means that the result must not be recursed into.
type RecursiveResult interface {
Result
RecursiveTarget() string
}

// RecursivePlugin is implemented by plugins which can change their target
// between scans. SetTarget is only called after the previous scan has fully
// stopped, so implementations do not need to synchronize target access.
type RecursivePlugin interface {
GobusterPlugin
SetTarget(string) error
}
Loading
Loading