1
1
Fork 0
mirror of https://github.com/OJ/gobuster.git synced 2024-05-06 11:16:05 +02:00

Dev Updates (#305)

* retry on timeout
* Google Cloud Bucket enumeration
* colors in output
* goreleaser
* fix nil reference errors
This commit is contained in:
Christian Mehlmauer 2022-10-08 18:41:25 +02:00 committed by GitHub
parent 9ea1da42f7
commit 0a0cab949f
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1652 additions and 1076 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: OJ github: [OJ, firefart]
patreon: OJReeves patreon: OJReeves
open_collective: gobuster open_collective: gobuster
ko_fi: OJReeves ko_fi: OJReeves

17
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every weekday
interval: "daily"

View File

@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go: ["1.17", "1.18", "1.19"] go: ["1.18", "1.19"]
steps: steps:
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v2 uses: actions/setup-go@v2
@ -28,8 +28,11 @@ jobs:
run: | run: |
go get -v -t -d ./... go get -v -t -d ./...
- name: Build - name: Build linux
run: go build -v . run: make linux
- name: Build windows
run: make windows
- name: Test - name: Test
run: make test run: make test

32
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: goreleaser
on:
push:
tags:
- "*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.19
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
distribution: goreleaser
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

38
.gitignore vendored
View File

@ -1,26 +1,19 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Binaries for programs and plugins
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe *.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
*.prof *.prof
*.txt *.txt
*.swp *.swp
@ -28,6 +21,5 @@ _testmain.go
.vscode/ .vscode/
gobuster gobuster
build build
v3
.idea/ dist/

3
.golangci.yml Normal file
View File

@ -0,0 +1,3 @@
linters:
enable:
- nonamedreturns

36
.goreleaser.yaml Normal file
View File

@ -0,0 +1,36 @@
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
before:
hooks:
# You may remove this if you don't use go modules.
- go mod tidy
# you may remove this if you don't need go generate
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ incpatch .Version }}-dev"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"

View File

@ -1,57 +1,28 @@
TARGET=./build .DEFAULT_GOAL := linux
ARCHS=amd64 386
LDFLAGS="-s -w"
.PHONY: current
current:
@go build -o ./gobuster; \
echo "Done."
.PHONY: fmt
fmt:
@go fmt ./...; \
echo "Done."
.PHONY: update
update:
@go get -u; \
go mod tidy -v; \
echo "Done."
.PHONY: windows
windows:
@for GOARCH in ${ARCHS}; do \
echo "Building for windows $${GOARCH} ..." ; \
mkdir -p ${TARGET}/gobuster-windows-$${GOARCH} ; \
GOOS=windows GOARCH=$${GOARCH} GO111MODULE=on CGO_ENABLED=0 go build -ldflags=${LDFLAGS} -trimpath -o ${TARGET}/gobuster-windows-$${GOARCH}/gobuster.exe ; \
done; \
echo "Done."
.PHONY: linux .PHONY: linux
linux: linux:
@for GOARCH in ${ARCHS}; do \ go build -o ./gobuster
echo "Building for linux $${GOARCH} ..." ; \
mkdir -p ${TARGET}/gobuster-linux-$${GOARCH} ; \
GOOS=linux GOARCH=$${GOARCH} GO111MODULE=on CGO_ENABLED=0 go build -ldflags=${LDFLAGS} -trimpath -o ${TARGET}/gobuster-linux-$${GOARCH}/gobuster ; \
done; \
echo "Done."
.PHONY: darwin .PHONY: windows
darwin: windows:
@for GOARCH in ${ARCHS}; do \ GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o ./gobuster.exe
echo "Building for darwin $${GOARCH} ..." ; \
mkdir -p ${TARGET}/gobuster-darwin-$${GOARCH} ; \ .PHONY: fmt
GOOS=darwin GOARCH=$${GOARCH} GO111MODULE=on CGO_ENABLED=0 go build -ldflags=${LDFLAGS} -trimpath -o ${TARGET}/gobuster-darwin-$${GOARCH}/gobuster ; \ fmt:
done; \ go fmt ./...
echo "Done."
.PHONY: update
update:
go get -u
go mod tidy -v
.PHONY: all .PHONY: all
all: clean fmt update test lint darwin linux windows all: fmt update linux windows test lint
.PHONY: test .PHONY: test
test: test:
@go test -v -race ./... ; \ go test -v -race ./...
echo "Done."
.PHONY: lint .PHONY: lint
lint: lint:
@ -62,14 +33,3 @@ lint:
lint-update: lint-update:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin
$$(go env GOPATH)/bin/golangci-lint --version $$(go env GOPATH)/bin/golangci-lint --version
.PHONY: lint-docker
lint-docker:
docker pull golangci/golangci-lint:latest
docker run --rm -v $$(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run
.PHONY: clean
clean:
@rm -rf ${TARGET}/* ; \
go clean ./... ; \
echo "Done."

786
README.md
View File

@ -1,4 +1,4 @@
# Gobuster v3.1.0 # Gobuster v3.2.0
Gobuster is a tool used to brute-force: Gobuster is a tool used to brute-force:
@ -9,28 +9,8 @@ Gobuster is a tool used to brute-force:
## Tags, Statuses, etc ## Tags, Statuses, etc
[![Build Status](https://travis-ci.com/OJ/gobuster.svg?branch=master)](https://travis-ci.com/OJ/gobuster) [![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)] [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)] [![Build Status](https://travis-ci.com/OJ/gobuster.svg?branch=master)](https://travis-ci.com/OJ/gobuster) [![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)
## Oh dear God.. WHY!?
Because I wanted:
1. ... something that didn't have a fat Java GUI (console FTW).
1. ... to build something that just worked on the command line.
1. ... something that did not do recursive brute force.
1. ... something that allowed me to brute force folders and multiple extensions at once.
1. ... something that compiled to native on multiple platforms.
1. ... something that was faster than an interpreted script (such as Python).
1. ... something that didn't require a runtime.
1. ... use something that was good with concurrency (hence Go).
1. ... to build something in Go that wasn't totally useless.
## But it's shit! And your implementation sucks!
Yes, you're probably correct. Feel free to:
- Not use it.
- Show me how to do it better.
## Love this tool? Back it! ## Love this tool? Back it!
@ -40,13 +20,19 @@ If you're backing us already, you rock. If you're not, that's cool too! Want to
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. 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.
## Changes in 3.1-dev # Changes
- Use go 1.16 ## 3.2-dev
- Use go 1.19
- use contexts in the correct way - use contexts in the correct way
- get rid of the wildcard flag (except in DNS mode) - get rid of the wildcard flag (except in DNS mode)
- color output
- retry on timeout
- google cloud bucket enumeration
- fix nil reference errors
## Changes in 3.1 ## 3.1
- enumerate public AWS S3 buckets - enumerate public AWS S3 buckets
- fuzzing mode - fuzzing mode
@ -54,118 +40,27 @@ All funds that are donated to this project will be donated to charity. A full lo
- 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. - 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 - The shorthand `p` flag which was assigned to proxy is now used by the pattern flag
## Changes in 3.0 ## 3.0
- New CLI options so modes are strictly separated (`-m` is now gone!) - New CLI options so modes are strictly separated (`-m` is now gone!)
- Performance Optimizations and better connection handling - Performance Optimizations and better connection handling
- Ability to enumerate vhost names - Ability to enumerate vhost names
- Option to supply custom HTTP headers - Option to supply custom HTTP headers
# License
See the LICENSE file.
# Manual
## Available Modes ## Available Modes
- dir - the classic directory brute-forcing mode - dir - the classic directory brute-forcing mode
- dns - DNS subdomain brute-forcing mode - dns - DNS subdomain brute-forcing mode
- s3 - Enumerate open S3 buckets and look for existence and bucket listings - s3 - Enumerate open S3 buckets and look for existence and bucket listings
- gcs - Enumerate open google cloud buckets
- vhost - virtual host brute-forcing mode (not the same as DNS!) - vhost - virtual host brute-forcing mode (not the same as DNS!)
- fuzz - some basic fuzzing, replaces the `FUZZ` keyword
## Built-in Help
Help is built-in!
- `gobuster help` - outputs the top-level help.
- `gobuster help <mode>` - outputs the help specific to that mode.
## `dns` Mode Help
```text
Usage:
gobuster dns [flags]
Flags:
-d, --domain string The target domain
-h, --help help for dns
-r, --resolver string Use custom DNS server (format server.com or server.com:port)
-c, --show-cname Show CNAME records (cannot be used with '-i' option)
-i, --show-ips Show IP addresses
--timeout duration DNS resolver timeout (default 1s)
--wildcard Force continued operation when wildcard found
Global Flags:
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
--delay duration Time each thread waits between requests (e.g. 1500ms)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
## `dir` Mode Options
```text
Usage:
gobuster dir [flags]
Flags:
-f, --add-slash Append / to each request
-c, --cookies string Cookies to use for the requests
-e, --expanded Expanded mode, print full URLs
-x, --extensions string File extension(s) to search for
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for dir
-l, --include-length Include the length of the body in the output
-k, --no-tls-validation Skip TLS certificate verification
-n, --no-status Don't print status codes
-P, --password string Password for Basic Auth
-p, --proxy string Proxy to use for requests [http(s)://host:port]
-s, --status-codes string Positive status codes (will be overwritten with status-codes-blacklist if set) (default "200,204,301,302,307,401,403")
-b, --status-codes-blacklist string Negative status codes (will override status-codes if set)
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.1.0")
-U, --username string Username for Basic Auth
-d, --discover-backup Upon finding a file search for backup files
--wildcard Force continued operation when wildcard found
Global Flags:
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
--delay duration Time each thread waits between requests (e.g. 1500ms)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
## `vhost` Mode Options
```text
Usage:
gobuster vhost [flags]
Flags:
-c, --cookies string Cookies to use for the requests
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for vhost
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
-p, --proxy string Proxy to use for requests [http(s)://host:port]
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.1.0")
-U, --username string Username for Basic Auth
Global Flags:
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
--delay duration Time each thread waits between requests (e.g. 1500ms)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
## Easy Installation ## Easy Installation
@ -177,17 +72,17 @@ If you're stupid enough to trust binaries that I've put together, you can downlo
### Using `go install` ### Using `go install`
If you have a [Go](https://golang.org/) environment ready to go (at least go 1.17), it's as easy as: If you have a [Go](https://golang.org/) environment ready to go (at least go 1.19), it's as easy as:
```bash ```bash
go install github.com/OJ/gobuster/v3@latest go install github.com/OJ/gobuster/v3@latest
``` ```
PS: You need at least go 1.17.0 to compile gobuster. PS: You need at least go 1.19 to compile gobuster.
## Building From Source ### Building From Source
Since this tool is written in [Go](https://golang.org/) you need to install the Go language/compiler/etc. Full details of installation and set up can be found [on the Go language website](https://golang.org/doc/install). Once installed you have two options. You need at least go 1.17.0 to compile gobuster. Since this tool is written in [Go](https://golang.org/) you need to install the Go language/compiler/etc. Full details of installation and set up can be found [on the Go language website](https://golang.org/doc/install). Once installed you have two options. You need at least go 1.19 to compile gobuster.
### Compiling ### Compiling
@ -203,196 +98,59 @@ This will create a `gobuster` binary for you. If you want to install it in the `
go install go install
``` ```
If you have all the dependencies already, you can make use of the build scripts: ## Modes
- `make` - builds for the current Go configuration (ie. runs `go build`). Help is built-in!
- `make windows` - builds 32 and 64 bit binaries for windows, and writes them to the `build` folder.
- `make linux` - builds 32 and 64 bit binaries for linux, and writes them to the `build` folder.
- `make darwin` - builds 32 and 64 bit binaries for darwin, and writes them to the `build` folder.
- `make all` - builds for all platforms and architectures, and writes the resulting binaries to the `build` folder.
- `make clean` - clears out the `build` folder.
- `make test` - runs the tests.
## Wordlists via STDIN - `gobuster help` - outputs the top-level help.
- `gobuster help <mode>` - outputs the help specific to that mode.
Wordlists can be piped into `gobuster` via stdin by providing a `-` to the `-w` option: ## `dns` Mode
```bash ### Options
hashcat -a 3 --stdout ?l | gobuster dir -u https://mysite.com -w -
```
Note: If the `-w` option is specified at the same time as piping from STDIN, an error will be shown and the program will terminate.
## Patterns
You can supply pattern files that will be applied to every word from the wordlist.
Just place the string `{GOBUSTER}` in it and this will be replaced with the word.
This feature is also handy in s3 mode to pre- or postfix certain patterns.
**Caution:** Using a big pattern file can cause a lot of request as every pattern is applied to every word in the wordlist.
### Example file
```text ```text
{GOBUSTER}Partial Uses DNS subdomain enumeration mode
{GOBUSTER}Service
PRE{GOBUSTER}POST Usage:
{GOBUSTER}-prod gobuster dns [flags]
{GOBUSTER}-dev
Flags:
-d, --domain string The target domain
-h, --help help for dns
-r, --resolver string Use custom DNS server (format server.com or server.com:port)
-c, --show-cname Show CNAME records (cannot be used with '-i' option)
-i, --show-ips Show IP addresses
--timeout duration DNS resolver timeout (default 1s)
--wildcard Force continued operation when wildcard found
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
``` ```
## Examples ### Examples
### `dir` Mode
Command line might look like this: ```text
```bash
gobuster dir -u https://mysite.com/path/to/folder -c 'session=123456' -t 50 -w common-files.txt -x .php,.html
```
Default options looks like this:
```bash
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.1.0
[+] Timeout : 10s
===============================================================
2019/06/21 11:49:43 Starting gobuster
===============================================================
/categories (Status: 301)
/contact (Status: 301)
/posts (Status: 301)
/index (Status: 200)
===============================================================
2019/06/21 11:49:44 Finished
===============================================================
```
Default options with status codes disabled looks like this:
```bash
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -n
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.1.0
[+] No status : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:18 Starting gobuster
===============================================================
/categories
/contact
/index
/posts
===============================================================
2019/06/21 11:50:18 Finished
===============================================================
```
Verbose output looks like this:
```bash
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -v
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.1.0
[+] Verbose : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:51 Starting gobuster
===============================================================
Missed: /alsodoesnotexist (Status: 404)
Found: /index (Status: 200)
Missed: /doesnotexist (Status: 404)
Found: /categories (Status: 301)
Found: /posts (Status: 301)
Found: /contact (Status: 301)
===============================================================
2019/06/21 11:50:51 Finished
===============================================================
```
Example showing content length:
```bash
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -l
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.1.0
[+] Show length : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:51:16 Starting gobuster
===============================================================
/categories (Status: 301) [Size: 178]
/posts (Status: 301) [Size: 178]
/contact (Status: 301) [Size: 178]
/index (Status: 200) [Size: 51759]
===============================================================
2019/06/21 11:51:17 Finished
===============================================================
```
Quiet output, with status disabled and expanded mode looks like this ("grep mode"):
```bash
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -q -n -e
https://buffered.io/index
https://buffered.io/contact
https://buffered.io/posts
https://buffered.io/categories
```
### `dns` Mode
Command line might look like this:
```bash
gobuster dns -d mysite.com -t 50 -w common-names.txt gobuster dns -d mysite.com -t 50 -w common-names.txt
``` ```
Normal sample run goes like this: Normal sample run goes like this:
```bash ```text
gobuster dns -d google.com -w ~/wordlists/subdomains.txt gobuster dns -d google.com -w ~/wordlists/subdomains.txt
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Mode : dns [+] Mode : dns
@ -427,11 +185,11 @@ Found: blog.google.com
Show IP sample run goes like this: Show IP sample run goes like this:
```bash ```text
gobuster dns -d google.com -w ~/wordlists/subdomains.txt -i gobuster dns -d google.com -w ~/wordlists/subdomains.txt -i
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Mode : dns [+] Mode : dns
@ -466,11 +224,11 @@ Found: mail.google.com [172.217.25.37, 2404:6800:4006:802::2005]
Base domain validation warning when the base domain fails to resolve. This is a warning rather than a failure in case the user fat-fingers while typing the domain. Base domain validation warning when the base domain fails to resolve. This is a warning rather than a failure in case the user fat-fingers while typing the domain.
```bash ```text
gobuster dns -d yp.to -w ~/wordlists/subdomains.txt -i gobuster dns -d yp.to -w ~/wordlists/subdomains.txt -i
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Mode : dns [+] Mode : dns
@ -489,11 +247,11 @@ Found: cr.yp.to [131.193.32.108, 131.193.32.109]
Wildcard DNS is also detected properly: Wildcard DNS is also detected properly:
```bash ```text
gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Mode : dns [+] Mode : dns
@ -512,11 +270,11 @@ by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
If the user wants to force processing of a domain that has wildcard entries, use `--wildcard`: If the user wants to force processing of a domain that has wildcard entries, use `--wildcard`:
```bash ```text
gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt --wildcard gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt --wildcard
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Mode : dns [+] Mode : dns
@ -534,27 +292,251 @@ Found: test.127.0.0.1.xip.io
=============================================================== ===============================================================
``` ```
### `vhost` Mode ## `dir` Mode
Command line might look like this: ### Options
```bash ```text
Uses directory/file enumeration mode
Usage:
gobuster dir [flags]
Flags:
-f, --add-slash Append / to each request
-c, --cookies string Cookies to use for the requests
-d, --discover-backup Also search for backup files by appending multiple backup extensions
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-e, --expanded Expanded mode, print full URLs
-x, --extensions string File extension(s) to search for
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for dir
--hide-length Hide the length of the body in the output
-m, --method string Use the following HTTP method (default "GET")
-n, --no-status Don't print status codes
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
-s, --status-codes string Positive status codes (will be overwritten with status-codes-blacklist if set)
-b, --status-codes-blacklist string Negative status codes (will override status-codes if set) (default "404")
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster dir -u https://mysite.com/path/to/folder -c 'session=123456' -t 50 -w common-files.txt -x .php,.html
```
Default options looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Timeout : 10s
===============================================================
2019/06/21 11:49:43 Starting gobuster
===============================================================
/categories (Status: 301)
/contact (Status: 301)
/posts (Status: 301)
/index (Status: 200)
===============================================================
2019/06/21 11:49:44 Finished
===============================================================
```
Default options with status codes disabled looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -n
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] No status : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:18 Starting gobuster
===============================================================
/categories
/contact
/index
/posts
===============================================================
2019/06/21 11:50:18 Finished
===============================================================
```
Verbose output looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -v
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Verbose : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:51 Starting gobuster
===============================================================
Missed: /alsodoesnotexist (Status: 404)
Found: /index (Status: 200)
Missed: /doesnotexist (Status: 404)
Found: /categories (Status: 301)
Found: /posts (Status: 301)
Found: /contact (Status: 301)
===============================================================
2019/06/21 11:50:51 Finished
===============================================================
```
Example showing content length:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -l
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Show length : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:51:16 Starting gobuster
===============================================================
/categories (Status: 301) [Size: 178]
/posts (Status: 301) [Size: 178]
/contact (Status: 301) [Size: 178]
/index (Status: 200) [Size: 51759]
===============================================================
2019/06/21 11:51:17 Finished
===============================================================
```
Quiet output, with status disabled and expanded mode looks like this ("grep mode"):
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -q -n -e
https://buffered.io/index
https://buffered.io/contact
https://buffered.io/posts
https://buffered.io/categories
```
## `vhost` Mode
### Options
```text
Uses VHOST enumeration mode (you most probably want to use the IP adress as the URL parameter
Usage:
gobuster vhost [flags]
Flags:
--append-domain Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.
-c, --cookies string Cookies to use for the requests
--domain string the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for vhost
-m, --method string Use the following HTTP method (default "GET")
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster vhost -u https://mysite.com -w common-vhosts.txt gobuster vhost -u https://mysite.com -w common-vhosts.txt
``` ```
Normal sample run goes like this: Normal sample run goes like this:
```bash ```text
gobuster vhost -u https://mysite.com -w common-vhosts.txt gobuster vhost -u https://mysite.com -w common-vhosts.txt
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Url: https://mysite.com [+] Url: https://mysite.com
[+] Threads: 10 [+] Threads: 10
[+] Wordlist: common-vhosts.txt [+] Wordlist: common-vhosts.txt
[+] User Agent: gobuster/3.1.0 [+] User Agent: gobuster/3.2.0
[+] Timeout: 10s [+] Timeout: 10s
=============================================================== ===============================================================
2019/06/21 08:36:00 Starting gobuster 2019/06/21 08:36:00 Starting gobuster
@ -567,20 +549,160 @@ Found: mail.mysite.com
=============================================================== ===============================================================
``` ```
### `s3` Mode ## `fuzz` Mode
Command line might look like this: ### Options
```bash ```text
Uses fuzzing mode
Usage:
gobuster fuzz [flags]
Flags:
-c, --cookies string Cookies to use for the requests
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-b, --excludestatuscodes string Negative status codes (will override statuscodes if set)
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for fuzz
-m, --method string Use the following HTTP method (default "GET")
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster fuzz -u https://example.com?FUZZ=test -w parameter-names.txt
```
## `s3` Mode
### Options
```text
Uses aws bucket enumeration mode
Usage:
gobuster s3 [flags]
Flags:
-h, --help help for s3
-m, --maxfiles int max files to list when listing buckets (only shown in verbose mode) (default 5)
-k, --no-tls-validation Skip TLS certificate verification
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster s3 -w bucket-names.txt gobuster s3 -w bucket-names.txt
``` ```
### `fuzzing` Mode ## `gcs` Mode
Command line might look like this: ### Options
```text
Uses gcs bucket enumeration mode
Usage:
gobuster gcs [flags]
Flags:
-h, --help help for gcs
-m, --maxfiles int max files to list when listing buckets (only shown in verbose mode) (default 5)
-k, --no-tls-validation Skip TLS certificate verification
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster gcs -w bucket-names.txt
```
## Wordlists via STDIN
Wordlists can be piped into `gobuster` via stdin by providing a `-` to the `-w` option:
```bash ```bash
gobuster fuzz -u https://example.com?FUZZ=test -w parameter-names.txt hashcat -a 3 --stdout ?l | gobuster dir -u https://mysite.com -w -
```
Note: If the `-w` option is specified at the same time as piping from STDIN, an error will be shown and the program will terminate.
## Patterns
You can supply pattern files that will be applied to every word from the wordlist.
Just place the string `{GOBUSTER}` in it and this will be replaced with the word.
This feature is also handy in s3 mode to pre- or postfix certain patterns.
**Caution:** Using a big pattern file can cause a lot of request as every pattern is applied to every word in the wordlist.
### Example file
```text
{GOBUSTER}Partial
{GOBUSTER}Service
PRE{GOBUSTER}POST
{GOBUSTER}-prod
{GOBUSTER}-dev
``` ```
#### Use case in combination with patterns #### Use case in combination with patterns
@ -594,7 +716,7 @@ curl -s --output - https://raw.githubusercontent.com/eth0izzle/bucket-stream/mas
- Run gobuster with the custom input. Be sure to turn verbose mode on to see the bucket details - Run gobuster with the custom input. Be sure to turn verbose mode on to see the bucket details
```bash ```text
gobuster s3 --wordlist my.custom.wordlist -p patterns.txt -v gobuster s3 --wordlist my.custom.wordlist -p patterns.txt -v
``` ```
@ -603,12 +725,12 @@ Normal sample run goes like this:
```text ```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Threads: 10 [+] Threads: 10
[+] Wordlist: .\wordlist.txt [+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.1.0 [+] User Agent: gobuster/3.2.0
[+] Timeout: 10s [+] Timeout: 10s
[+] Maximum files to list: 5 [+] Maximum files to list: 5
=============================================================== ===============================================================
@ -632,12 +754,12 @@ Verbose and sample run
```text ```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -v PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -v
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Threads: 10 [+] Threads: 10
[+] Wordlist: .\wordlist.txt [+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.1.0 [+] User Agent: gobuster/3.2.0
[+] Verbose: true [+] Verbose: true
[+] Timeout: 10s [+] Timeout: 10s
[+] Maximum files to list: 5 [+] Maximum files to list: 5
@ -662,12 +784,12 @@ Extended sample run
```text ```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -e PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -e
=============================================================== ===============================================================
Gobuster v3.1.0 Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart) by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
=============================================================== ===============================================================
[+] Threads: 10 [+] Threads: 10
[+] Wordlist: .\wordlist.txt [+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.1.0 [+] User Agent: gobuster/3.2.0
[+] Timeout: 10s [+] Timeout: 10s
[+] Expanded: true [+] Expanded: true
[+] Maximum files to list: 5 [+] Maximum files to list: 5
@ -686,11 +808,3 @@ http://localhost.s3.amazonaws.com/
2019/08/12 21:48:38 Finished 2019/08/12 21:48:38 Finished
=============================================================== ===============================================================
``` ```
## License
See the LICENSE file.
## Thanks
See the THANKS file for people who helped out.

View File

@ -1 +0,0 @@
# TODO

View File

@ -59,6 +59,8 @@ func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) {
plugin.NoTLSValidation = httpOpts.NoTLSValidation plugin.NoTLSValidation = httpOpts.NoTLSValidation
plugin.Headers = httpOpts.Headers plugin.Headers = httpOpts.Headers
plugin.Method = httpOpts.Method plugin.Method = httpOpts.Method
plugin.RetryOnTimeout = httpOpts.RetryOnTimeout
plugin.RetryAttempts = httpOpts.RetryAttempts
plugin.Extensions, err = cmdDir.Flags().GetString("extensions") plugin.Extensions, err = cmdDir.Flags().GetString("extensions")
if err != nil { if err != nil {
@ -93,7 +95,8 @@ func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) {
plugin.StatusCodesBlacklistParsed = ret3 plugin.StatusCodesBlacklistParsed = ret3
if plugin.StatusCodes != "" && plugin.StatusCodesBlacklist != "" { if plugin.StatusCodes != "" && plugin.StatusCodesBlacklist != "" {
return nil, nil, fmt.Errorf("status-codes and status-codes-blacklist are both set, please set only one") return nil, nil, fmt.Errorf("status-codes (%q) and status-codes-blacklist (%q) are both set - please set only one. status-codes-blacklist is set by default so you might want to disable it by supplying an empty string.",
plugin.StatusCodes, plugin.StatusCodesBlacklist)
} }
if plugin.StatusCodes == "" && plugin.StatusCodesBlacklist == "" { if plugin.StatusCodes == "" && plugin.StatusCodesBlacklist == "" {
@ -151,7 +154,7 @@ func init() {
cmdDir.Flags().BoolP("no-status", "n", false, "Don't print status codes") cmdDir.Flags().BoolP("no-status", "n", false, "Don't print status codes")
cmdDir.Flags().Bool("hide-length", false, "Hide the length of the body in the output") cmdDir.Flags().Bool("hide-length", false, "Hide the length of the body in the output")
cmdDir.Flags().BoolP("add-slash", "f", false, "Append / to each request") cmdDir.Flags().BoolP("add-slash", "f", false, "Append / to each request")
cmdDir.Flags().BoolP("discover-backup", "d", false, "Upon finding a file search for backup files") cmdDir.Flags().BoolP("discover-backup", "d", false, "Also search for backup files by appending multiple backup extensions")
cmdDir.Flags().IntSlice("exclude-length", []int{}, "exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.") cmdDir.Flags().IntSlice("exclude-length", []int{}, "exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.")
cmdDir.PersistentPreRun = func(cmd *cobra.Command, args []string) { cmdDir.PersistentPreRun = func(cmd *cobra.Command, args []string) {

View File

@ -59,6 +59,8 @@ func parseFuzzOptions() (*libgobuster.Options, *gobusterfuzz.OptionsFuzz, error)
plugin.NoTLSValidation = httpOpts.NoTLSValidation plugin.NoTLSValidation = httpOpts.NoTLSValidation
plugin.Headers = httpOpts.Headers plugin.Headers = httpOpts.Headers
plugin.Method = httpOpts.Method plugin.Method = httpOpts.Method
plugin.RetryOnTimeout = httpOpts.RetryOnTimeout
plugin.RetryAttempts = httpOpts.RetryAttempts
// blacklist will override the normal status codes // blacklist will override the normal status codes
plugin.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes") plugin.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes")

76
cli/cmd/gcs.go Normal file
View File

@ -0,0 +1,76 @@
package cmd
import (
"fmt"
"github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobustergcs"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdGCS *cobra.Command
func runGCS(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseGCSOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobustergcs.NewGobusterGCS(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobustergcs: %w", err)
}
if err := cli.Gobuster(mainContext, globalopts, plugin); err != nil {
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseGCSOptions() (*libgobuster.Options, *gobustergcs.OptionsGCS, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginopts := gobustergcs.NewOptionsGCS()
httpOpts, err := parseBasicHTTPOptions(cmdGCS)
if err != nil {
return nil, nil, err
}
pluginopts.UserAgent = httpOpts.UserAgent
pluginopts.Proxy = httpOpts.Proxy
pluginopts.Timeout = httpOpts.Timeout
pluginopts.NoTLSValidation = httpOpts.NoTLSValidation
pluginopts.RetryOnTimeout = httpOpts.RetryOnTimeout
pluginopts.RetryAttempts = httpOpts.RetryAttempts
pluginopts.MaxFilesToList, err = cmdGCS.Flags().GetInt("maxfiles")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err)
}
return globalopts, pluginopts, nil
}
// nolint:gochecknoinits
func init() {
cmdGCS = &cobra.Command{
Use: "gcs",
Short: "Uses gcs bucket enumeration mode",
RunE: runGCS,
}
addBasicHTTPOptions(cmdGCS)
cmdGCS.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets (only shown in verbose mode)")
cmdGCS.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdGCS)
}

View File

@ -20,6 +20,8 @@ func addBasicHTTPOptions(cmd *cobra.Command) {
cmd.Flags().StringP("proxy", "", "", "Proxy to use for requests [http(s)://host:port]") cmd.Flags().StringP("proxy", "", "", "Proxy to use for requests [http(s)://host:port]")
cmd.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout") cmd.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout")
cmd.Flags().BoolP("no-tls-validation", "k", false, "Skip TLS certificate verification") cmd.Flags().BoolP("no-tls-validation", "k", false, "Skip TLS certificate verification")
cmd.Flags().BoolP("retry", "", false, "Should retry on request timeout")
cmd.Flags().IntP("retry-attempts", "", 3, "Times to retry on request timeout")
} }
func addCommonHTTPOptions(cmd *cobra.Command) error { func addCommonHTTPOptions(cmd *cobra.Command) error {
@ -69,6 +71,16 @@ func parseBasicHTTPOptions(cmd *cobra.Command) (libgobuster.BasicHTTPOptions, er
return options, fmt.Errorf("invalid value for timeout: %w", err) return options, fmt.Errorf("invalid value for timeout: %w", err)
} }
options.RetryOnTimeout, err = cmd.Flags().GetBool("retry")
if err != nil {
return options, fmt.Errorf("invalid value for retry: %w", err)
}
options.RetryAttempts, err = cmd.Flags().GetInt("retry-attempts")
if err != nil {
return options, fmt.Errorf("invalid value for retry-attempts: %w", err)
}
options.NoTLSValidation, err = cmd.Flags().GetBool("no-tls-validation") options.NoTLSValidation, err = cmd.Flags().GetBool("no-tls-validation")
if err != nil { if err != nil {
return options, fmt.Errorf("invalid value for no-tls-validation: %w", err) return options, fmt.Errorf("invalid value for no-tls-validation: %w", err)
@ -88,6 +100,8 @@ func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error)
options.Timeout = basic.Timeout options.Timeout = basic.Timeout
options.UserAgent = basic.UserAgent options.UserAgent = basic.UserAgent
options.NoTLSValidation = basic.NoTLSValidation options.NoTLSValidation = basic.NoTLSValidation
options.RetryOnTimeout = basic.RetryOnTimeout
options.RetryAttempts = basic.RetryAttempts
options.URL, err = cmd.Flags().GetString("url") options.URL, err = cmd.Flags().GetString("url")
if err != nil { if err != nil {

View File

@ -9,6 +9,7 @@ import (
"os/signal" "os/signal"
"github.com/OJ/gobuster/v3/libgobuster" "github.com/OJ/gobuster/v3/libgobuster"
"github.com/fatih/color"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -136,6 +137,14 @@ func parseGlobalOptions() (*libgobuster.Options, error) {
return nil, fmt.Errorf("invalid value for no-error: %w", err) return nil, fmt.Errorf("invalid value for no-error: %w", err)
} }
noColor, err := rootCmd.Flags().GetBool("no-color")
if err != nil {
return nil, fmt.Errorf("invalid value for no-color: %w", err)
}
if noColor {
color.NoColor = true
}
return globalopts, nil return globalopts, nil
} }
@ -160,4 +169,5 @@ func init() {
rootCmd.PersistentFlags().BoolP("no-progress", "z", false, "Don't display progress") rootCmd.PersistentFlags().BoolP("no-progress", "z", false, "Don't display progress")
rootCmd.PersistentFlags().Bool("no-error", false, "Don't display errors") rootCmd.PersistentFlags().Bool("no-error", false, "Don't display errors")
rootCmd.PersistentFlags().StringP("pattern", "p", "", "File containing replacement patterns") rootCmd.PersistentFlags().StringP("pattern", "p", "", "File containing replacement patterns")
rootCmd.PersistentFlags().Bool("no-color", false, "Disable color output")
} }

View File

@ -46,6 +46,8 @@ func parseS3Options() (*libgobuster.Options, *gobusters3.OptionsS3, error) {
plugin.Proxy = httpOpts.Proxy plugin.Proxy = httpOpts.Proxy
plugin.Timeout = httpOpts.Timeout plugin.Timeout = httpOpts.Timeout
plugin.NoTLSValidation = httpOpts.NoTLSValidation plugin.NoTLSValidation = httpOpts.NoTLSValidation
plugin.RetryOnTimeout = httpOpts.RetryOnTimeout
plugin.RetryAttempts = httpOpts.RetryAttempts
plugin.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles") plugin.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles")
if err != nil { if err != nil {

View File

@ -52,6 +52,8 @@ func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, err
plugin.NoTLSValidation = httpOpts.NoTLSValidation plugin.NoTLSValidation = httpOpts.NoTLSValidation
plugin.Headers = httpOpts.Headers plugin.Headers = httpOpts.Headers
plugin.Method = httpOpts.Method plugin.Method = httpOpts.Method
plugin.RetryOnTimeout = httpOpts.RetryOnTimeout
plugin.RetryAttempts = httpOpts.RetryAttempts
plugin.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain") plugin.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain")
if err != nil { if err != nil {

7
cli/const.go Normal file
View File

@ -0,0 +1,7 @@
//go:build !windows
package cli
const (
TERMINAL_CLEAR_LINE = "\r\x1b[2K"
)

7
cli/const_windows.go Normal file
View File

@ -0,0 +1,7 @@
//go:build windows
package cli
const (
TERMINAL_CLEAR_LINE = "\r\r"
)

View File

@ -19,27 +19,9 @@ func banner() {
fmt.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)") fmt.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)")
} }
type outputType struct {
Mu *sync.RWMutex
MaxCharsWritten int
}
// right pad a string
// nolint:unparam
func rightPad(s string, padStr string, overallLen int) string {
strLen := len(s)
if overallLen <= strLen {
return s
}
toPad := overallLen - strLen - 1
pad := strings.Repeat(padStr, toPad)
return fmt.Sprintf("%s%s", s, pad)
}
// resultWorker outputs the results as they come in. This needs to be a range and should not handle // resultWorker outputs the results as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block. // the context so the channel always has a receiver and libgobuster will not block.
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup, output *outputType) { func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
var f *os.File var f *os.File
@ -52,20 +34,14 @@ func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup,
defer f.Close() defer f.Close()
} }
for r := range g.Results() { for r := range g.Progress.ResultChan {
s, err := r.ResultToString() s, err := r.ResultToString()
if err != nil { if err != nil {
g.LogError.Fatal(err) g.LogError.Fatal(err)
} }
if s != "" { if s != "" {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
output.Mu.Lock() _, _ = fmt.Printf("%s%s\n", TERMINAL_CLEAR_LINE, s)
w, _ := fmt.Printf("\r%s\n", rightPad(s, " ", output.MaxCharsWritten))
// -1 to remove the newline, otherwise it's always bigger
if (w - 1) > output.MaxCharsWritten {
output.MaxCharsWritten = w - 1
}
output.Mu.Unlock()
if f != nil { if f != nil {
err = writeToFile(f, s) err = writeToFile(f, s)
if err != nil { if err != nil {
@ -78,21 +54,19 @@ func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup,
// errorWorker outputs the errors as they come in. This needs to be a range and should not handle // errorWorker outputs the errors as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block. // the context so the channel always has a receiver and libgobuster will not block.
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup, output *outputType) { func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
for e := range g.Errors() { for e := range g.Progress.ErrorChan {
if !g.Opts.Quiet && !g.Opts.NoError { if !g.Opts.Quiet && !g.Opts.NoError {
output.Mu.Lock() g.LogError.Printf("[!] %s\n", e.Error())
g.LogError.Printf("[!] %v", e)
output.Mu.Unlock()
} }
} }
} }
// progressWorker outputs the progress every tick. It will stop once cancel() is called // progressWorker outputs the progress every tick. It will stop once cancel() is called
// on the context // on the context
func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup, output *outputType) { func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
tick := time.NewTicker(cliProgressUpdate) tick := time.NewTicker(cliProgressUpdate)
@ -101,25 +75,16 @@ func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitG
select { select {
case <-tick.C: case <-tick.C:
if !g.Opts.Quiet && !g.Opts.NoProgress { if !g.Opts.Quiet && !g.Opts.NoProgress {
g.RequestsCountMutex.RLock() requestsIssued := g.Progress.RequestsIssued()
output.Mu.Lock() requestsExpected := g.Progress.RequestsExpected()
var charsWritten int
if g.Opts.Wordlist == "-" { if g.Opts.Wordlist == "-" {
s := fmt.Sprintf("\rProgress: %d", g.RequestsIssued) s := fmt.Sprintf("%sProgress: %d", TERMINAL_CLEAR_LINE, requestsIssued)
s = rightPad(s, " ", output.MaxCharsWritten) _, _ = fmt.Fprint(os.Stderr, s)
charsWritten, _ = fmt.Fprint(os.Stderr, s)
// only print status if we already read in the wordlist // only print status if we already read in the wordlist
} else if g.RequestsExpected > 0 { } else if requestsExpected > 0 {
s := fmt.Sprintf("\rProgress: %d / %d (%3.2f%%)", g.RequestsIssued, g.RequestsExpected, float32(g.RequestsIssued)*100.0/float32(g.RequestsExpected)) s := fmt.Sprintf("%sProgress: %d / %d (%3.2f%%)", TERMINAL_CLEAR_LINE, requestsIssued, requestsExpected, float32(requestsIssued)*100.0/float32(requestsExpected))
s = rightPad(s, " ", output.MaxCharsWritten) _, _ = fmt.Fprint(os.Stderr, s)
charsWritten, _ = fmt.Fprint(os.Stderr, s)
} }
if charsWritten > output.MaxCharsWritten {
output.MaxCharsWritten = charsWritten
}
output.Mu.Unlock()
g.RequestsCountMutex.RUnlock()
} }
case <-ctx.Done(): case <-ctx.Done():
return return
@ -173,22 +138,16 @@ func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster
// when we call wg.Wait() // when we call wg.Wait()
var wg sync.WaitGroup var wg sync.WaitGroup
outputMutex := new(sync.RWMutex) wg.Add(1)
o := &outputType{ go resultWorker(gobuster, opts.OutputFilename, &wg)
Mu: outputMutex,
MaxCharsWritten: 0,
}
wg.Add(1) wg.Add(1)
go resultWorker(gobuster, opts.OutputFilename, &wg, o) go errorWorker(gobuster, &wg)
wg.Add(1)
go errorWorker(gobuster, &wg, o)
if !opts.Quiet && !opts.NoProgress { if !opts.Quiet && !opts.NoProgress {
// if not quiet add a new workgroup entry and start the goroutine // if not quiet add a new workgroup entry and start the goroutine
wg.Add(1) wg.Add(1)
go progressWorker(ctxCancel, gobuster, &wg, o) go progressWorker(ctxCancel, gobuster, &wg)
} }
err = gobuster.Run(ctxCancel) err = gobuster.Run(ctxCancel)
@ -205,8 +164,6 @@ func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster
} }
if !opts.Quiet { if !opts.Quiet {
// clear stderr progress
fmt.Fprintf(os.Stderr, "\r%s\n", rightPad("", " ", o.MaxCharsWritten))
fmt.Println(ruler) fmt.Println(ruler)
gobuster.LogInfo.Println("Finished") gobuster.LogInfo.Println("Finished")
fmt.Println(ruler) fmt.Println(ruler)

View File

@ -1,7 +1,7 @@
// cSpell Settings // cSpell Settings
{ {
// Version of the setting file. Always 0.1 // Version of the setting file. Always 0.2
"version": "0.1", "version": "0.2",
// language - current active spelling language // language - current active spelling language
"language": "en", "language": "en",
// words - list of words to be always considered correct // words - list of words to be always considered correct
@ -13,6 +13,7 @@
"gobusterdns", "gobusterdns",
"gobusterfuzz", "gobusterfuzz",
"gobustervhost", "gobustervhost",
"gobustergcs",
"vhost", "vhost",
"vhosts", "vhosts",
"cname", "cname",
@ -28,7 +29,10 @@
"unconvert", "unconvert",
"unparam", "unparam",
"prealloc", "prealloc",
"gochecknoglobals" "gochecknoglobals",
"gochecknoinits",
"fatih",
"netip"
], ],
// flagWords - list of words to be always considered incorrect // flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors. // This is useful for offensive words and common spelling errors.

11
go.mod
View File

@ -1,15 +1,18 @@
module github.com/OJ/gobuster/v3 module github.com/OJ/gobuster/v3
go 1.19
require ( require (
github.com/fatih/color v1.13.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 golang.org/x/term v0.0.0-20220919170432-7a66f970e087
) )
require ( require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 // indirect golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
) )
go 1.17

22
go.sum
View File

@ -1,18 +1,30 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2 h1:wM1k/lXfpc5HdkJJyW9GELpd8ERGdnh8sMGL6Gzq3Ho= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220909162455-aba9fc2a8ff2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 h1:tPwmk4vmvVCMdr98VgL4JH+qZxPL8fqlUOHnyOM8N3w=
golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -5,6 +5,8 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"net"
"net/http"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
@ -33,10 +35,9 @@ func (e *ErrWildcard) Error() string {
// GobusterDir is the main type to implement the interface // GobusterDir is the main type to implement the interface
type GobusterDir struct { type GobusterDir struct {
options *OptionsDir options *OptionsDir
globalopts *libgobuster.Options globalopts *libgobuster.Options
http *libgobuster.HTTPClient http *libgobuster.HTTPClient
requestsPerRun *int // helper variable so we do not recalculate this over and over
} }
// NewGobusterDir creates a new initialized GobusterDir // NewGobusterDir creates a new initialized GobusterDir
@ -59,6 +60,8 @@ func NewGobusterDir(globalopts *libgobuster.Options, opts *OptionsDir) (*Gobuste
Timeout: opts.Timeout, Timeout: opts.Timeout,
UserAgent: opts.UserAgent, UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation, NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
} }
httpOpts := libgobuster.HTTPOptions{ httpOpts := libgobuster.HTTPOptions{
@ -85,26 +88,6 @@ func (d *GobusterDir) Name() string {
return "directory enumeration" return "directory enumeration"
} }
// RequestsPerRun returns the number of requests this plugin makes per single wordlist item
func (d *GobusterDir) RequestsPerRun() int {
if d.requestsPerRun != nil {
return *d.requestsPerRun
}
num := 1 + len(d.options.ExtensionsParsed.Set)
if d.options.DiscoverBackup {
// default word
num += len(backupExtensions)
num += len(backupDotExtensions)
// backups of filenames
num += len(d.options.ExtensionsParsed.Set) * len(backupExtensions)
num += len(d.options.ExtensionsParsed.Set) * len(backupDotExtensions)
}
d.requestsPerRun = &num
return *d.requestsPerRun
}
// PreRun is the pre run implementation of gobusterdir // PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context) error { func (d *GobusterDir) PreRun(ctx context.Context) error {
// add trailing slash // add trailing slash
@ -134,12 +117,12 @@ func (d *GobusterDir) PreRun(ctx context.Context) error {
} }
if d.options.StatusCodesBlacklistParsed.Length() > 0 { if d.options.StatusCodesBlacklistParsed.Length() > 0 {
if !d.options.StatusCodesBlacklistParsed.Contains(*wildcardResp) { if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
return &ErrWildcard{url: url, statusCode: *wildcardResp, length: wildcardLength} return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
} }
} else if d.options.StatusCodesParsed.Length() > 0 { } else if d.options.StatusCodesParsed.Length() > 0 {
if d.options.StatusCodesParsed.Contains(*wildcardResp) { if d.options.StatusCodesParsed.Contains(wildcardResp) {
return &ErrWildcard{url: url, statusCode: *wildcardResp, length: wildcardLength} return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
} }
} else { } else {
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen") return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
@ -163,72 +146,90 @@ func getBackupFilenames(word string) []string {
return ret return ret
} }
// Run is the process implementation of gobusterdir func (d *GobusterDir) AdditionalWords(word string) []string {
func (d *GobusterDir) Run(ctx context.Context, word string, resChannel chan<- libgobuster.Result) error { var words []string
suffix := ""
if d.options.UseSlash {
suffix = "/"
}
// build list of urls to check // build list of urls to check
// 1: No extension // 1: No extension
// 2: With extension // 2: With extension
// 3: backupextension // 3: backupextension
urlsToCheck := make(map[string]string)
entity := fmt.Sprintf("%s%s", word, suffix)
dirURL := fmt.Sprintf("%s%s", d.options.URL, entity)
urlsToCheck[entity] = dirURL
if d.options.DiscoverBackup { if d.options.DiscoverBackup {
for _, u := range getBackupFilenames(word) { words = append(words, getBackupFilenames(word)...)
url := fmt.Sprintf("%s%s", d.options.URL, u)
urlsToCheck[u] = url
}
} }
for ext := range d.options.ExtensionsParsed.Set { for ext := range d.options.ExtensionsParsed.Set {
filename := fmt.Sprintf("%s.%s", word, ext) filename := fmt.Sprintf("%s.%s", word, ext)
url := fmt.Sprintf("%s%s", d.options.URL, filename) words = append(words, filename)
urlsToCheck[filename] = url
if d.options.DiscoverBackup { if d.options.DiscoverBackup {
for _, u := range getBackupFilenames(filename) { words = append(words, getBackupFilenames(filename)...)
url2 := fmt.Sprintf("%s%s", d.options.URL, u)
urlsToCheck[u] = url2
}
} }
} }
return words
}
for entity, url := range urlsToCheck { // ProcessWord is the process implementation of gobusterdir
statusCode, size, header, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{}) 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)
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 { if err != nil {
return err // check if it's a timeout and if we should try again and try again
} // otherwise the timeout error is raised
if statusCode != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
resultStatus := false continue
} else if strings.Contains(err.Error(), "invalid control character in URL") {
if d.options.StatusCodesBlacklistParsed.Length() > 0 { // put error in error chan so it's printed out and ignore it
if !d.options.StatusCodesBlacklistParsed.Contains(*statusCode) { // so gobuster will not quit
resultStatus = true progress.ErrorChan <- err
} continue
} else if d.options.StatusCodesParsed.Length() > 0 {
if d.options.StatusCodesParsed.Contains(*statusCode) {
resultStatus = true
}
} else { } else {
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen") return err
} }
}
break
}
if (resultStatus && !helper.SliceContains(d.options.ExcludeLength, int(size))) || d.globalopts.Verbose { if statusCode != 0 {
resChannel <- Result{ resultStatus := false
URL: d.options.URL,
Path: entity, if d.options.StatusCodesBlacklistParsed.Length() > 0 {
Verbose: d.globalopts.Verbose, if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
Expanded: d.options.Expanded, resultStatus = true
NoStatus: d.options.NoStatus, }
HideLength: d.options.HideLength, } else if d.options.StatusCodesParsed.Length() > 0 {
Found: resultStatus, if d.options.StatusCodesParsed.Contains(statusCode) {
Header: header, resultStatus = true
StatusCode: *statusCode, }
Size: size, } else {
} return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
}
if (resultStatus && !helper.SliceContains(d.options.ExcludeLength, 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,
} }
} }
} }

View File

@ -8,11 +8,11 @@ import (
type OptionsDir struct { type OptionsDir struct {
libgobuster.HTTPOptions libgobuster.HTTPOptions
Extensions string Extensions string
ExtensionsParsed libgobuster.StringSet ExtensionsParsed libgobuster.Set[string]
StatusCodes string StatusCodes string
StatusCodesParsed libgobuster.IntSet StatusCodesParsed libgobuster.Set[int]
StatusCodesBlacklist string StatusCodesBlacklist string
StatusCodesBlacklistParsed libgobuster.IntSet StatusCodesBlacklistParsed libgobuster.Set[int]
UseSlash bool UseSlash bool
HideLength bool HideLength bool
Expanded bool Expanded bool
@ -24,8 +24,8 @@ type OptionsDir struct {
// NewOptionsDir returns a new initialized OptionsDir // NewOptionsDir returns a new initialized OptionsDir
func NewOptionsDir() *OptionsDir { func NewOptionsDir() *OptionsDir {
return &OptionsDir{ return &OptionsDir{
StatusCodesParsed: libgobuster.NewIntSet(), StatusCodesParsed: libgobuster.NewSet[int](),
StatusCodesBlacklistParsed: libgobuster.NewIntSet(), StatusCodesBlacklistParsed: libgobuster.NewSet[int](),
ExtensionsParsed: libgobuster.NewStringSet(), ExtensionsParsed: libgobuster.NewSet[string](),
} }
} }

View File

@ -4,6 +4,17 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"net/http" "net/http"
"github.com/fatih/color"
)
var (
white = color.New(color.FgWhite).FprintfFunc()
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
blue = color.New(color.FgBlue).FprintfFunc()
red = color.New(color.FgRed).FprintfFunc()
cyan = color.New(color.FgCyan).FprintfFunc()
) )
// Result represents a single result // Result represents a single result
@ -51,9 +62,18 @@ func (r Result) ResultToString() (string, error) {
} }
if !r.NoStatus { if !r.NoStatus {
if _, err := fmt.Fprintf(buf, " (Status: %d)", r.StatusCode); err != nil { color := white
return "", err if r.StatusCode == 200 {
color = green
} else if r.StatusCode >= 300 && r.StatusCode < 400 {
color = cyan
} else if r.StatusCode >= 400 && r.StatusCode < 500 {
color = yellow
} else if r.StatusCode >= 500 && r.StatusCode < 600 {
color = red
} }
color(buf, " (Status: %d)", r.StatusCode)
} }
if !r.HideLength { if !r.HideLength {
@ -64,9 +84,7 @@ func (r Result) ResultToString() (string, error) {
location := r.Header.Get("Location") location := r.Header.Get("Location")
if location != "" { if location != "" {
if _, err := fmt.Fprintf(buf, " [--> %s]", location); err != nil { blue(buf, " [--> %s]", location)
return "", err
}
} }
if _, err := fmt.Fprintf(buf, "\n"); err != nil { if _, err := fmt.Fprintf(buf, "\n"); err != nil {

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"net/netip"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"time" "time"
@ -17,7 +18,7 @@ import (
// ErrWildcard is returned if a wildcard response is found // ErrWildcard is returned if a wildcard response is found
type ErrWildcard struct { type ErrWildcard struct {
wildcardIps libgobuster.StringSet wildcardIps libgobuster.Set[netip.Addr]
} }
// Error is the implementation of the error interface // Error is the implementation of the error interface
@ -31,7 +32,7 @@ type GobusterDNS struct {
globalopts *libgobuster.Options globalopts *libgobuster.Options
options *OptionsDNS options *OptionsDNS
isWildcard bool isWildcard bool
wildcardIps libgobuster.StringSet wildcardIps libgobuster.Set[netip.Addr]
} }
func newCustomDialer(server string) func(ctx context.Context, network, address string) (net.Conn, error) { func newCustomDialer(server string) func(ctx context.Context, network, address string) (net.Conn, error) {
@ -65,7 +66,7 @@ func NewGobusterDNS(globalopts *libgobuster.Options, opts *OptionsDNS) (*Gobuste
g := GobusterDNS{ g := GobusterDNS{
options: opts, options: opts,
globalopts: globalopts, globalopts: globalopts,
wildcardIps: libgobuster.NewStringSet(), wildcardIps: libgobuster.NewSet[netip.Addr](),
resolver: resolver, resolver: resolver,
} }
return &g, nil return &g, nil
@ -76,11 +77,6 @@ func (d *GobusterDNS) Name() string {
return "DNS enumeration" return "DNS enumeration"
} }
// RequestsPerRun returns the number of requests this plugin makes per single wordlist item
func (d *GobusterDNS) RequestsPerRun() int {
return 1
}
// PreRun is the pre run implementation of gobusterdns // PreRun is the pre run implementation of gobusterdns
func (d *GobusterDNS) PreRun(ctx context.Context) error { func (d *GobusterDNS) PreRun(ctx context.Context) error {
// Resolve a subdomain that probably shouldn't exist // Resolve a subdomain that probably shouldn't exist
@ -106,8 +102,8 @@ func (d *GobusterDNS) PreRun(ctx context.Context) error {
return nil return nil
} }
// Run is the process implementation of gobusterdns // ProcessWord is the process implementation of gobusterdns
func (d *GobusterDNS) Run(ctx context.Context, word string, resChannel chan<- libgobuster.Result) error { func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain) subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain)
ips, err := d.dnsLookup(ctx, subdomain) ips, err := d.dnsLookup(ctx, subdomain)
if err == nil { if err == nil {
@ -126,10 +122,10 @@ func (d *GobusterDNS) Run(ctx context.Context, word string, resChannel chan<- li
result.CNAME = cname result.CNAME = cname
} }
} }
resChannel <- result progress.ResultChan <- result
} }
} else if d.globalopts.Verbose { } else if d.globalopts.Verbose {
resChannel <- Result{ progress.ResultChan <- Result{
Subdomain: subdomain, Subdomain: subdomain,
Found: false, Found: false,
ShowIPs: d.options.ShowIPs, ShowIPs: d.options.ShowIPs,
@ -139,6 +135,10 @@ func (d *GobusterDNS) Run(ctx context.Context, word string, resChannel chan<- li
return nil return nil
} }
func (d *GobusterDNS) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config // GetConfigString returns the string representation of the current config
func (d *GobusterDNS) GetConfigString() (string, error) { func (d *GobusterDNS) GetConfigString() (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
@ -219,10 +219,10 @@ func (d *GobusterDNS) GetConfigString() (string, error) {
return strings.TrimSpace(buffer.String()), nil return strings.TrimSpace(buffer.String()), nil
} }
func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]string, error) { func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) {
ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout) ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
defer cancel() defer cancel()
return d.resolver.LookupHost(ctx2, domain) return d.resolver.LookupNetIP(ctx2, "ip", domain)
} }
func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) { func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) {

View File

@ -2,8 +2,15 @@ package gobusterdns
import ( import (
"bytes" "bytes"
"fmt" "net/netip"
"strings" "strings"
"github.com/fatih/color"
)
var (
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
) )
// Result represents a single result // Result represents a single result
@ -12,7 +19,7 @@ type Result struct {
ShowCNAME bool ShowCNAME bool
Found bool Found bool
Subdomain string Subdomain string
IPs []string IPs []netip.Addr
CNAME string CNAME string
} }
@ -20,28 +27,25 @@ type Result struct {
func (r Result) ResultToString() (string, error) { func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
c := green
if r.Found { if r.Found {
if _, err := fmt.Fprintf(buf, "Found: "); err != nil { c(buf, "Found: ")
return "", err
}
} else { } else {
if _, err := fmt.Fprintf(buf, "Missed: "); err != nil { c = yellow
return "", err c(buf, "Missed: ")
}
} }
if r.ShowIPs && r.Found { if r.ShowIPs && r.Found {
if _, err := fmt.Fprintf(buf, "%s [%s]\n", r.Subdomain, strings.Join(r.IPs, ",")); err != nil { ips := make([]string, len(r.IPs))
return "", err for i := range r.IPs {
ips[i] = r.IPs[i].String()
} }
c(buf, "%s [%s]\n", r.Subdomain, strings.Join(ips, ","))
} else if r.ShowCNAME && r.Found && r.CNAME != "" { } else if r.ShowCNAME && r.Found && r.CNAME != "" {
if _, err := fmt.Fprintf(buf, "%s [%s]\n", r.Subdomain, r.CNAME); err != nil { c(buf, "%s [%s]\n", r.Subdomain, r.CNAME)
return "", err
}
} else { } else {
if _, err := fmt.Fprintf(buf, "%s\n", r.Subdomain); err != nil { c(buf, "%s\n", r.Subdomain)
return "", err
}
} }
s := buf.String() s := buf.String()

View File

@ -5,6 +5,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"net"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
@ -50,6 +51,8 @@ func NewGobusterFuzz(globalopts *libgobuster.Options, opts *OptionsFuzz) (*Gobus
Timeout: opts.Timeout, Timeout: opts.Timeout,
UserAgent: opts.UserAgent, UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation, NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
} }
httpOpts := libgobuster.HTTPOptions{ httpOpts := libgobuster.HTTPOptions{
@ -75,24 +78,44 @@ func (d *GobusterFuzz) Name() string {
return "fuzzing" return "fuzzing"
} }
// RequestsPerRun returns the number of requests this plugin makes per single wordlist item
func (d *GobusterFuzz) RequestsPerRun() int {
return 1
}
// PreRun is the pre run implementation of gobusterfuzz // PreRun is the pre run implementation of gobusterfuzz
func (d *GobusterFuzz) PreRun(ctx context.Context) error { func (d *GobusterFuzz) PreRun(ctx context.Context) error {
return nil return nil
} }
// Run is the process implementation of gobusterfuzz // ProcessWord is the process implementation of gobusterfuzz
func (d *GobusterFuzz) Run(ctx context.Context, word string, resChannel chan<- libgobuster.Result) error { func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
workingURL := strings.ReplaceAll(d.options.URL, "FUZZ", word) url := strings.ReplaceAll(d.options.URL, "FUZZ", word)
statusCode, size, _, _, err := d.http.Request(ctx, workingURL, libgobuster.RequestOptions{})
if err != nil { tries := 1
return err if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += d.options.RetryAttempts
} }
if statusCode != nil {
var statusCode int
var size int64
for i := 1; i <= tries; i++ {
var err error
statusCode, size, _, _, 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 := true resultStatus := true
if helper.SliceContains(d.options.ExcludeLength, int(size)) { if helper.SliceContains(d.options.ExcludeLength, int(size)) {
@ -100,17 +123,17 @@ func (d *GobusterFuzz) Run(ctx context.Context, word string, resChannel chan<- l
} }
if d.options.ExcludedStatusCodesParsed.Length() > 0 { if d.options.ExcludedStatusCodesParsed.Length() > 0 {
if d.options.ExcludedStatusCodesParsed.Contains(*statusCode) { if d.options.ExcludedStatusCodesParsed.Contains(statusCode) {
resultStatus = false resultStatus = false
} }
} }
if resultStatus || d.globalopts.Verbose { if resultStatus || d.globalopts.Verbose {
resChannel <- Result{ progress.ResultChan <- Result{
Verbose: d.globalopts.Verbose, Verbose: d.globalopts.Verbose,
Found: resultStatus, Found: resultStatus,
Path: workingURL, Path: url,
StatusCode: *statusCode, StatusCode: statusCode,
Size: size, Size: size,
} }
} }
@ -118,6 +141,10 @@ func (d *GobusterFuzz) Run(ctx context.Context, word string, resChannel chan<- l
return nil return nil
} }
func (d *GobusterFuzz) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config // GetConfigString returns the string representation of the current config
func (d *GobusterFuzz) GetConfigString() (string, error) { func (d *GobusterFuzz) GetConfigString() (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer

View File

@ -8,13 +8,13 @@ import (
type OptionsFuzz struct { type OptionsFuzz struct {
libgobuster.HTTPOptions libgobuster.HTTPOptions
ExcludedStatusCodes string ExcludedStatusCodes string
ExcludedStatusCodesParsed libgobuster.IntSet ExcludedStatusCodesParsed libgobuster.Set[int]
ExcludeLength []int ExcludeLength []int
} }
// NewOptionsFuzz returns a new initialized OptionsFuzz // NewOptionsFuzz returns a new initialized OptionsFuzz
func NewOptionsFuzz() *OptionsFuzz { func NewOptionsFuzz() *OptionsFuzz {
return &OptionsFuzz{ return &OptionsFuzz{
ExcludedStatusCodesParsed: libgobuster.NewIntSet(), ExcludedStatusCodesParsed: libgobuster.NewSet[int](),
} }
} }

View File

@ -2,7 +2,13 @@ package gobusterfuzz
import ( import (
"bytes" "bytes"
"fmt"
"github.com/fatih/color"
)
var (
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
) )
// Result represents a single result // Result represents a single result
@ -18,30 +24,22 @@ type Result struct {
func (r Result) ResultToString() (string, error) { func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
c := green
// Prefix if we're in verbose mode // Prefix if we're in verbose mode
if r.Verbose { if r.Verbose {
if r.Found { if r.Found {
if _, err := fmt.Fprintf(buf, "Found: "); err != nil { c(buf, "Found: ")
return "", err
}
} else { } else {
if _, err := fmt.Fprintf(buf, "Missed: "); err != nil { c = yellow
return "", err c(buf, "Missed: ")
}
} }
} else if r.Found { } else if r.Found {
if _, err := fmt.Fprintf(buf, "Found: "); err != nil { c(buf, "Found: ")
return "", err
}
} }
if _, err := fmt.Fprintf(buf, "[Status=%d] [Length=%d] %s", r.StatusCode, r.Size, r.Path); err != nil { c(buf, "[Status=%d] [Length=%d] %s", r.StatusCode, r.Size, r.Path)
return "", err c(buf, "\n")
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String() s := buf.String()
return s, nil return s, nil

265
gobustergcs/gobustersgcs.go Normal file
View File

@ -0,0 +1,265 @@
package gobustergcs
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"regexp"
"strings"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
)
// GobusterGCS is the main type to implement the interface
type GobusterGCS struct {
options *OptionsGCS
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
bucketRegex *regexp.Regexp
}
// NewGobusterGCS creates a new initialized GobusterGCS
func NewGobusterGCS(globalopts *libgobuster.Options, opts *OptionsGCS) (*GobusterGCS, 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 := GobusterGCS{
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,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
// needed so we can list bucket contents
FollowRedirect: true,
}
h, err := libgobuster.NewHTTPClient(&httpOpts)
if err != nil {
return nil, err
}
g.http = h
// https://cloud.google.com/storage/docs/naming-buckets
g.bucketRegex = regexp.MustCompile(`^[a-z0-9][a-z0-9\-_.]{1,61}[a-z0-9](\.[a-z0-9][a-z0-9\-_.]{1,61}[a-z0-9])*$`)
return &g, nil
}
// Name should return the name of the plugin
func (s *GobusterGCS) Name() string {
return "GCS bucket enumeration"
}
// PreRun is the pre run implementation of GobusterS3
func (s *GobusterGCS) PreRun(ctx context.Context) error {
return nil
}
// ProcessWord is the process implementation of GobusterS3
func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
// only check for valid bucket names
if !s.isValidBucketName(word) {
return nil
}
bucketURL := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o?maxResults=%d", word, s.options.MaxFilesToList)
tries := 1
if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += s.options.RetryAttempts
}
var statusCode int
var body []byte
for i := 1; i <= tries; i++ {
var err error
statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true})
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 || body == nil {
return nil
}
// looks like 401, 403, and 404 are the only negative status codes
found := false
switch statusCode {
case http.StatusUnauthorized,
http.StatusForbidden,
http.StatusNotFound:
found = false
case http.StatusOK:
// listing enabled
found = true
default:
// default to found as we use negative status codes
found = true
}
// nothing found, bail out
// may add the result later if we want to enable verbose output
if !found {
return nil
}
extraStr := ""
if s.globalopts.Verbose {
// get status
var result map[string]interface{}
err := json.Unmarshal(body, &result)
if err != nil {
return fmt.Errorf("could not parse response json: %w", err)
}
if _, exist := result["error"]; exist {
// https://cloud.google.com/storage/docs/json_api/v1/status-codes
gcsError := GCSError{}
err := json.Unmarshal(body, &gcsError)
if err != nil {
return fmt.Errorf("could not parse error json: %w", err)
}
extraStr = fmt.Sprintf("Error: %s (%d)", gcsError.Error.Message, gcsError.Error.Code)
} else if v, exist := result["kind"]; exist && v == "storage#objects" {
// https://cloud.google.com/storage/docs/json_api/v1/status-codes
// bucket listing enabled
gcsListing := GCSListing{}
err := json.Unmarshal(body, &gcsListing)
if err != nil {
return fmt.Errorf("could not parse result json: %w", err)
}
extraStr = "Bucket Listing enabled: "
for _, x := range gcsListing.Items {
extraStr += fmt.Sprintf("%s (%sb), ", x.Name, x.Size)
}
extraStr = strings.TrimRight(extraStr, ", ")
}
}
progress.ResultChan <- Result{
Found: found,
BucketName: word,
Status: extraStr,
}
return nil
}
func (s *GobusterGCS) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (s *GobusterGCS) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := s.options
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", s.globalopts.Threads); err != nil {
return "", err
}
if s.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if s.globalopts.Wordlist != "-" {
wordlist = s.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if s.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if s.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); 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
}
// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
func (s *GobusterGCS) isValidBucketName(bucketName string) bool {
if len(bucketName) > 222 || !s.bucketRegex.MatchString(bucketName) {
return false
}
if strings.HasPrefix(bucketName, "-") || strings.HasSuffix(bucketName, "-") ||
strings.HasPrefix(bucketName, "_") || strings.HasSuffix(bucketName, "_") ||
strings.HasPrefix(bucketName, ".") || strings.HasSuffix(bucketName, ".") {
return false
}
return true
}

16
gobustergcs/options.go Normal file
View File

@ -0,0 +1,16 @@
package gobustergcs
import (
"github.com/OJ/gobuster/v3/libgobuster"
)
// OptionsGCS is the struct to hold all options for this plugin
type OptionsGCS struct {
libgobuster.BasicHTTPOptions
MaxFilesToList int
}
// NewOptionsGCS returns a new initialized OptionsS3
func NewOptionsGCS() *OptionsGCS {
return &OptionsGCS{}
}

35
gobustergcs/result.go Normal file
View File

@ -0,0 +1,35 @@
package gobustergcs
import (
"bytes"
"github.com/fatih/color"
)
var (
green = color.New(color.FgGreen).FprintfFunc()
)
// Result represents a single result
type Result struct {
Found bool
BucketName string
Status string
}
// ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
c := green
c(buf, "https://storage.googleapis.com/storage/v1/b/%s/o", r.BucketName)
if r.Status != "" {
c(buf, " [%s]", r.Status)
}
c(buf, "\n")
str := buf.String()
return str, nil
}

25
gobustergcs/types.go Normal file
View File

@ -0,0 +1,25 @@
package gobustergcs
// GCSError represents a returned error from GCS
type GCSError struct {
Error struct {
Code int `json:"code"`
Message string `json:"message"`
Errors []struct {
Message string `json:"message"`
Reason string `json:"reason"`
LocationType string `json:"locationType"`
Location string `json:"location"`
} `json:"errors"`
} `json:"error"`
}
// GCSListing contains only a subset of returned properties
type GCSListing struct {
IsTruncated string `json:"nextPageToken"`
Items []struct {
Name string `json:"name"`
LastModified string `json:"updated"`
Size string `json:"size"`
} `json:"items"`
}

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"net"
"net/http" "net/http"
"regexp" "regexp"
"strings" "strings"
@ -42,6 +43,8 @@ func NewGobusterS3(globalopts *libgobuster.Options, opts *OptionsS3) (*GobusterS
Timeout: opts.Timeout, Timeout: opts.Timeout,
UserAgent: opts.UserAgent, UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation, NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
} }
httpOpts := libgobuster.HTTPOptions{ httpOpts := libgobuster.HTTPOptions{
@ -65,36 +68,55 @@ func (s *GobusterS3) Name() string {
return "S3 bucket enumeration" return "S3 bucket enumeration"
} }
// RequestsPerRun returns the number of requests this plugin makes per single wordlist item
func (s *GobusterS3) RequestsPerRun() int {
return 1
}
// PreRun is the pre run implementation of GobusterS3 // PreRun is the pre run implementation of GobusterS3
func (s *GobusterS3) PreRun(ctx context.Context) error { func (s *GobusterS3) PreRun(ctx context.Context) error {
return nil return nil
} }
// Run is the process implementation of GobusterS3 // ProcessWord is the process implementation of GobusterS3
func (s *GobusterS3) Run(ctx context.Context, word string, resChannel chan<- libgobuster.Result) error { func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
// only check for valid bucket names // only check for valid bucket names
if !s.isValidBucketName(word) { if !s.isValidBucketName(word) {
return nil return nil
} }
bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", word, s.options.MaxFilesToList) bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", word, s.options.MaxFilesToList)
status, _, _, body, err := s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true})
if err != nil { tries := 1
return err if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += s.options.RetryAttempts
} }
if status == nil || body == nil { var statusCode int
var body []byte
for i := 1; i <= tries; i++ {
var err error
statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true})
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 || body == nil {
return nil return nil
} }
// looks like 404 and 400 are the only negative status codes // looks like 404 and 400 are the only negative status codes
found := false found := false
switch *status { switch statusCode {
case http.StatusBadRequest: case http.StatusBadRequest:
case http.StatusNotFound: case http.StatusNotFound:
found = false found = false
@ -139,7 +161,7 @@ func (s *GobusterS3) Run(ctx context.Context, word string, resChannel chan<- lib
} }
} }
resChannel <- Result{ progress.ResultChan <- Result{
Found: found, Found: found,
BucketName: word, BucketName: word,
Status: extraStr, Status: extraStr,
@ -148,6 +170,10 @@ func (s *GobusterS3) Run(ctx context.Context, word string, resChannel chan<- lib
return nil return nil
} }
func (d *GobusterS3) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config // GetConfigString returns the string representation of the current config
func (s *GobusterS3) GetConfigString() (string, error) { func (s *GobusterS3) GetConfigString() (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer

View File

@ -2,7 +2,12 @@ package gobusters3
import ( import (
"bytes" "bytes"
"fmt"
"github.com/fatih/color"
)
var (
green = color.New(color.FgGreen).FprintfFunc()
) )
// Result represents a single result // Result represents a single result
@ -16,19 +21,14 @@ type Result struct {
func (r Result) ResultToString() (string, error) { func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
if _, err := fmt.Fprintf(buf, "http://%s.s3.amazonaws.com/", r.BucketName); err != nil { c := green
return "", err
} c(buf, "http://%s.s3.amazonaws.com/", r.BucketName)
if r.Status != "" { if r.Status != "" {
if _, err := fmt.Fprintf(buf, " [%s]", r.Status); err != nil { c(buf, " [%s]", r.Status)
return "", err
}
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
} }
c(buf, "\n")
str := buf.String() str := buf.String()
return str, nil return str, nil

View File

@ -5,6 +5,8 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"net"
"net/http"
"net/url" "net/url"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
@ -16,12 +18,12 @@ import (
// GobusterVhost is the main type to implement the interface // GobusterVhost is the main type to implement the interface
type GobusterVhost struct { type GobusterVhost struct {
options *OptionsVhost options *OptionsVhost
globalopts *libgobuster.Options globalopts *libgobuster.Options
http *libgobuster.HTTPClient http *libgobuster.HTTPClient
domain string domain string
baseline1 []byte normalBody []byte
baseline2 []byte abnormalBody []byte
} }
// NewGobusterVhost creates a new initialized GobusterDir // NewGobusterVhost creates a new initialized GobusterDir
@ -44,6 +46,8 @@ func NewGobusterVhost(globalopts *libgobuster.Options, opts *OptionsVhost) (*Gob
Timeout: opts.Timeout, Timeout: opts.Timeout,
UserAgent: opts.UserAgent, UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation, NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
} }
httpOpts := libgobuster.HTTPOptions{ httpOpts := libgobuster.HTTPOptions{
@ -69,11 +73,6 @@ func (v *GobusterVhost) Name() string {
return "VHOST enumeration" return "VHOST enumeration"
} }
// RequestsPerRun returns the number of requests this plugin makes per single wordlist item
func (v *GobusterVhost) RequestsPerRun() int {
return 1
}
// PreRun is the pre run implementation of gobusterdir // PreRun is the pre run implementation of gobusterdir
func (v *GobusterVhost) PreRun(ctx context.Context) error { func (v *GobusterVhost) PreRun(ctx context.Context) error {
// add trailing slash // add trailing slash
@ -91,25 +90,25 @@ func (v *GobusterVhost) PreRun(ctx context.Context) error {
v.domain = urlParsed.Host v.domain = urlParsed.Host
} }
// request default vhost for baseline1 // request default vhost for normalBody
_, _, _, tmp, err := v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{ReturnBody: true}) _, _, _, body, err := v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{ReturnBody: true})
if err != nil { if err != nil {
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err) return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
} }
v.baseline1 = tmp v.normalBody = body
// request non existent vhost for baseline2 // request non existent vhost for abnormalBody
subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain) subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain)
_, _, _, tmp, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true}) _, _, _, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
if err != nil { if err != nil {
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err) return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
} }
v.baseline2 = tmp v.abnormalBody = body
return nil return nil
} }
// Run is the process implementation of gobusterdir // ProcessWord is the process implementation of gobusterdir
func (v *GobusterVhost) Run(ctx context.Context, word string, resChannel chan<- libgobuster.Result) error { func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
var subdomain string var subdomain string
if v.options.AppendDomain { if v.options.AppendDomain {
subdomain = fmt.Sprintf("%s.%s", word, v.domain) subdomain = fmt.Sprintf("%s.%s", word, v.domain)
@ -117,23 +116,49 @@ func (v *GobusterVhost) Run(ctx context.Context, word string, resChannel chan<-
// wordlist needs to include full domains // wordlist needs to include full domains
subdomain = word subdomain = word
} }
status, size, header, body, err := v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
if err != nil { tries := 1
return err if v.options.RetryOnTimeout && v.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += v.options.RetryAttempts
}
var statusCode int
var size int64
var header http.Header
var body []byte
for i := 1; i <= tries; i++ {
var err error
statusCode, size, header, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
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
} }
// subdomain must not match default vhost and non existent vhost // subdomain must not match default vhost and non existent vhost
// or verbose mode is enabled // or verbose mode is enabled
found := !bytes.Equal(body, v.baseline1) && !bytes.Equal(body, v.baseline2) found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody)
if (found && !helper.SliceContains(v.options.ExcludeLength, int(size))) || v.globalopts.Verbose { if (found && !helper.SliceContains(v.options.ExcludeLength, int(size))) || v.globalopts.Verbose {
resultStatus := false resultStatus := false
if found { if found {
resultStatus = true resultStatus = true
} }
resChannel <- Result{ progress.ResultChan <- Result{
Found: resultStatus, Found: resultStatus,
Vhost: subdomain, Vhost: subdomain,
StatusCode: *status, StatusCode: statusCode,
Size: size, Size: size,
Header: header, Header: header,
} }
@ -141,6 +166,10 @@ func (v *GobusterVhost) Run(ctx context.Context, word string, resChannel chan<-
return nil return nil
} }
func (v *GobusterVhost) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config // GetConfigString returns the string representation of the current config
func (v *GobusterVhost) GetConfigString() (string, error) { func (v *GobusterVhost) GetConfigString() (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer

View File

@ -1,9 +1,19 @@
package gobustervhost package gobustervhost
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"github.com/fatih/color"
)
var (
white = color.New(color.FgWhite).SprintFunc()
yellow = color.New(color.FgYellow).SprintFunc()
green = color.New(color.FgGreen).SprintFunc()
blue = color.New(color.FgBlue).SprintFunc()
red = color.New(color.FgRed).SprintFunc()
cyan = color.New(color.FgCyan).SprintFunc()
) )
// Result represents a single result // Result represents a single result
@ -17,17 +27,29 @@ type Result struct {
// ResultToString converts the Result to it's textual representation // ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) { func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{} statusText := yellow("Missed")
statusText := "Missed"
if r.Found { if r.Found {
statusText = "Found" statusText = green("Found")
} }
if _, err := fmt.Fprintf(buf, "%s: %s (Status: %d) [Size: %d]\n", statusText, r.Vhost, r.StatusCode, r.Size); err != nil { statusCodeColor := white
return "", err if r.StatusCode == 200 {
statusCodeColor = green
} else if r.StatusCode >= 300 && r.StatusCode < 400 {
statusCodeColor = cyan
} else if r.StatusCode >= 400 && r.StatusCode < 500 {
statusCodeColor = yellow
} else if r.StatusCode >= 500 && r.StatusCode < 600 {
statusCodeColor = red
} }
s := buf.String() statusCode := statusCodeColor(fmt.Sprintf("Status: %d", r.StatusCode))
return s, nil
location := r.Header.Get("Location")
locationString := ""
if location != "" {
locationString = blue(fmt.Sprintf(" [--> %s]", location))
}
return fmt.Sprintf("%s: %s %s [Size: %d]%s\n", statusText, r.Vhost, statusCode, r.Size, locationString), nil
} }

View File

@ -9,12 +9,13 @@ import (
) )
// ParseExtensions parses the extensions provided as a comma separated list // ParseExtensions parses the extensions provided as a comma separated list
func ParseExtensions(extensions string) (libgobuster.StringSet, error) { func ParseExtensions(extensions string) (libgobuster.Set[string], error) {
ret := libgobuster.NewSet[string]()
if extensions == "" { if extensions == "" {
return libgobuster.StringSet{}, nil return ret, nil
} }
ret := libgobuster.NewStringSet()
for _, e := range strings.Split(extensions, ",") { for _, e := range strings.Split(extensions, ",") {
e = strings.TrimSpace(e) e = strings.TrimSpace(e)
// remove leading . from extensions // remove leading . from extensions
@ -24,17 +25,18 @@ func ParseExtensions(extensions string) (libgobuster.StringSet, error) {
} }
// ParseCommaSeparatedInt parses the status codes provided as a comma separated list // ParseCommaSeparatedInt parses the status codes provided as a comma separated list
func ParseCommaSeparatedInt(inputString string) (libgobuster.IntSet, error) { func ParseCommaSeparatedInt(inputString string) (libgobuster.Set[int], error) {
ret := libgobuster.NewSet[int]()
if inputString == "" { if inputString == "" {
return libgobuster.IntSet{}, nil return ret, nil
} }
ret := libgobuster.NewIntSet()
for _, c := range strings.Split(inputString, ",") { for _, c := range strings.Split(inputString, ",") {
c = strings.TrimSpace(c) c = strings.TrimSpace(c)
i, err := strconv.Atoi(c) i, err := strconv.Atoi(c)
if err != nil { if err != nil {
return libgobuster.IntSet{}, fmt.Errorf("invalid string given: %s", c) return libgobuster.NewSet[int](), fmt.Errorf("invalid string given: %s", c)
} }
ret.Add(i) ret.Add(i)
} }

View File

@ -12,14 +12,14 @@ func TestParseExtensions(t *testing.T) {
var tt = []struct { var tt = []struct {
testName string testName string
extensions string extensions string
expectedExtensions libgobuster.StringSet expectedExtensions libgobuster.Set[string]
expectedError string expectedError string
}{ }{
{"Valid extensions", "php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Valid extensions", "php,asp,txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Spaces", "php, asp , txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Double extensions", "php,asp,txt,php,asp,txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Leading dot", ".php,asp,.txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", libgobuster.NewStringSet(), "invalid extension string provided"}, {"Empty string", "", libgobuster.NewSet[string](), "invalid extension string provided"},
} }
for _, x := range tt { for _, x := range tt {
@ -43,15 +43,15 @@ func TestParseCommaSeparatedInt(t *testing.T) {
var tt = []struct { var tt = []struct {
testName string testName string
stringCodes string stringCodes string
expectedCodes libgobuster.IntSet expectedCodes libgobuster.Set[int]
expectedError string expectedError string
}{ }{
{"Valid codes", "200,100,202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Valid codes", "200,100,202", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Spaces", "200, 100 , 202", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Double codes", "200, 100, 202, 100", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", libgobuster.NewIntSet(), "invalid string given: AAA"}, {"Invalid code", "200,AAA", libgobuster.NewSet[int](), "invalid string given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", libgobuster.NewIntSet(), "invalid string given: 2000000000000000000000000000000"}, {"Invalid integer", "2000000000000000000000000000000", libgobuster.NewSet[int](), "invalid string given: 2000000000000000000000000000000"},
{"Empty string", "", libgobuster.NewIntSet(), "invalid string provided"}, {"Empty string", "", libgobuster.NewSet[int](), "invalid string provided"},
} }
for _, x := range tt { for _, x := range tt {
@ -74,14 +74,14 @@ func BenchmarkParseExtensions(b *testing.B) {
var tt = []struct { var tt = []struct {
testName string testName string
extensions string extensions string
expectedExtensions libgobuster.StringSet expectedExtensions libgobuster.Set[string]
expectedError string expectedError string
}{ }{
{"Valid extensions", "php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Valid extensions", "php,asp,txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Spaces", "php, asp , txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Double extensions", "php,asp,txt,php,asp,txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", libgobuster.StringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Leading dot", ".php,asp,.txt", libgobuster.Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", libgobuster.NewStringSet(), "invalid extension string provided"}, {"Empty string", "", libgobuster.NewSet[string](), "invalid extension string provided"},
} }
for _, x := range tt { for _, x := range tt {
@ -98,15 +98,15 @@ func BenchmarkParseCommaSeparatedInt(b *testing.B) {
var tt = []struct { var tt = []struct {
testName string testName string
stringCodes string stringCodes string
expectedCodes libgobuster.IntSet expectedCodes libgobuster.Set[int]
expectedError string expectedError string
}{ }{
{"Valid codes", "200,100,202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Valid codes", "200,100,202", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Spaces", "200, 100 , 202", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", libgobuster.IntSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Double codes", "200, 100, 202, 100", libgobuster.Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", libgobuster.NewIntSet(), "invalid string given: AAA"}, {"Invalid code", "200,AAA", libgobuster.NewSet[int](), "invalid string given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", libgobuster.NewIntSet(), "invalid string given: 2000000000000000000000000000000"}, {"Invalid integer", "2000000000000000000000000000000", libgobuster.NewSet[int](), "invalid string given: 2000000000000000000000000000000"},
{"Empty string", "", libgobuster.NewIntSet(), "invalid string string provided"}, {"Empty string", "", libgobuster.NewSet[int](), "invalid string string provided"},
} }
for _, x := range tt { for _, x := range tt {

View File

@ -5,47 +5,41 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"sort"
"strings" "strings"
) )
// IntSet is a set of Ints // Set is a set of Ts
type IntSet struct { type Set[T comparable] struct {
Set map[int]bool Set map[T]bool
} }
// StringSet is a set of Strings // NewSSet creates a new initialized Set
type StringSet struct { func NewSet[T comparable]() Set[T] {
Set map[string]bool return Set[T]{Set: map[T]bool{}}
}
// NewStringSet creates a new initialized StringSet
func NewStringSet() StringSet {
return StringSet{Set: map[string]bool{}}
} }
// Add an element to a set // Add an element to a set
func (set *StringSet) Add(s string) bool { func (set *Set[T]) Add(s T) bool {
_, found := set.Set[s] _, found := set.Set[s]
set.Set[s] = true set.Set[s] = true
return !found return !found
} }
// AddRange adds a list of elements to a set // AddRange adds a list of elements to a set
func (set *StringSet) AddRange(ss []string) { func (set *Set[T]) AddRange(ss []T) {
for _, s := range ss { for _, s := range ss {
set.Set[s] = true set.Set[s] = true
} }
} }
// Contains tests if an element is in a set // Contains tests if an element is in a set
func (set *StringSet) Contains(s string) bool { func (set *Set[T]) Contains(s T) bool {
_, found := set.Set[s] _, found := set.Set[s]
return found return found
} }
// ContainsAny checks if any of the elements exist // ContainsAny checks if any of the elements exist
func (set *StringSet) ContainsAny(ss []string) bool { func (set *Set[T]) ContainsAny(ss []T) bool {
for _, s := range ss { for _, s := range ss {
if set.Set[s] { if set.Set[s] {
return true return true
@ -55,58 +49,21 @@ func (set *StringSet) ContainsAny(ss []string) bool {
} }
// Length returns the length of the Set // Length returns the length of the Set
func (set *StringSet) Length() int { func (set *Set[T]) Length() int {
return len(set.Set) return len(set.Set)
} }
// Stringify the set // Stringify the set
func (set *StringSet) Stringify() string { func (set *Set[T]) Stringify() string {
values := make([]string, len(set.Set)) values := make([]string, len(set.Set))
i := 0 i := 0
for s := range set.Set { for s := range set.Set {
values[i] = s values[i] = fmt.Sprint(s)
i++ i++
} }
return strings.Join(values, ",") return strings.Join(values, ",")
} }
// NewIntSet creates a new initialized IntSet
func NewIntSet() IntSet {
return IntSet{Set: map[int]bool{}}
}
// Add adds an element to a set
func (set *IntSet) Add(i int) bool {
_, found := set.Set[i]
set.Set[i] = true
return !found
}
// Contains tests if an element is in a set
func (set *IntSet) Contains(i int) bool {
_, found := set.Set[i]
return found
}
// Stringify the set
func (set *IntSet) Stringify() string {
values := make([]int, len(set.Set))
i := 0
for s := range set.Set {
values[i] = s
i++
}
sort.Ints(values)
delim := ","
return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(values)), delim), "[]")
}
// Length returns the length of the Set
func (set *IntSet) Length() int {
return len(set.Set)
}
func lineCounter(r io.Reader) (int, error) { func lineCounter(r io.Reader) (int, error) {
buf := make([]byte, 32*1024) buf := make([]byte, 32*1024)
count := 1 count := 1

View File

@ -2,77 +2,109 @@ package libgobuster
import ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"testing" "testing"
"testing/iotest" "testing/iotest"
) )
func TestNewStringSet(t *testing.T) { func TestNewSet(t *testing.T) {
t.Parallel() t.Parallel()
if NewStringSet().Set == nil { if NewSet[string]().Set == nil {
t.Fatal("newStringSet returned nil Set") t.Fatal("NewSet[string] returned nil Set")
}
if NewSet[int]().Set == nil {
t.Fatal("NewSet[int] returned nil Set")
} }
} }
func TestNewIntSet(t *testing.T) { func TestSetAdd(t *testing.T) {
t.Parallel() t.Parallel()
if NewIntSet().Set == nil { x := NewSet[string]()
t.Fatal("newIntSet returned nil Set")
}
}
func TestStringSetAdd(t *testing.T) {
t.Parallel()
x := NewStringSet()
x.Add("test") x.Add("test")
if len(x.Set) != 1 { if len(x.Set) != 1 {
t.Fatalf("Unexpected size. Should have 1 Got %v", len(x.Set)) t.Fatalf("Unexpected string size. Should have 1 Got %v", len(x.Set))
}
y := NewSet[int]()
y.Add(1)
if len(y.Set) != 1 {
t.Fatalf("Unexpected int size. Should have 1 Got %v", len(y.Set))
} }
} }
func TestStringSetAddDouble(t *testing.T) { func TestSetAddDouble(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
x.Add("test") x.Add("test")
x.Add("test") x.Add("test")
if len(x.Set) != 1 { if len(x.Set) != 1 {
t.Fatalf("Unexpected size. Should have 1 Got %d", len(x.Set)) t.Fatalf("Unexpected string size. Should be 1 (unique) Got %v", len(x.Set))
}
y := NewSet[int]()
y.Add(1)
y.Add(1)
if len(y.Set) != 1 {
t.Fatalf("Unexpected int size. Should be 1 (unique) Got %v", len(y.Set))
} }
} }
func TestStringSetAddRange(t *testing.T) { func TestSetAddRange(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
x.AddRange([]string{"string1", "string2"}) x.AddRange([]string{"string1", "string2"})
if len(x.Set) != 2 { if len(x.Set) != 2 {
t.Fatalf("Unexpected size. Should have 2 Got %d", len(x.Set)) t.Fatalf("Unexpected string size. Should have 2 Got %v", len(x.Set))
}
y := NewSet[int]()
y.AddRange([]int{1, 2})
if len(y.Set) != 2 {
t.Fatalf("Unexpected int size. Should have 2 Got %v", len(y.Set))
} }
} }
func TestStringSetAddRangeDouble(t *testing.T) { func TestSetAddRangeDouble(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
x.AddRange([]string{"string1", "string2", "string1", "string2"}) x.AddRange([]string{"string1", "string2", "string1", "string2"})
if len(x.Set) != 2 { if len(x.Set) != 2 {
t.Fatalf("Unexpected size. Should have 2 Got %d", len(x.Set)) t.Fatalf("Unexpected string size. Should be 2 (unique) Got %v", len(x.Set))
}
y := NewSet[int]()
y.AddRange([]int{1, 2, 1, 2})
if len(y.Set) != 2 {
t.Fatalf("Unexpected int size. Should be 2 (unique) Got %v", len(y.Set))
} }
} }
func TestStringSetContains(t *testing.T) { func TestSetContains(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"} v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v) x.AddRange(v)
for _, y := range v { for _, i := range v {
if !x.Contains(y) { if !x.Contains(i) {
t.Fatalf("Did not find value %s in array. %v", y, x.Set) t.Fatalf("Did not find value %s in array. %v", i, x.Set)
}
}
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
for _, i := range v2 {
if !y.Contains(i) {
t.Fatalf("Did not find value %d in array. %v", i, y.Set)
} }
} }
} }
func TestStringSetContainsAny(t *testing.T) { func TestSetContainsAny(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"} v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v) x.AddRange(v)
if !x.ContainsAny(v) { if !x.ContainsAny(v) {
@ -83,70 +115,45 @@ func TestStringSetContainsAny(t *testing.T) {
if x.ContainsAny([]string{"mmmm", "nnnnn"}) { if x.ContainsAny([]string{"mmmm", "nnnnn"}) {
t.Fatal("Found unexpected values") t.Fatal("Found unexpected values")
} }
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
if !y.ContainsAny(v2) {
t.Fatalf("Did not find any")
}
// test not found
if y.ContainsAny([]int{9235, 2398532}) {
t.Fatal("Found unexpected values")
}
} }
func TestStringSetStringify(t *testing.T) { func TestSetStringify(t *testing.T) {
t.Parallel() t.Parallel()
x := NewStringSet() x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"} v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v) x.AddRange(v)
z := x.Stringify() z := x.Stringify()
// order is random // order is random
for _, y := range v { for _, i := range v {
if !strings.Contains(z, y) { if !strings.Contains(z, i) {
t.Fatalf("Did not find value %q in %q", y, z) t.Fatalf("Did not find value %q in %q", i, z)
} }
} }
}
func TestIntSetAdd(t *testing.T) { y := NewSet[int]()
t.Parallel() v2 := []int{1, 2312, 123121, 999, -99}
x := NewIntSet() y.AddRange(v2)
x.Add(1) z = y.Stringify()
if len(x.Set) != 1 { // order is random
t.Fatalf("Unexpected size. Should have 1 Got %d", len(x.Set)) for _, i := range v2 {
} if !strings.Contains(z, fmt.Sprint(i)) {
} t.Fatalf("Did not find value %q in %q", i, z)
func TestIntSetAddDouble(t *testing.T) {
t.Parallel()
x := NewIntSet()
x.Add(1)
x.Add(1)
if len(x.Set) != 1 {
t.Fatalf("Unexpected size. Should have 1 Got %d", len(x.Set))
}
}
func TestIntSetContains(t *testing.T) {
t.Parallel()
x := NewIntSet()
v := []int{1, 2, 3, 4}
for _, y := range v {
x.Add(y)
}
for _, y := range v {
if !x.Contains(y) {
t.Fatalf("Did not find value %d in array. %v", y, x.Set)
} }
} }
} }
func TestIntSetStringify(t *testing.T) {
t.Parallel()
x := NewIntSet()
v := []int{1, 3, 2, 4}
expected := "1,2,3,4"
for _, y := range v {
x.Add(y)
}
z := x.Stringify()
// should be sorted
if expected != z {
t.Fatalf("Expected %q got %q", expected, z)
}
}
func TestLineCounter(t *testing.T) { func TestLineCounter(t *testing.T) {
t.Parallel() t.Parallel()
var tt = []struct { var tt = []struct {

View File

@ -97,14 +97,14 @@ func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) {
// Request makes an http request and returns the status, the content length, the headers, the body and an error // Request makes an http request and returns the status, the content length, the headers, the body and an error
// if you want the body returned set the corresponding property inside RequestOptions // if you want the body returned set the corresponding property inside RequestOptions
func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts RequestOptions) (*int, int64, http.Header, []byte, error) { func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts RequestOptions) (int, int64, http.Header, []byte, error) {
resp, err := client.makeRequest(ctx, fullURL, opts.Host, opts.Body) resp, err := client.makeRequest(ctx, fullURL, opts.Host, opts.Body)
if err != nil { if err != nil {
// ignore context canceled errors // ignore context canceled errors
if errors.Is(ctx.Err(), context.Canceled) { if errors.Is(ctx.Err(), context.Canceled) {
return nil, 0, nil, nil, nil return 0, 0, nil, nil, nil
} }
return nil, 0, nil, nil, err return 0, 0, nil, nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -113,7 +113,7 @@ func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts Requ
if opts.ReturnBody { if opts.ReturnBody {
body, err = io.ReadAll(resp.Body) body, err = io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, 0, nil, nil, fmt.Errorf("could not read body %w", err) return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err)
} }
length = int64(len(body)) length = int64(len(body))
} else { } else {
@ -121,11 +121,11 @@ func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts Requ
// absolutely needed so golang will reuse connections! // absolutely needed so golang will reuse connections!
length, err = io.Copy(io.Discard, resp.Body) length, err = io.Copy(io.Discard, resp.Body)
if err != nil { if err != nil {
return nil, 0, nil, nil, err return 0, 0, nil, nil, err
} }
} }
return &resp.StatusCode, length, resp.Header, body, nil return resp.StatusCode, length, resp.Header, body, nil
} }
func (client *HTTPClient) makeRequest(ctx context.Context, fullURL, host string, data io.Reader) (*http.Response, error) { func (client *HTTPClient) makeRequest(ctx context.Context, fullURL, host string, data io.Reader) (*http.Response, error) {

View File

@ -59,7 +59,7 @@ func TestRequest(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Got Error: %v", err) t.Fatalf("Got Error: %v", err)
} }
if *status != 200 { if status != 200 {
t.Fatalf("Invalid status returned: %d", status) t.Fatalf("Invalid status returned: %d", status)
} }
if length != int64(len(ret)) { if length != int64(len(ret)) {

View File

@ -5,9 +5,9 @@ import "context"
// GobusterPlugin is an interface which plugins must implement // GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface { type GobusterPlugin interface {
Name() string Name() string
RequestsPerRun() int
PreRun(context.Context) error PreRun(context.Context) error
Run(context.Context, string, chan<- Result) error ProcessWord(context.Context, string, *Progress) error
AdditionalWords(string) []string
GetConfigString() (string, error) GetConfigString() (string, error)
} }

View File

@ -9,6 +9,8 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/fatih/color"
) )
// PATTERN is the pattern for wordlist replacements in pattern file // PATTERN is the pattern for wordlist replacements in pattern file
@ -25,15 +27,11 @@ type ResultToStringFunc func(*Gobuster, *Result) (*string, error)
// Gobuster is the main object when creating a new run // Gobuster is the main object when creating a new run
type Gobuster struct { type Gobuster struct {
Opts *Options Opts *Options
RequestsExpected int plugin GobusterPlugin
RequestsIssued int LogInfo *log.Logger
RequestsCountMutex *sync.RWMutex LogError *log.Logger
plugin GobusterPlugin Progress *Progress
resultChan chan Result
errorChan chan error
LogInfo *log.Logger
LogError *log.Logger
} }
// NewGobuster returns a new Gobuster object // NewGobuster returns a new Gobuster object
@ -41,31 +39,13 @@ func NewGobuster(opts *Options, plugin GobusterPlugin) (*Gobuster, error) {
var g Gobuster var g Gobuster
g.Opts = opts g.Opts = opts
g.plugin = plugin g.plugin = plugin
g.RequestsCountMutex = new(sync.RWMutex)
g.resultChan = make(chan Result)
g.errorChan = make(chan error)
g.LogInfo = log.New(os.Stdout, "", log.LstdFlags) g.LogInfo = log.New(os.Stdout, "", log.LstdFlags)
g.LogError = log.New(os.Stderr, "[ERROR] ", log.LstdFlags) g.LogError = log.New(os.Stderr, color.New(color.FgRed).Sprint("[ERROR] "), log.LstdFlags)
g.Progress = NewProgress()
return &g, nil return &g, nil
} }
// Results returns a channel of Results
func (g *Gobuster) Results() <-chan Result {
return g.resultChan
}
// Errors returns a channel of errors
func (g *Gobuster) Errors() <-chan error {
return g.errorChan
}
func (g *Gobuster) incrementRequests() {
g.RequestsCountMutex.Lock()
g.RequestsIssued += g.plugin.RequestsPerRun()
g.RequestsCountMutex.Unlock()
}
func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.WaitGroup) { func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
for { for {
@ -77,7 +57,7 @@ func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.
if !ok { if !ok {
return return
} }
g.incrementRequests() g.Progress.incrementRequests()
wordCleaned := strings.TrimSpace(word) wordCleaned := strings.TrimSpace(word)
// Skip "comment" (starts with #), as well as empty lines // Skip "comment" (starts with #), as well as empty lines
@ -86,10 +66,10 @@ func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.
} }
// Mode-specific processing // Mode-specific processing
err := g.plugin.Run(ctx, wordCleaned, g.resultChan) err := g.plugin.ProcessWord(ctx, wordCleaned, g.Progress)
if err != nil { if err != nil {
// do not exit and continue // do not exit and continue
g.errorChan <- err g.Progress.ErrorChan <- err
continue continue
} }
@ -117,15 +97,17 @@ func (g *Gobuster) getWordlist() (*bufio.Scanner, error) {
return nil, fmt.Errorf("failed to get number of lines: %w", err) return nil, fmt.Errorf("failed to get number of lines: %w", err)
} }
g.RequestsIssued = 0
// calcutate expected requests // calcutate expected requests
g.RequestsExpected = lines g.Progress.IncrementTotalRequests(lines)
if g.Opts.PatternFile != "" {
g.RequestsExpected += lines * len(g.Opts.Patterns)
}
g.RequestsExpected *= g.plugin.RequestsPerRun() // call the function once with a dummy entry to receive the number
// of custom words per wordlist word
customWordsLen := len(g.plugin.AdditionalWords("dummy"))
if customWordsLen > 0 {
origExpected := g.Progress.RequestsExpected()
inc := origExpected * customWordsLen
g.Progress.IncrementTotalRequests(inc)
}
// rewind wordlist // rewind wordlist
_, err = wordlist.Seek(0, 0) _, err = wordlist.Seek(0, 0)
@ -138,8 +120,8 @@ func (g *Gobuster) getWordlist() (*bufio.Scanner, error) {
// Run the busting of the website with the given // Run the busting of the website with the given
// set of settings from the command line. // set of settings from the command line.
func (g *Gobuster) Run(ctx context.Context) error { func (g *Gobuster) Run(ctx context.Context) error {
defer close(g.resultChan) defer close(g.Progress.ResultChan)
defer close(g.errorChan) defer close(g.Progress.ErrorChan)
if err := g.plugin.PreRun(ctx); err != nil { if err := g.plugin.PreRun(ctx); err != nil {
return err return err
@ -180,6 +162,15 @@ Scan:
case wordChan <- w: case wordChan <- w:
} }
} }
for _, w := range g.plugin.AdditionalWords(word) {
select {
// need to check here too otherwise wordChan will block
case <-ctx.Done():
break Scan
case wordChan <- w:
}
}
} }
} }
close(wordChan) close(wordChan)

View File

@ -10,6 +10,8 @@ type BasicHTTPOptions struct {
Proxy string Proxy string
NoTLSValidation bool NoTLSValidation bool
Timeout time.Duration Timeout time.Duration
RetryOnTimeout bool
RetryAttempts int
} }
// HTTPOptions is the struct to pass in all http options to Gobuster // HTTPOptions is the struct to pass in all http options to Gobuster

46
libgobuster/progress.go Normal file
View File

@ -0,0 +1,46 @@
package libgobuster
import "sync"
type Progress struct {
requestsExpectedMutex *sync.RWMutex
requestsExpected int
requestsCountMutex *sync.RWMutex
requestsIssued int
ResultChan chan Result
ErrorChan chan error
}
func NewProgress() *Progress {
var p Progress
p.requestsIssued = 0
p.requestsExpectedMutex = new(sync.RWMutex)
p.requestsCountMutex = new(sync.RWMutex)
p.ResultChan = make(chan Result)
p.ErrorChan = make(chan error)
return &p
}
func (p *Progress) RequestsExpected() int {
p.requestsExpectedMutex.RLock()
defer p.requestsExpectedMutex.RUnlock()
return p.requestsExpected
}
func (p *Progress) RequestsIssued() int {
p.requestsCountMutex.RLock()
defer p.requestsCountMutex.RUnlock()
return p.requestsIssued
}
func (p *Progress) incrementRequests() {
p.requestsCountMutex.Lock()
defer p.requestsCountMutex.Unlock()
p.requestsIssued++
}
func (p *Progress) IncrementTotalRequests(by int) {
p.requestsCountMutex.Lock()
defer p.requestsCountMutex.Unlock()
p.requestsExpected += by
}

View File

@ -2,5 +2,5 @@ package libgobuster
const ( const (
// VERSION contains the current gobuster version // VERSION contains the current gobuster version
VERSION = "3.1.0" VERSION = "3.2.0-dev"
) )

150
make.bat
View File

@ -1,150 +0,0 @@
@echo off
SET ARG=%1
SET TARGET=.\build
SET BUILDARGS=-ldflags="-s -w" -trimpath
IF "%ARG%"=="test" (
CALL :Test
GOTO Done
)
IF "%ARG%"=="clean" (
del /F /Q %TARGET%\*.*
go clean ./...
echo Done.
GOTO Done
)
IF "%ARG%"=="windows" (
CALL :Windows
GOTO Done
)
IF "%ARG%"=="darwin" (
CALL :Darwin
GOTO Done
)
IF "%ARG%"=="linux" (
CALL :Linux
GOTO Done
)
IF "%ARG%"=="update" (
CALL :Update
GOTO Done
)
IF "%ARG%"=="fmt" (
CALL :Fmt
GOTO Done
)
IF "%ARG%"=="lint" (
CALL :Lint
GOTO Done
)
IF "%ARG%"=="all" (
CALL :Fmt
CALL :Update
CALL :Lint
CALL :Test
CALL :Darwin
CALL :Linux
CALL :Windows
GOTO Done
)
IF "%ARG%"=="" (
go build -o .\gobuster.exe
GOTO Done
)
GOTO Done
:Test
set GO111MODULE=on
set CGO_ENABLED=0
echo Testing ...
go test -v ./...
echo Done
EXIT /B 0
:Lint
set GO111MODULE=on
echo Linting ...
go get -u github.com/golangci/golangci-lint@master
golangci-lint run ./...
rem remove test deps
go mod tidy
echo Done
:Fmt
set GO111MODULE=on
echo Formatting ...
go fmt ./...
echo Done.
EXIT /B 0
:Update
set GO111MODULE=on
echo Updating ...
go get -u
go mod tidy -v
echo Done.
EXIT /B 0
:Darwin
set GOOS=darwin
set GOARCH=amd64
set GO111MODULE=on
set CGO_ENABLED=0
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster
set GOARCH=386
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster
echo Done.
EXIT /B 0
:Linux
set GOOS=linux
set GOARCH=amd64
set GO111MODULE=on
set CGO_ENABLED=0
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster
set GOARCH=386
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster
echo Done.
EXIT /B 0
:Windows
set GOOS=windows
set GOARCH=amd64
set GO111MODULE=on
set CGO_ENABLED=0
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster.exe
set GOARCH=386
echo Building for %GOOS% %GOARCH% ...
set DIR=%TARGET%\gobuster-%GOOS%-%GOARCH%
mkdir %DIR% 2> NUL
go build %BUILDARGS% -o %DIR%\gobuster.exe
echo Done.
EXIT /B 0
:Done