From 5e491e7635be8340ffe2f1730e330785dbfda62f Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer <105281+firefart@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:26:53 +0200 Subject: [PATCH 1/5] recursion? --- README.md | 574 ++++++++------------------------ cli/dir/dir.go | 12 + gobusterdir/gobusterdir.go | 58 +++- gobusterdir/result.go | 15 +- libgobuster/interfaces.go | 15 + libgobuster/libgobuster.go | 89 ++++- libgobuster/libgobuster_test.go | 125 +++++++ libgobuster/options.go | 3 + 8 files changed, 444 insertions(+), 447 deletions(-) diff --git a/README.md b/README.md index fb22a14c..7b4afa69 100644 --- a/README.md +++ b/README.md @@ -1,509 +1,213 @@ # Gobuster -[![Go Report Card](https://goreportcard.com/badge/github.com/OJ/gobuster/v3)](https://goreportcard.com/report/github.com/OJ/gobuster/v3) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/OJ/gobuster/blob/master/LICENSE) [![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)](https://opencollective.com/gobuster) +[![Go Report Card](https://goreportcard.com/badge/github.com/OJ/gobuster/v3)](https://goreportcard.com/report/github.com/OJ/gobuster/v3) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) +[![Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) -## 💻 Introduction +Gobuster is a fast, flexible enumeration tool written in Go. It uses wordlists to +discover directories and files, DNS names, virtual hosts, cloud storage buckets, +and TFTP files. It can also fuzz values in URLs, headers, and request bodies. -> A fast and flexible brute-forcing tool written in Go +> Use Gobuster only against systems you own or have explicit permission to test. -**Gobuster** is a high-performance directory/file, DNS and virtual host brute-forcing tool written in Go. It's designed to be fast, reliable, and easy to use for security professionals and penetration testers. +## Modes -## ✨ Features +| Mode | Purpose | +| ------- | --------------------------------------------------- | +| `dir` | Discover directories and files on web servers | +| `dns` | Discover DNS subdomains | +| `vhost` | Discover virtual hosts on a web server | +| `fuzz` | Replace `FUZZ` in URLs, headers, and request bodies | +| `s3` | Enumerate Amazon S3 buckets | +| `gcs` | Enumerate Google Cloud Storage buckets | +| `tftp` | Discover files on TFTP servers | -- 🚀 **High Performance**: Multi-threaded scanning with configurable concurrency -- 🔍 **Multiple Modes**: Directory, DNS, virtual host, S3, GCS, TFTP, and fuzzing modes -- 🛡️ **Security Focused**: Built for penetration testing and security assessments -- 🐳 **Docker Support**: Available as a Docker container -- 🔧 **Extensible**: Pattern-based scanning and custom wordlists +## Installation -## 🎯 What Can Gobuster Do? +### Go -- **Web Directory/File Enumeration**: Discover hidden directories and files on web servers -- **DNS Subdomain Discovery**: Find subdomains with wildcard support -- **Virtual Host Detection**: Identify virtual hosts on target web servers -- **Cloud Storage Enumeration**: Discover open Amazon S3 and Google Cloud Storage buckets -- **TFTP File Discovery**: Find files on TFTP servers -- **Custom Fuzzing**: Flexible fuzzing with customizable parameters +Gobuster requires Go 1.27 or newer. -## 🚀 Quick Start - -```bash -# Install gobuster +```console go install github.com/OJ/gobuster/v3@latest - -# Basic directory enumeration -gobuster dir -u https://example.com -w /path/to/wordlist.txt - -# DNS subdomain enumeration -gobuster dns -do example.com -w /path/to/wordlist.txt - -# Virtual host discovery -gobuster vhost -u https://example.com -w /path/to/wordlist.txt - -# S3 bucket enumeration -gobuster s3 -w /path/to/bucket-names.txt ``` -## 📦 Installation - -### Quick Install (Recommended) - -```bash -go install github.com/OJ/gobuster/v3@latest -``` +Make sure the Go binary directory is in your `PATH`. You can find it with +`go env GOBIN`; when that value is empty, Go uses `$(go env GOPATH)/bin`. -**Requirements**: Go 1.24 or higher +### Prebuilt binaries -### Alternative Installation Methods +Download an archive for your platform from the +[GitHub releases page](https://github.com/OJ/gobuster/releases). -#### Using Binary Releases +### Docker -Download pre-compiled binaries from the [releases page](https://github.com/OJ/gobuster/releases). - -#### Using Docker - -```bash -# Pull the latest image +```console docker pull ghcr.io/oj/gobuster:latest - -# Run gobuster in Docker -docker run --rm -it ghcr.io/oj/gobuster:latest dir -u https://example.com -w /usr/share/wordlists/dirb/common.txt -``` - -#### Building from Source - -```bash -git clone https://github.com/OJ/gobuster.git -cd gobuster -go mod tidy -go build +docker run --rm -it \ + -v "$PWD/wordlists:/wordlists:ro" \ + ghcr.io/oj/gobuster:latest \ + dir -u https://example.com -w /wordlists/common.txt ``` -### Troubleshooting Installation +## Quick start -If you encounter issues: +Every mode has its own options. Start with the built-in help when exploring a +new mode: -- Ensure Go version 1.24+ is installed: `go version` -- Check your `$GOPATH` and `$GOBIN` environment variables -- Verify `$GOPATH/bin` is in your `$PATH` - -## 🎯 Usage - -Gobuster uses a mode-based approach. Each mode is designed for specific enumeration tasks: - -```bash -gobuster [mode] [options] +```console +gobuster --help +gobuster dir --help ``` -### Getting Help - -```bash -gobuster help # Show general help -gobuster help [mode] # Show help for specific mode -gobuster [mode] --help # Alternative help syntax -``` - -### 📊 Available Modes - -#### 🌐 Directory Mode (`dir`) - -Enumerate directories and files on web servers. - -**Basic Usage:** +### Directory and file discovery -```bash +```console gobuster dir -u https://example.com -w wordlist.txt ``` -**Advanced Options:** - -```bash -# With file extensions -gobuster dir -u https://example.com -w wordlist.txt -x php,html,js,txt - -# With custom headers and cookies -gobuster dir -u https://example.com -w wordlist.txt -H "Authorization: Bearer token" -c "session=value" - -# Show response length -gobuster dir -u https://example.com -w wordlist.txt -l - -# Filter by status codes -gobuster dir -u https://example.com -w wordlist.txt -s 200,301,302 - -# Filter using a regex against the response body -# This can be handy for websites that return status code 200 for everything, but the html contains an error message -gobuster dir -u https://example.com -w wordlist.txt -re "error\shello" - -# Filter using a regex but inverted against the response body -gobuster dir -u https://example.com -w wordlist.txt -rei "(?i)\berror\b" -``` - -#### 🔍 DNS Mode (`dns`) +Add extensions, choose accepted status codes, and write results to a file: -Discover subdomains through DNS resolution. - -**Basic Usage:** - -```bash -gobuster dns -do example.com -w wordlist.txt +```console +gobuster dir \ + -u https://example.com \ + -w wordlist.txt \ + -x php,html,js \ + -s 200,204,301,302,307,401,403 \ + -b "" \ + -o results.txt ``` -**Advanced Options:** - -```bash -# Use custom DNS server -gobuster dns -do example.com -w wordlist.txt -r 8.8.8.8:53 - -# Increase threads for faster scanning -gobuster dns -do example.com -w wordlist.txt -t 50 -``` +The status-code blacklist defaults to `404` and overrides the positive list, so +explicitly clear it with `-b ""` when using `-s`. -#### 🏠 Virtual Host Mode (`vhost`) +Useful directory-mode options include: -Discover virtual hosts on web servers. +- `-H 'Name: value'` to add a header; repeat it for multiple headers +- `-c 'name=value'` to send cookies +- `-U user -P password` for HTTP Basic authentication +- `-x php,html` to try file extensions +- `--exclude-length 123,456-500` to ignore response sizes +- `--regex PATTERN` or `--regex-invert PATTERN` to filter response bodies +- `--recursive` to scan discovered directories recursively +- `--body-output-dir PATH` to save response bodies +- `-k` to skip TLS certificate verification -**Basic Usage:** +### DNS discovery -```bash -gobuster vhost -u https://example.com --append-domain -w wordlist.txt +```console +gobuster dns --domain example.com -w subdomains.txt ``` -#### ☁️ S3 Mode (`s3`) +Use a custom resolver or inspect CNAME records: -Enumerate Amazon S3 buckets. - -**Basic Usage:** - -```bash -gobuster s3 -w bucket-names.txt +```console +gobuster dns --domain example.com -w subdomains.txt --resolver 1.1.1.1 +gobuster dns --domain example.com -w subdomains.txt --check-cname ``` -**With Debug Output:** +### Virtual-host discovery -```bash -gobuster s3 -w bucket-names.txt --debug +```console +gobuster vhost -u https://example.com -w hosts.txt --append-domain ``` -#### 🖥️ TFTP Mode (`tftp`) +Point `-u` at the server you want to test. Use `--append-domain` when the +wordlist contains prefixes such as `admin` rather than complete hostnames. -Enumerate files on tftp servers. +### Fuzzing -**Basic Usage:** +Put the literal marker `FUZZ` wherever Gobuster should substitute each wordlist +entry: -```bash -gobuster tftp -s 10.0.0.1 -w wordlist.txt +```console +gobuster fuzz -u 'https://example.com/?page=FUZZ' -w values.txt +gobuster fuzz -u https://example.com -H 'X-Api-Version: FUZZ' -w versions.txt +gobuster fuzz -u https://example.com/login -m POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -B 'username=admin&password=FUZZ' \ + -w passwords.txt ``` -#### ☁️ GCS Mode (`gcs`) - -Enumerate Google Cloud Storage Buckets. +### Cloud storage and TFTP -**Basic Usage:** - -```bash +```console +gobuster s3 -w bucket-names.txt gobuster gcs -w bucket-names.txt +gobuster tftp -s 192.0.2.10 -w filenames.txt ``` -**With Debug Output:** - -```bash -gobuster gcs -w bucket-names.txt --debug -``` - -#### 🔧 Fuzz Mode (`fuzz`) - -Custom fuzzing with the `FUZZ` keyword. +## Controlling a scan -**Basic Usage:** +The following options are shared by most modes: -```bash -gobuster fuzz -u https://example.com?FUZZ=test -w wordlist.txt -``` +| Option | Description | +| ---------------- | --------------------------------------------------------- | +| `-w, --wordlist` | Wordlist path; use `-` to read from standard input | +| `-t, --threads` | Number of concurrent workers (default: `10`) | +| `-d, --delay` | Delay applied by each worker, such as `250ms` | +| `--timeout` | Network timeout, such as `15s` | +| `-o, --output` | Write discovered results to a file | +| `-q, --quiet` | Print results without the banner and informational output | +| `--no-progress` | Disable the progress display | +| `--debug` | Enable diagnostic output | -**Advanced Examples:** +Start conservatively and increase concurrency only when the target can handle +it. A delay is often more useful than a very high thread count when testing +rate-limited services. -```bash -# Fuzz URL parameters -gobuster fuzz -u https://example.com?param=FUZZ -w wordlist.txt +## Patterns -# Fuzz headers -gobuster fuzz -u https://example.com -H "X-Custom-Header: FUZZ" -w wordlist.txt +`--pattern` expands every wordlist entry through a pattern file. Each occurrence +of `{GOBUSTER}` is replaced with the current word: -# Fuzz POST data -gobuster fuzz -u https://example.com -d "username=admin&password=FUZZ" -w passwords.txt +```text +{GOBUSTER}-dev +{GOBUSTER}-staging +api-{GOBUSTER} ``` -## 💰 Support - -[![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)](https://opencollective.com/gobuster) - -### Love this tool? Back it! - -If you're backing us already, you rock. If you're not, that's cool too! Want to back us? [Become a backer](https://opencollective.com/gobuster#backer)! - -[![Backers](https://opencollective.com/gobuster/backers.svg?width=890)](https://opencollective.com/gobuster#backers) - -All funds that are donated to this project will be donated to charity. A full log of charity donations will be available in this repository as they are processed. - -## 💡 Common Use Cases - -### Web Application Security Testing - -```bash -# Comprehensive directory enumeration -gobuster dir -u https://target.com -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,js,txt,asp,aspx,jsp - -# API endpoint discovery -gobuster dir -u https://api.target.com -w /usr/share/wordlists/dirb/common.txt -x json - -# Admin panel discovery -gobuster dir -u https://target.com -w admin-panels.txt -s 200,301,302,403 +```console +gobuster dns --domain example.com -w words.txt --pattern patterns.txt ``` -### DNS Reconnaissance +Patterns multiply the number of requests, so review the pattern file before a +large scan. `--discover-pattern` applies a pattern file only to successful +guesses. -```bash -# Comprehensive subdomain enumeration -gobuster dns -do target.com -w /usr/share/wordlists/dnsrecon/subdomains-top1mil-5000.txt -t 50 -``` - -### Cloud Storage Assessment +## Building from source -```bash -# S3 bucket enumeration with patterns -gobuster s3 -w company-names.txt -v +Clone the repository and use [Task](https://taskfile.dev/) for the standard +development workflow: -# GCS bucket enumeration -gobuster gcs -w company-names.txt -v +```console +git clone https://github.com/OJ/gobuster.git +cd gobuster +task build ``` -## 🔧 Troubleshooting - -### Common Issues - -#### "Permission Denied" or "Access Denied" - -- Try reducing thread count with `-t` flag -- Add delays between requests with `--delay` -- Use different user agent with `-a` flag - -#### "Connection Timeout" +Common development commands: -- Increase timeout with `--timeout` flag -- Reduce thread count for slower targets -- Check your internet connection - -#### "No Results Found" - -- Verify the target URL is accessible -- Try different wordlists -- Check status code filtering with `-s` flag - -### Performance Issues - -#### Slow Scanning - -- Increase thread count with `-t` flag (but be careful not to overwhelm the target) -- Use smaller, more targeted wordlists - -## 🎯 Best Practices - -### Security Testing Guidelines - -1. **Always get proper authorization** before testing any target -2. **Start with low thread counts** to avoid overwhelming servers -3. **Use appropriate wordlists** for the target technology -4. **Respect rate limits** and implement delays if needed -5. **Monitor your network traffic** to avoid detection - -### Wordlist Selection - -- **For web applications**: Use technology-specific wordlists (PHP, ASP.NET, etc.) -- **For APIs**: Focus on common API endpoints and versioning patterns -- **For DNS**: Use subdomain-specific wordlists with common patterns -- **For cloud storage**: Use company/brand-specific patterns - -### Output Management - -```bash -# Save results to file -gobuster dir -u https://example.com -w wordlist.txt -o results.txt - -# Use quiet mode for clean output -gobuster dir -u https://example.com -w wordlist.txt -q +```console +task test # format, vet, and run tests with race detection and coverage +task check # format, run gofumpt, vet, and apply Go fixes +task lint # run golangci-lint and verify module files are tidy +task linux # build a Linux AMD64 binary +task windows # build a Windows AMD64 binary ``` -## 📚 Additional Resources - -### Recommended Wordlists - -- **SecLists**: [https://github.com/danielmiessler/SecLists](https://github.com/danielmiessler/SecLists) -- **FuzzDB**: [https://github.com/fuzzdb-project/fuzzdb](https://github.com/fuzzdb-project/fuzzdb) -- **Seclists DNS**: [https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS](https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS) - ---- - -**Happy hacking! 🚀** - -_Remember: Always test responsibly and with proper authorization._ - -# Changes - -
- -3.8.3 - -## 3.8.3 - -- Add option to filter body by regex -- Add option to save response bodies -- Allow comma in Header values passed via the CLI - -
- -
- -3.8.2 - -## 3.8.2 - -- Fix expanded mode to show the full url again - -
- -
- -3.8.1 - -## 3.8.1 - -- Fix expanded mode showing the entries twice - -
- -
- -3.8 - -## 3.8 - -- Add exclude-hostname-length flag to dynamically adjust exclude-length by @0xyy66 -- Fix Fuzzing query parameters -- Add `--force` flag in `dir` mode to continue execution if precheck errors occur - -
- -
- -3.7 - -## 3.7 - -- use new cli library -- a lot more short options due to the new cli library -- more user friendly error messages -- clean up DNS mode -- renamed `show-cname` to `check-cname` in dns mode -- got rid of `verbose` flag and introduced `debug` instead -- the version command now also shows some build variables for more info -- switched to another pkcs12 library to support p12s generated with openssl3 that use SHA256 HMAC -- comments in wordlists (strings starting with #) are no longer ignored -- warn in vhost mode if the --append-domain switch might have been forgotten -- allow to exclude status code and length in vhost mode -- added automaxprocs for use in docker with cpu limits -- log http requests with debug enabled -- allow fuzzing of Host header in fuzz mode -- automatically disable progress output when output is redirected -- fix extra special characters when run with `--no-progress` -- warn when using vhost mode with a proxy and http based urls as this might not work as expected -- add `interface` and `local-ip` parameters to specify the outgoing interface for http requests -- add support for tls renegotiation -- fix progress with patterns by @acammack -- fix backup discovery by @acammack -- support tcp protocol on dns servers -- add support for URL query parameters - -
- -
-3.6 - -## 3.6 - -- Wordlist offset parameter to skip x lines from the wordlist -- prevent double slashes when building up an url in dir mode -- allow for multiple values and ranges on `--exclude-length` -- `no-fqdn` parameter on dns bruteforce to disable the use of the systems search domains. This should speed up the run if you have configured some search domains. [https://github.com/OJ/gobuster/pull/418](https://github.com/OJ/gobuster/pull/418) - -
- -
-3.5 - -## 3.5 - -- Allow Ranges in status code and status code blacklist. Example: 200,300-305,404 - -
- -
-3.4 - -## 3.4 - -- Enable TLS1.0 and TLS1.1 support -- Add TFTP mode to search for files on tftp servers - -
- -
-3.3 - -## 3.3 - -- Support TLS client certificates / mtls -- support loading extensions from file -- support fuzzing POST body, HTTP headers and basic auth -- new option to not canonicalize header names - -
- -
-3.2 - -## 3.2 - -- Use go 1.19 -- use contexts in the correct way -- get rid of the wildcard flag (except in DNS mode) -- color output -- retry on timeout -- google cloud bucket enumeration -- fix nil reference errors - -
- -
-3.1 - -## 3.1 - -- enumerate public AWS S3 buckets -- fuzzing mode -- specify HTTP method -- added support for patterns. You can now specify a file containing patterns that are applied to every word, one by line. Every occurrence of the term `{GOBUSTER}` in it will be replaced with the current wordlist item. Please use with caution as this can cause increase the number of requests issued a lot. -- The shorthand `p` flag which was assigned to proxy is now used by the pattern flag - -
+Bug reports should include the Gobuster version, the mode and command used +(with secrets removed), and relevant debug output. Open an issue before making +a large behavioral change. -
-3.0 +## Support -## 3.0 +Gobuster is maintained by Christian Mehlmauer +([@firefart](https://github.com/firefart)) and OJ Reeves +([@TheColonial](https://github.com/TheColonial)). You can support development +through [Open Collective](https://opencollective.com/gobuster). Project funds +are donated to charity. -- New CLI options so modes are strictly separated (`-m` is now gone!) -- Performance Optimizations and better connection handling -- Ability to enumerate vhost names -- Option to supply custom HTTP headers +## License -
+Gobuster is available under the [Apache License 2.0](LICENSE). diff --git a/cli/dir/dir.go b/cli/dir/dir.go index c8861df9..0c0817c9 100644 --- a/cli/dir/dir.go +++ b/cli/dir/dir.go @@ -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."}, @@ -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 bigger or equal to 0") + } + if globalOpts.RecursionMaxTargets < 0 { + return errors.New("recursion-max-targets must be bigger or equal to 0") + } log := libgobuster.NewLogger(globalOpts.Debug) pluginOpts := gobusterdir.NewOptions() diff --git a/gobusterdir/gobusterdir.go b/gobusterdir/gobusterdir.go index bd683711..13df4ede 100644 --- a/gobusterdir/gobusterdir.go +++ b/gobusterdir/gobusterdir.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "path/filepath" "strings" @@ -49,6 +50,7 @@ type GobusterDir struct { options *OptionsDir globalopts *libgobuster.Options http *libgobuster.HTTPClient + rootURL *url.URL } // New creates a new initialized GobusterDir @@ -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, @@ -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" @@ -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() @@ -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 } @@ -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 @@ -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 diff --git a/gobusterdir/result.go b/gobusterdir/result.go index a1b015e9..fbc8536c 100644 --- a/gobusterdir/result.go +++ b/gobusterdir/result.go @@ -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 diff --git a/libgobuster/interfaces.go b/libgobuster/interfaces.go index 46e21e76..d96ccf57 100644 --- a/libgobuster/interfaces.go +++ b/libgobuster/interfaces.go @@ -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 +} diff --git a/libgobuster/libgobuster.go b/libgobuster/libgobuster.go index 91f1cc1d..00b7ecaf 100644 --- a/libgobuster/libgobuster.go +++ b/libgobuster/libgobuster.go @@ -39,6 +39,11 @@ type Guess struct { discoverOnSuccess bool } +type successfulGuess struct { + guess *Guess + result Result +} + type Wordlist struct { scanner *bufio.Scanner guessesPerLine int @@ -56,7 +61,7 @@ func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (*Gobuste return &g, nil } -func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successChan chan<- *Guess, wg *sync.WaitGroup) { +func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successChan chan<- successfulGuess, wg *sync.WaitGroup) { defer wg.Done() for { // Prioritize stopping when the context is done @@ -69,6 +74,9 @@ func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successC case <-ctx.Done(): return case guess := <-guessChan: + if guess == nil { + return + } // Mode-specific processing res, err := g.plugin.ProcessWord(ctx, guess.word, g.Progress) @@ -84,7 +92,7 @@ func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successC case <-ctx.Done(): g.Progress.incrementRequests() return - case successChan <- guess: + case successChan <- successfulGuess{guess: guess, result: res}: } } @@ -234,6 +242,70 @@ func (g *Gobuster) Run(ctx context.Context) error { defer close(g.Progress.ErrorChan) defer close(g.Progress.MessageChan) + if !g.Opts.Recursion { + return g.runTarget(ctx, nil) + } + plugin, ok := g.plugin.(RecursivePlugin) + if !ok { + return errors.New("the selected plugin does not support recursion") + } + if g.Opts.Wordlist == "-" { + return errors.New("recursion is not supported with a wordlist read from stdin") + } + + type target struct { + url string + depth int + } + queue := []target{{}} + seen := make(map[string]struct{}) + for len(queue) > 0 { + current := queue[0] + queue = queue[1:] + if current.url != "" { + if err := plugin.SetTarget(current.url); err != nil { + return fmt.Errorf("failed to set recursive target %q: %w", current.url, err) + } + } + + var discovered []string + if err := g.runTarget(ctx, func(result Result) { + recursiveResult, ok := result.(RecursiveResult) + if !ok { + return + } + targetURL := recursiveResult.RecursiveTarget() + if targetURL != "" { + discovered = append(discovered, targetURL) + } + }); err != nil { + return err + } + if err := ctx.Err(); err != nil { + return nil + } + if g.Opts.RecursionDepth > 0 && current.depth >= g.Opts.RecursionDepth { + continue + } + for _, targetURL := range discovered { + if _, exists := seen[targetURL]; exists { + continue + } + seen[targetURL] = struct{}{} + if g.Opts.RecursionMaxTargets > 0 && len(seen) > g.Opts.RecursionMaxTargets { + return fmt.Errorf("recursive target limit of %d exceeded", g.Opts.RecursionMaxTargets) + } + queue = append(queue, target{url: targetURL, depth: current.depth + 1}) + } + } + return nil +} + +// runTarget runs one complete wordlist against the plugin's current target. +// It does not close the public channels; Run owns their lifetime across all +// recursive targets. +func (g *Gobuster) runTarget(ctx context.Context, onResult func(Result)) error { + if err := g.plugin.PreRun(ctx, g.Progress); err != nil { return err } @@ -247,7 +319,7 @@ func (g *Gobuster) Run(ctx context.Context) error { workerGroup.Add(g.Opts.Threads) guessChan := make(chan *Guess, g.Opts.Threads*3) - successChan := make(chan *Guess) + successChan := make(chan successfulGuess) scanDone := make(chan error, 1) var f io.ReadSeekCloser @@ -293,17 +365,20 @@ ListenForMore: if scanErr != nil { break ListenForMore } - case successGuess := <-successChan: + case success := <-successChan: + if onResult != nil && success.guess.discoverOnSuccess { + onResult(success.result) + } // Add more guesses based on the results of previous attempts - if successGuess.discoverOnSuccess { - discoverWords := g.plugin.AdditionalSuccessWords(successGuess.word) + if success.guess.discoverOnSuccess { + discoverWords := g.plugin.AdditionalSuccessWords(success.guess.word) if len(discoverWords) > 0 { g.Progress.IncrementTotalRequests(len(discoverWords)) feederGroup.Add(1) go g.feeder(feederCtx, guessChan, discoverWords, false, &feederGroup) } - patternDiscoverWords := g.processDiscoverPatterns(successGuess.word) + patternDiscoverWords := g.processDiscoverPatterns(success.guess.word) if len(patternDiscoverWords) > 0 { g.Progress.IncrementTotalRequests(len(patternDiscoverWords)) feederGroup.Add(1) diff --git a/libgobuster/libgobuster_test.go b/libgobuster/libgobuster_test.go index 8f65b9ca..f78f853d 100644 --- a/libgobuster/libgobuster_test.go +++ b/libgobuster/libgobuster_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "strings" + "sync" "testing" "time" ) @@ -15,6 +16,130 @@ func (testPlugin) PreRun(context.Context, *Progress) error { return nil } func (testPlugin) ProcessWord(context.Context, string, *Progress) (Result, error) { return nil, nil //nolint:nilnil // A test plugin intentionally produces no result. } + +type recursiveTestResult string + +func (recursiveTestResult) ResultToString() (string, error) { return "", nil } +func (r recursiveTestResult) RecursiveTarget() string { return string(r) } + +type recursiveTestPlugin struct { + mu sync.Mutex + target string + started []string + distinctTargets bool +} + +func (*recursiveTestPlugin) Name() string { return "recursive test" } +func (p *recursiveTestPlugin) PreRun(context.Context, *Progress) error { + p.mu.Lock() + defer p.mu.Unlock() + p.started = append(p.started, p.target) + return nil +} +func (p *recursiveTestPlugin) ProcessWord(_ context.Context, word string, _ *Progress) (Result, error) { + p.mu.Lock() + defer p.mu.Unlock() + switch p.target { + case "root": + if p.distinctTargets { + return recursiveTestResult(word), nil + } + return recursiveTestResult("child"), nil + case "child": + return recursiveTestResult("grandchild"), nil + default: + return nil, nil //nolint:nilnil + } +} + +func TestRunRecursionEnforcesTargetLimit(t *testing.T) { + wordlist := t.TempDir() + "/words.txt" + if err := os.WriteFile(wordlist, []byte("one\ntwo\n"), 0o600); err != nil { + t.Fatal(err) + } + plugin := &recursiveTestPlugin{target: "root", distinctTargets: true} + g, err := NewGobuster(&Options{ + Threads: 1, Wordlist: wordlist, Recursion: true, + RecursionMaxTargets: 1, + }, plugin, NewLogger(false)) + if err != nil { + t.Fatal(err) + } + drainProgress(g.Progress) + if err := g.Run(t.Context()); err == nil || !strings.Contains(err.Error(), "target limit") { + t.Fatalf("expected recursive target limit error, got %v", err) + } +} +func (*recursiveTestPlugin) AdditionalWords(string) []string { return nil } +func (*recursiveTestPlugin) AdditionalWordsLen() int { return 0 } +func (*recursiveTestPlugin) AdditionalSuccessWords(string) []string { return nil } +func (*recursiveTestPlugin) GetConfigString() (string, error) { return "", nil } +func (p *recursiveTestPlugin) SetTarget(target string) error { + p.mu.Lock() + defer p.mu.Unlock() + p.target = target + return nil +} + +func TestRunRecursionIsSequentialDeduplicatedAndDepthLimited(t *testing.T) { + wordlist := t.TempDir() + "/words.txt" + if err := os.WriteFile(wordlist, []byte("one\ntwo\n"), 0o600); err != nil { + t.Fatal(err) + } + plugin := &recursiveTestPlugin{target: "root"} + g, err := NewGobuster(&Options{ + Threads: 2, Wordlist: wordlist, Recursion: true, + RecursionDepth: 1, RecursionMaxTargets: 10, + }, plugin, NewLogger(false)) + if err != nil { + t.Fatal(err) + } + drainProgress(g.Progress) + if err := g.Run(t.Context()); err != nil { + t.Fatal(err) + } + plugin.mu.Lock() + defer plugin.mu.Unlock() + if got, want := strings.Join(plugin.started, ","), "root,child"; got != want { + t.Fatalf("scanned targets %q, want %q", got, want) + } +} + +func TestRunRejectsRecursionForUnsupportedPlugin(t *testing.T) { + wordlist := t.TempDir() + "/words.txt" + if err := os.WriteFile(wordlist, []byte("one\ntwo\n"), 0o600); err != nil { + t.Fatal(err) + } + plugin := &recursiveTestPlugin{target: "root"} + g, err := NewGobuster(&Options{ + Threads: 1, Wordlist: wordlist, Recursion: true, + RecursionMaxTargets: 0, + }, plugin, NewLogger(false)) + if err != nil { + t.Fatal(err) + } + // Use a non-recursive plugin to verify the capability check independently. + g.plugin = testPlugin{} + drainProgress(g.Progress) + if err := g.Run(t.Context()); err == nil || !strings.Contains(err.Error(), "does not support recursion") { + t.Fatalf("expected unsupported recursion error, got %v", err) + } +} + +func drainProgress(progress *Progress) { + go func() { + for range progress.ResultChan { + } + }() + go func() { + for range progress.ErrorChan { + } + }() + go func() { + for range progress.MessageChan { + } + }() +} func (testPlugin) AdditionalWords(string) []string { return nil } func (testPlugin) AdditionalWordsLen() int { return 0 } func (testPlugin) AdditionalSuccessWords(string) []string { return nil } diff --git a/libgobuster/options.go b/libgobuster/options.go index 3054aebf..8f69a599 100644 --- a/libgobuster/options.go +++ b/libgobuster/options.go @@ -19,4 +19,7 @@ type Options struct { NoError bool Quiet bool Delay time.Duration + Recursion bool + RecursionDepth int + RecursionMaxTargets int } From 283861259e429c7c2fc69a0908744d26cc8c10f3 Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer <105281+firefart@users.noreply.github.com> Date: Mon, 31 Aug 2026 00:29:45 +0200 Subject: [PATCH 2/5] lint --- libgobuster/libgobuster.go | 1 - libgobuster/libgobuster_test.go | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/libgobuster/libgobuster.go b/libgobuster/libgobuster.go index 00b7ecaf..2bbcecf0 100644 --- a/libgobuster/libgobuster.go +++ b/libgobuster/libgobuster.go @@ -305,7 +305,6 @@ func (g *Gobuster) Run(ctx context.Context) error { // It does not close the public channels; Run owns their lifetime across all // recursive targets. func (g *Gobuster) runTarget(ctx context.Context, onResult func(Result)) error { - if err := g.plugin.PreRun(ctx, g.Progress); err != nil { return err } diff --git a/libgobuster/libgobuster_test.go b/libgobuster/libgobuster_test.go index f78f853d..eef85ee6 100644 --- a/libgobuster/libgobuster_test.go +++ b/libgobuster/libgobuster_test.go @@ -128,15 +128,18 @@ func TestRunRejectsRecursionForUnsupportedPlugin(t *testing.T) { func drainProgress(progress *Progress) { go func() { - for range progress.ResultChan { + for result := range progress.ResultChan { + _ = result } }() go func() { - for range progress.ErrorChan { + for err := range progress.ErrorChan { + _ = err } }() go func() { - for range progress.MessageChan { + for message := range progress.MessageChan { + _ = message } }() } From e302f16d2f2edf3f436e4fc36c1657fbda820b58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:35:12 +0000 Subject: [PATCH 3/5] fix recursive callback and docs Co-authored-by: firefart <105281+firefart@users.noreply.github.com> --- README.md | 2 ++ cli/dir/dir.go | 4 +-- libgobuster/libgobuster.go | 2 +- libgobuster/libgobuster_test.go | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7b4afa69..172eaa73 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Useful directory-mode options include: - `--exclude-length 123,456-500` to ignore response sizes - `--regex PATTERN` or `--regex-invert PATTERN` to filter response bodies - `--recursive` to scan discovered directories recursively +- `--recursion-depth N` to limit recursion depth (`0` for unlimited) +- `--recursion-max-targets N` to cap discovered recursive targets (`0` for unlimited) - `--body-output-dir PATH` to save response bodies - `-k` to skip TLS certificate verification diff --git a/cli/dir/dir.go b/cli/dir/dir.go index 0c0817c9..abe7fa22 100644 --- a/cli/dir/dir.go +++ b/cli/dir/dir.go @@ -55,10 +55,10 @@ func run(c *cli.Context) error { globalOpts.RecursionDepth = c.Int("recursion-depth") globalOpts.RecursionMaxTargets = c.Int("recursion-max-targets") if globalOpts.RecursionDepth < 0 { - return errors.New("recursion-depth must be bigger or equal to 0") + return errors.New("recursion-depth must be greater than or equal to 0") } if globalOpts.RecursionMaxTargets < 0 { - return errors.New("recursion-max-targets must be bigger or equal to 0") + return errors.New("recursion-max-targets must be greater than or equal to 0") } log := libgobuster.NewLogger(globalOpts.Debug) diff --git a/libgobuster/libgobuster.go b/libgobuster/libgobuster.go index 2bbcecf0..ca5df2bb 100644 --- a/libgobuster/libgobuster.go +++ b/libgobuster/libgobuster.go @@ -365,7 +365,7 @@ ListenForMore: break ListenForMore } case success := <-successChan: - if onResult != nil && success.guess.discoverOnSuccess { + if onResult != nil { onResult(success.result) } // Add more guesses based on the results of previous attempts diff --git a/libgobuster/libgobuster_test.go b/libgobuster/libgobuster_test.go index eef85ee6..df23ac0a 100644 --- a/libgobuster/libgobuster_test.go +++ b/libgobuster/libgobuster_test.go @@ -52,6 +52,51 @@ func (p *recursiveTestPlugin) ProcessWord(_ context.Context, word string, _ *Pro } } +type recursiveDiscoveryTestPlugin struct{} + +func (*recursiveDiscoveryTestPlugin) Name() string { return "recursive discovery test" } +func (*recursiveDiscoveryTestPlugin) PreRun(context.Context, *Progress) error { return nil } +func (*recursiveDiscoveryTestPlugin) ProcessWord(_ context.Context, word string, _ *Progress) (Result, error) { + switch word { + case "one": + return recursiveTestResult("root"), nil + case "discovered": + return recursiveTestResult("child"), nil + default: + return nil, nil //nolint:nilnil + } +} + +func (*recursiveDiscoveryTestPlugin) AdditionalWords(string) []string { return nil } +func (*recursiveDiscoveryTestPlugin) AdditionalWordsLen() int { return 0 } +func (*recursiveDiscoveryTestPlugin) AdditionalSuccessWords(string) []string { return []string{"discovered"} } +func (*recursiveDiscoveryTestPlugin) GetConfigString() (string, error) { return "", nil } + +func TestRunTargetInvokesCallbackForDiscoveredResults(t *testing.T) { + wordlist := t.TempDir() + "/words.txt" + if err := os.WriteFile(wordlist, []byte("one\n"), 0o600); err != nil { + t.Fatal(err) + } + g, err := NewGobuster(&Options{Threads: 1, Wordlist: wordlist}, &recursiveDiscoveryTestPlugin{}, NewLogger(false)) + if err != nil { + t.Fatal(err) + } + drainProgress(g.Progress) + var got []string + if err := g.runTarget(t.Context(), func(result Result) { + recursiveResult, ok := result.(RecursiveResult) + if !ok { + return + } + got = append(got, recursiveResult.RecursiveTarget()) + }); err != nil { + t.Fatal(err) + } + if want := "root,child"; strings.Join(got, ",") != want { + t.Fatalf("discovered targets %q, want %q", strings.Join(got, ","), want) + } +} + func TestRunRecursionEnforcesTargetLimit(t *testing.T) { wordlist := t.TempDir() + "/words.txt" if err := os.WriteFile(wordlist, []byte("one\ntwo\n"), 0o600); err != nil { From ccc9977268486b0dd29ba49826190acbcdff316b Mon Sep 17 00:00:00 2001 From: Christian Mehlmauer <105281+firefart@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:10:00 +0200 Subject: [PATCH 4/5] lint --- libgobuster/libgobuster_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libgobuster/libgobuster_test.go b/libgobuster/libgobuster_test.go index df23ac0a..02e49339 100644 --- a/libgobuster/libgobuster_test.go +++ b/libgobuster/libgobuster_test.go @@ -54,7 +54,7 @@ func (p *recursiveTestPlugin) ProcessWord(_ context.Context, word string, _ *Pro type recursiveDiscoveryTestPlugin struct{} -func (*recursiveDiscoveryTestPlugin) Name() string { return "recursive discovery test" } +func (*recursiveDiscoveryTestPlugin) Name() string { return "recursive discovery test" } func (*recursiveDiscoveryTestPlugin) PreRun(context.Context, *Progress) error { return nil } func (*recursiveDiscoveryTestPlugin) ProcessWord(_ context.Context, word string, _ *Progress) (Result, error) { switch word { @@ -67,10 +67,12 @@ func (*recursiveDiscoveryTestPlugin) ProcessWord(_ context.Context, word string, } } -func (*recursiveDiscoveryTestPlugin) AdditionalWords(string) []string { return nil } -func (*recursiveDiscoveryTestPlugin) AdditionalWordsLen() int { return 0 } -func (*recursiveDiscoveryTestPlugin) AdditionalSuccessWords(string) []string { return []string{"discovered"} } -func (*recursiveDiscoveryTestPlugin) GetConfigString() (string, error) { return "", nil } +func (*recursiveDiscoveryTestPlugin) AdditionalWords(string) []string { return nil } +func (*recursiveDiscoveryTestPlugin) AdditionalWordsLen() int { return 0 } +func (*recursiveDiscoveryTestPlugin) AdditionalSuccessWords(string) []string { + return []string{"discovered"} +} +func (*recursiveDiscoveryTestPlugin) GetConfigString() (string, error) { return "", nil } func TestRunTargetInvokesCallbackForDiscoveredResults(t *testing.T) { wordlist := t.TempDir() + "/words.txt" From c098dea3524389a3116574b33049b549b92c78b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Aug 2026 23:20:45 +0000 Subject: [PATCH 5/5] cache recursive wordlist metadata Co-authored-by: firefart <105281+firefart@users.noreply.github.com> --- libgobuster/libgobuster.go | 70 +++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 9 deletions(-) diff --git a/libgobuster/libgobuster.go b/libgobuster/libgobuster.go index ca5df2bb..91092fd8 100644 --- a/libgobuster/libgobuster.go +++ b/libgobuster/libgobuster.go @@ -28,10 +28,16 @@ type ResultToStringFunc func(*Gobuster, *Result) (*string, error) // Gobuster is the main object when creating a new run type Gobuster struct { - Opts *Options - Logger *Logger - plugin GobusterPlugin - Progress *Progress + Opts *Options + Logger *Logger + plugin GobusterPlugin + Progress *Progress + wordlistCache *wordlistCache +} + +type wordlistCache struct { + guessesPerLine int + lineCount int } type Guess struct { @@ -182,8 +188,7 @@ func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- *Guess, wo } } -func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) { - // calculate expected requests +func (g *Gobuster) getWordlistCache() (*wordlistCache, error) { var guessesPerLine int if len(g.Opts.Patterns) > 0 { nPats := len(g.Opts.Patterns) @@ -192,6 +197,40 @@ func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) { guessesPerLine = 1 + g.plugin.AdditionalWordsLen() } + if g.Opts.Wordlist == "-" { + return &wordlistCache{guessesPerLine: guessesPerLine}, nil + } + + f, err := os.Open(g.Opts.Wordlist) + if err != nil { + return nil, fmt.Errorf("failed to open wordlist: %w", err) + } + defer f.Close() + + lines, err := lineCounter(f) + if err != nil { + return nil, fmt.Errorf("failed to get number of lines: %w", err) + } + if lines-g.Opts.WordlistOffset <= 0 { + return nil, errors.New("offset is greater than the number of lines in the wordlist") + } + + return &wordlistCache{guessesPerLine: guessesPerLine, lineCount: lines}, nil +} + +func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) { + guessesPerLine := 0 + if g.wordlistCache != nil { + guessesPerLine = g.wordlistCache.guessesPerLine + } else { + if len(g.Opts.Patterns) > 0 { + nPats := len(g.Opts.Patterns) + guessesPerLine = nPats + nPats*g.plugin.AdditionalWordsLen() + } else { + guessesPerLine = 1 + g.plugin.AdditionalWordsLen() + } + } + if g.Opts.Wordlist == "-" { // Read directly from stdin scanner := bufio.NewScanner(os.Stdin) @@ -199,9 +238,15 @@ func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) { return &Wordlist{scanner: scanner, guessesPerLine: guessesPerLine, isStream: true}, nil } - lines, err := lineCounter(wordlist) - if err != nil { - return nil, fmt.Errorf("failed to get number of lines: %w", err) + lines := 0 + var err error + if g.wordlistCache != nil { + lines = g.wordlistCache.lineCount + } else { + lines, err = lineCounter(wordlist) + if err != nil { + return nil, fmt.Errorf("failed to get number of lines: %w", err) + } } if lines-g.Opts.WordlistOffset <= 0 { @@ -241,6 +286,7 @@ func (g *Gobuster) Run(ctx context.Context) error { defer close(g.Progress.ResultChan) defer close(g.Progress.ErrorChan) defer close(g.Progress.MessageChan) + g.wordlistCache = nil if !g.Opts.Recursion { return g.runTarget(ctx, nil) @@ -253,6 +299,12 @@ func (g *Gobuster) Run(ctx context.Context) error { return errors.New("recursion is not supported with a wordlist read from stdin") } + cache, err := g.getWordlistCache() + if err != nil { + return err + } + g.wordlistCache = cache + type target struct { url string depth int