Skip to content
Open

3.8.3 #602

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
2 changes: 1 addition & 1 deletion .github/workflows/hadolint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: hadolint/hadolint-action@v3.2.0
- uses: hadolint/hadolint-action@v3.3.0
with:
dockerfile: Dockerfile
# DL3007: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
Expand Down
18 changes: 13 additions & 5 deletions .github/workflows/vhs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ on:
push:
paths:
- vhs/**.tape
schedule:
# every week
- cron: "0 0 * * 0"
workflow_dispatch:

permissions:
contents: write
Expand All @@ -29,19 +33,23 @@ jobs:
- name: Build linux
run: task linux

- name: Install deps
- name: Install vhs deps
run: |
sudo apt update
sudo apt install -y ffmpeg ttyd

- uses: charmbracelet/vhs-action@v2
with:
path: "vhs/gobuster_dir.tape"
- name: Install vhs
run: |
go install github.com/charmbracelet/vhs@latest

- name: Generate vhs gif
run: |
vhs vhs/gobuster_dir.tape -o vhs/gobuster_dir.gif

- name: commit and push changes
run: |
git config user.name "Github"
git config user.email "<>"
git add vhs/*.gif
git commit -m "update vhs gifs" || echo "no changes to commit"
git push origin master
git push
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ config.json
gobuster
*.txt
dist/
wordlist
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ 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`)
Expand Down Expand Up @@ -344,6 +351,17 @@ _Remember: Always test responsibly and with proper authorization._

<details>

<summary>3.8.3</summary>

## 3.8.3

- Add option to filter body by regex
- Add option to save response bodies

</details>

<details>

<summary>3.8.2</summary>

## 3.8.2
Expand Down
31 changes: 27 additions & 4 deletions cli/dir/dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dir
import (
"errors"
"fmt"
"regexp"

internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterdir"
Expand Down Expand Up @@ -36,11 +37,19 @@ func getFlags() []cli.Flag {
&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.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."},
&cli.StringFlag{Name: "regex-invert", Aliases: []string{"rei"}, Usage: "Use regex to filter the results, but inverted, 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 NOT matching the regex will be displayed."},
}...)
return flags
}

func run(c *cli.Context) error {
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
log := libgobuster.NewLogger(globalOpts.Debug)

pluginOpts := gobusterdir.NewOptions()

httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
Expand Down Expand Up @@ -101,12 +110,26 @@ func run(c *cli.Context) error {
}
pluginOpts.ExcludeLengthParsed = ret4

globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
if c.IsSet("regex") && c.IsSet("regex-invert") {
return errors.New("regex and regex-invert are mutually exclusive, please set only one")
}

log := libgobuster.NewLogger(globalOpts.Debug)
if c.IsSet("regex") && c.String("regex") != "" {
regex, err := regexp.Compile(c.String("regex"))
if err != nil {
return fmt.Errorf("invalid value for regex: %w", err)
}
pluginOpts.Regex = regex
}

if c.IsSet("regex-invert") && c.String("regex-invert") != "" {
regex, err := regexp.Compile(c.String("regex-invert"))
if err != nil {
return fmt.Errorf("invalid value for regex-invert: %w", err)
}
pluginOpts.Regex = regex
pluginOpts.RegexInvert = true
}

plugin, err := gobusterdir.New(&globalOpts, pluginOpts, log)
if err != nil {
Expand Down
9 changes: 9 additions & 0 deletions cli/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ func CommonHTTPOptions() []cli.Flag {
&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"},
&cli.StringFlag{Name: "body-output-dir", Usage: "Directory to store response bodies"},
}...)
flags = append(flags, BasicHTTPOptions()...)
return flags
Expand Down Expand Up @@ -209,6 +210,14 @@ func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) {
opts.Headers = append(opts.Headers, header)
}

if c.IsSet("body-output-dir") {
opts.BodyOutputDir = c.String("body-output-dir")
err = os.MkdirAll(opts.BodyOutputDir, 0o755)
if err != nil {
return opts, fmt.Errorf("could not create body output dir %q: %w", opts.BodyOutputDir, err)
}
}

return opts, nil
}

Expand Down
16 changes: 8 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/pin/tftp/v3 v3.1.0
github.com/urfave/cli/v2 v2.27.7
go.uber.org/automaxprocs v1.6.0
golang.org/x/term v0.34.0
golang.org/x/term v0.36.0
software.sslmate.com/src/go-pkcs12 v0.6.0
)

Expand All @@ -19,13 +19,13 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/tools v0.36.0 // indirect
mvdan.cc/gofumpt v0.9.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/tools v0.37.0 // indirect
mvdan.cc/gofumpt v0.9.1 // indirect
)

tool mvdan.cc/gofumpt
32 changes: 16 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,27 +37,27 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBi
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/gofumpt v0.9.0 h1:W0wNHMSvDBDIyZsm3nnGbVfgp5AknzBrGJnfLCy501w=
mvdan.cc/gofumpt v0.9.0/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
mvdan.cc/gofumpt v0.9.1 h1:p5YT2NfFWsYyTieYgwcQ8aKV3xRvFH4uuN/zB2gBbMQ=
mvdan.cc/gofumpt v0.9.1/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
61 changes: 56 additions & 5 deletions gobusterdir/gobusterdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"syscall"
"text/tabwriter"
Expand Down Expand Up @@ -40,7 +41,7 @@ func (e *WildcardError) Error() string {
} else {
addInfo = fmt.Sprintf("%s => %d (Length: %d)", e.url, e.statusCode, e.length)
}
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length or the status code or set the wildcard option.", addInfo)
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length (or as a range), the status code or set the force option (but expect false positives).", addInfo)
}

// GobusterDir is the main type to implement the interface
Expand Down Expand Up @@ -86,6 +87,7 @@ func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
BodyOutputDir: opts.BodyOutputDir,
}

h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
Expand All @@ -102,6 +104,18 @@ func (d *GobusterDir) Name() string {
return "directory enumeration"
}

func (d *GobusterDir) regexBodyIsAMatch(body []byte) bool {
switch {
case d.options.Regex != nil && body != nil:
if d.options.RegexInvert {
return !d.options.Regex.Match(body)
}
return d.options.Regex.Match(body)
default:
return true
}
}

// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) error {
// add trailing slash
Expand Down Expand Up @@ -139,7 +153,7 @@ func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) erro
url.Path = fmt.Sprintf("%s/", url.Path)
}

wildcardResp, wildcardLength, wildcardHeader, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
wildcardResp, wildcardLength, wildcardHeader, wildcardBody, err := d.http.Request(ctx, url, libgobuster.RequestOptions{ReturnBody: true})
if err != nil {
var retErr error
switch {
Expand Down Expand Up @@ -170,10 +184,16 @@ func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) erro
switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
if d.regexBodyIsAMatch(wildcardBody) {
return nil
}
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(wildcardResp) {
if d.regexBodyIsAMatch(wildcardBody) {
return nil
}
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
default:
Expand Down Expand Up @@ -252,9 +272,16 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
var statusCode int
var size int64
var header http.Header
var body []byte

requestOptions := libgobuster.RequestOptions{}
if d.options.Regex != nil || d.options.BodyOutputDir != "" {
requestOptions.ReturnBody = true
}

for i := 1; i <= tries; i++ {
var err error
statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{})
statusCode, size, header, body, err = d.http.Request(ctx, url, 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
Expand All @@ -280,17 +307,29 @@ func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *li
break
}

if d.options.BodyOutputDir != "" && body != nil {
fname := libgobuster.SanitizeFilename(fmt.Sprintf("%s_%d.html", strings.Trim(entity, "/"), statusCode))
fpath := filepath.Join(d.options.BodyOutputDir, fname)
err := os.WriteFile(fpath, body, 0o600)
if err != nil {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelError,
Message: fmt.Sprintf("Could not write body to file %s: %v", fpath, err),
}
}
}

if statusCode != 0 {
resultStatus := false

switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
resultStatus = true
resultStatus = d.regexBodyIsAMatch(body)
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(statusCode) {
resultStatus = true
resultStatus = d.regexBodyIsAMatch(body)
}
default:
return nil, errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
Expand Down Expand Up @@ -448,6 +487,18 @@ func (d *GobusterDir) GetConfigString() (string, error) {
}
}

if o.Regex != nil {
if o.RegexInvert {
if _, err := fmt.Fprintf(tw, "[+] Regex Inverted:\t%s\n", o.Regex.String()); err != nil {
return "", err
}
} else {
if _, err := fmt.Fprintf(tw, "[+] Regex:\t%s\n", o.Regex.String()); err != nil {
return "", err
}
}
}

if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
Expand Down
4 changes: 4 additions & 0 deletions gobusterdir/options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package gobusterdir

import (
"regexp"

"github.com/OJ/gobuster/v3/libgobuster"
)

Expand All @@ -22,6 +24,8 @@ type OptionsDir struct {
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
Force bool
Regex *regexp.Regexp
RegexInvert bool
}

// NewOptions returns a new initialized OptionsDir
Expand Down
Loading