Showing preview only (662K chars total). Download the full file or copy to clipboard to get everything.
Repository: OJ/gobuster
Branch: master
Commit: c77583fbb882
Files: 79
Total size: 634.1 KB
Directory structure:
gitextract_sdpr052u/
├── .devcontainer/
│ └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── auto-merge-dependabot.yml
│ ├── docker.yml
│ ├── go.yml
│ ├── golangci-lint.yml
│ ├── hadolint.yml
│ ├── release.yml
│ ├── update.yml
│ ├── vhs.yml
│ └── yamllint.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .yamllint.yml
├── Dockerfile
├── LICENSE
├── README.md
├── Taskfile.yml
├── cli/
│ ├── const.go
│ ├── const_windows.go
│ ├── dir/
│ │ ├── dir.go
│ │ └── dir_test.go
│ ├── dns/
│ │ └── dns.go
│ ├── fuzz/
│ │ └── fuzz.go
│ ├── gcs/
│ │ └── gcs.go
│ ├── gobuster.go
│ ├── options.go
│ ├── s3/
│ │ └── s3.go
│ ├── tftp/
│ │ └── tftp.go
│ └── vhost/
│ ├── vhost.go
│ └── vhost_test.go
├── go.mod
├── go.sum
├── gobusterdir/
│ ├── gobusterdir.go
│ ├── gobusterdir_test.go
│ ├── options.go
│ ├── options_test.go
│ └── result.go
├── gobusterdns/
│ ├── gobusterdns.go
│ ├── options.go
│ └── result.go
├── gobusterfuzz/
│ ├── gobusterfuzz.go
│ ├── options.go
│ ├── options_test.go
│ └── result.go
├── gobustergcs/
│ ├── gobustersgcs.go
│ ├── options.go
│ ├── result.go
│ └── types.go
├── gobusters3/
│ ├── gobusters3.go
│ ├── options.go
│ ├── result.go
│ └── types.go
├── gobustertftp/
│ ├── gobustertftp.go
│ ├── options.go
│ └── result.go
├── gobustervhost/
│ ├── gobustervhost.go
│ ├── options.go
│ └── result.go
├── libgobuster/
│ ├── errors.go
│ ├── helpers.go
│ ├── helpers_test.go
│ ├── http.go
│ ├── http_test.go
│ ├── interfaces.go
│ ├── libgobuster.go
│ ├── logger.go
│ ├── options.go
│ ├── options_http.go
│ ├── progress.go
│ ├── useragents.go
│ └── version.go
├── main.go
└── vhs/
├── gobuster_dir.tape
└── server.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go
{
"name": "Go",
"image": "mcr.microsoft.com/devcontainers/go",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/guiyomh/features/golangci-lint:0": {},
"ghcr.io/devcontainers-extra/features/go-task:1": {}
},
"postCreateCommand": {
"install dependencies": "task deps"
},
"customizations": {
"vscode": {
"extensions": [
"golang.go",
"shardulm94.trailing-spaces",
"IBM.output-colorizer",
"github.vscode-github-actions",
"ms-azuretools.vscode-docker",
"task.vscode-task",
"redhat.vscode-yaml",
"usernamehw.errorlens",
"Gruntfuggly.todo-tree"
]
}
}
}
================================================
FILE: .dockerignore
================================================
*.exe
*.out
*.prof
*.txt
*.swp
.vscode/
gobuster
.git/
mitm*
.idea/
================================================
FILE: .gitattributes
================================================
# https://github.com/gitattributes/gitattributes/blob/master/Common.gitattributes
# Common settings that generally should always be used with your language specific settings
# Auto detect text files and perform LF normalization
* text=auto
#
# The above will handle all files NOT found below
#
# Documents
*.bibtex text diff=bibtex
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain
*.md text diff=markdown
*.mdx text diff=markdown
*.tex text diff=tex
*.adoc text
*.textile text
*.mustache text
*.csv text eol=crlf
*.tab text
*.tsv text
*.txt text
*.sql text
*.epub diff=astextplain
# Graphics
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.tif binary
*.tiff binary
*.ico binary
# SVG treated as text by default.
*.svg text
# If you want to treat it as binary,
# use the following line instead.
# *.svg binary
*.eps binary
# Scripts
*.bash text eol=lf
*.fish text eol=lf
*.sh text eol=lf
*.zsh text eol=lf
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
*.cmd text eol=crlf
*.ps1 text eol=crlf
# Serialisation
*.json text
*.toml text
*.xml text
*.yaml text
*.yml text
# Archives
*.7z binary
*.gz binary
*.tar binary
*.tgz binary
*.zip binary
# Text files where line endings should be preserved
*.patch -text
#
# Exclude files from exporting
#
.gitattributes export-ignore
.gitignore export-ignore
.gitkeep export-ignore
# https://github.com/gitattributes/gitattributes/blob/master/Go.gitattributes
# Treat all Go files in this repo as binary, with no git magic updating
# line endings. Windows users contributing to Go will need to use a
# modern version of git and editors capable of LF line endings.
*.go -text diff=golang
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [OJ, firefart]
patreon: OJReeves
open_collective: gobuster
ko_fi: OJReeves
================================================
FILE: .github/dependabot.yml
================================================
# 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: "/"
target-branch: "dev"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
target-branch: "dev"
schedule:
interval: "daily"
- package-ecosystem: docker
directory: "/"
target-branch: "dev"
schedule:
interval: "daily"
- package-ecosystem: "devcontainers"
directory: "/"
target-branch: "dev"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/auto-merge-dependabot.yml
================================================
name: Auto-merge dependabot updates
on:
pull_request:
branches: [main, dev]
permissions:
pull-requests: write
contents: write
jobs:
dependabot-merge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]' && startsWith(github.repository, 'firefart/')
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v2.4.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
# Only if version bump is not a major version change
if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
================================================
FILE: .github/workflows/docker.yml
================================================
name: Build Docker Images
on:
push:
branches:
- main
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
jobs:
Dockerhub:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: checkout sources
uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386,linux/ppc64le
tags: |
ghcr.io/oj/gobuster:latest
================================================
FILE: .github/workflows/go.yml
================================================
name: Go
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Install Task
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Get dependencies
run: task deps
- name: Build linux
run: task linux
- name: Build windows
run: task windows
- name: Test
run: task test
================================================
FILE: .github/workflows/golangci-lint.yml
================================================
name: golangci-lint
on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
jobs:
golangci:
name: lint
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "stable"
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=5m
================================================
FILE: .github/workflows/hadolint.yml
================================================
name: Hadolint
on:
push:
paths:
- "**/Dockerfile"
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
hadolint:
name: hadolint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: hadolint/hadolint-action@v3.2.0
with:
dockerfile: Dockerfile
# DL3007: Using latest is prone to errors if the image will ever update. Pin the version explicitly to a release tag
# DL3018 Pin versions in apk add. Instead of `apk add <package>` use `apk add <package>=<version>`
ignore: DL3007,DL3018
================================================
FILE: .github/workflows/release.yml
================================================
name: goreleaser
on:
push:
tags:
- "*"
permissions:
contents: write
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Fetch all tags
run: git fetch --force --tags
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6.4.0
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/update.yml
================================================
name: Update Dependencies
on:
schedule:
- cron: "0 4 * * *"
workflow_dispatch:
permissions:
contents: write
jobs:
update-dependencies:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: dev
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Update all dependencies
run: |
go get -u ./...
go mod tidy
- name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "chore: update dependencies [automated]"
branch: dev
================================================
FILE: .github/workflows/vhs.yml
================================================
name: vhs
on:
push:
paths:
- vhs/**.tape
permissions:
contents: write
jobs:
vhs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: "stable"
- name: Install Task
uses: arduino/setup-task@v2
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Get dependencies
run: task deps
- name: Build linux
run: task linux
- name: Install deps
run: |
sudo apt update
sudo apt install -y ffmpeg ttyd
- uses: charmbracelet/vhs-action@v2
with:
path: "vhs/gobuster_dir.tape"
- name: commit and push changes
run: |
git config user.name "Github"
git config user.email "<>"
git add vhs/*.gif
git commit -m "update vhs gifs" || echo "no changes to commit"
git push origin master
================================================
FILE: .github/workflows/yamllint.yml
================================================
name: yamllint
on:
push:
paths:
- "**.yml"
- "**.yaml"
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
yamllint:
name: yamllint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: karancode/yamllint-github-action@master
with:
# fail on warnings and errors
yamllint_strict: true
yamllint_config_filepath: ".yamllint.yml"
================================================
FILE: .gitignore
================================================
# https://github.com/github/gitignore/blob/main/Go.gitignore
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
config.json
!config_sample.json
*.secret
*.env
.vscode/
gobuster
*.txt
dist/
================================================
FILE: .golangci.yml
================================================
# based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
version: "2"
run:
relative-path-mode: gomod
linters:
default: none
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- copyloopvar
- depguard
- durationcheck
- errcheck
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- gocheckcompilerdirectives
- gochecknoinits
- gochecksumtype
- goconst
- gocritic
- gomoddirectives
- goprintffuncname
- gosec
- govet
- iface
- ineffassign
- intrange
- loggercheck
- makezero
- mirror
- musttag
- nakedret
- nilerr
- nilnesserr
- nilnil
- noctx
- nonamedreturns
- nosprintfhostport
- perfsprint
- predeclared
- promlinter
- protogetter
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- testableexamples
- testifylint
- tparallel
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- wastedassign
- whitespace
settings:
depguard:
rules:
deprecated:
files:
- $all
deny:
- pkg: github.com/golang/protobuf
desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
- pkg: github.com/satori/go.uuid
desc: Use github.com/google/uuid instead, satori's package is not maintained
- pkg: github.com/gofrs/uuid$
desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
non-main files:
files:
- "!**/main.go"
deny:
- pkg: log$
desc: Use log/slog instead, see https://go.dev/blog/slog
non-test files:
files:
- "!$test"
deny:
- pkg: math/rand$
desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
errcheck:
check-type-assertions: true
exhaustive:
check:
- switch
- map
exhaustruct:
exclude:
- ^net/http.Client$
- ^net/http.Cookie$
- ^net/http.Request$
- ^net/http.Response$
- ^net/http.Server$
- ^net/http.Transport$
- ^net/url.URL$
- ^os/exec.Cmd$
- ^reflect.StructField$
- ^github.com/Shopify/sarama.Config$
- ^github.com/Shopify/sarama.ProducerMessage$
- ^github.com/mitchellh/mapstructure.DecoderConfig$
- ^github.com/prometheus/client_golang/.+Opts$
- ^github.com/spf13/cobra.Command$
- ^github.com/spf13/cobra.CompletionOptions$
- ^github.com/stretchr/testify/mock.Mock$
- ^github.com/testcontainers/testcontainers-go.+Request$
- ^github.com/testcontainers/testcontainers-go.FromDockerfile$
- ^golang.org/x/tools/go/analysis.Analyzer$
- ^google.golang.org/protobuf/.+Options$
- ^gopkg.in/yaml.v3.Node$
gocognit:
min-complexity: 20
gochecksumtype:
default-signifies-exhaustive: false
gocritic:
settings:
captLocal:
paramsOnly: false
underef:
skipRecvDeref: false
govet:
disable:
- fieldalignment
- shadow
enable-all: true
inamedparam:
skip-single-param: true
perfsprint:
strconcat: false
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
sloglint:
no-global: all
context: ""
usetesting:
os-temp-dir: true
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- gocritic
source: //noinspection
- linters:
- bodyclose
- errcheck
- goconst
- gosec
- noctx
- wrapcheck
path: _test\.go
paths:
- third_party$
- builtin$
- examples$
issues:
max-same-issues: 50
formatters:
enable:
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
================================================
FILE: .goreleaser.yaml
================================================
# This is an example .goreleaser.yml file with some sensible defaults.
# Make sure to check the documentation at https://goreleaser.com
# The lines below are called `modelines`. See `:help modeline`
# Feel free to remove those if you don't want/need to use them.
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 2
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:
- formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
formats: [zip]
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
================================================
FILE: .yamllint.yml
================================================
---
extends: default
rules:
truthy: disable
line-length: disable
document-start: disable
================================================
FILE: Dockerfile
================================================
# syntax=docker/dockerfile:1
FROM golang:latest AS build-env
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.mod /src/
RUN go mod download
COPY . .
RUN go build -a -o gobuster -trimpath
FROM alpine:latest
RUN apk add --no-cache ca-certificates \
&& rm -rf /var/cache/*
RUN mkdir -p /app \
&& adduser -D gobuster \
&& chown -R gobuster:gobuster /app
USER gobuster
WORKDIR /app
COPY --from=build-env /src/gobuster .
ENTRYPOINT [ "./gobuster" ]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Gobuster
[](https://goreportcard.com/report/github.com/OJ/gobuster/v3) [](https://github.com/OJ/gobuster/blob/master/LICENSE) [](https://opencollective.com/gobuster) [](https://opencollective.com/gobuster)
## 💻 Introduction
> A fast and flexible brute-forcing tool written in Go
**Gobuster** is a high-performance directory/file, DNS and virtual host brute-forcing tool written in Go. It's designed to be fast, reliable, and easy to use for security professionals and penetration testers.
## ✨ Features
- 🚀 **High Performance**: Multi-threaded scanning with configurable concurrency
- 🔍 **Multiple Modes**: Directory, DNS, virtual host, S3, GCS, TFTP, and fuzzing modes
- 🛡️ **Security Focused**: Built for penetration testing and security assessments
- 🐳 **Docker Support**: Available as a Docker container
- 🔧 **Extensible**: Pattern-based scanning and custom wordlists
## 🎯 What Can Gobuster Do?
- **Web Directory/File Enumeration**: Discover hidden directories and files on web servers
- **DNS Subdomain Discovery**: Find subdomains with wildcard support
- **Virtual Host Detection**: Identify virtual hosts on target web servers
- **Cloud Storage Enumeration**: Discover open Amazon S3 and Google Cloud Storage buckets
- **TFTP File Discovery**: Find files on TFTP servers
- **Custom Fuzzing**: Flexible fuzzing with customizable parameters
## 🚀 Quick Start
```bash
# Install gobuster
go install github.com/OJ/gobuster/v3@latest
# Basic directory enumeration
gobuster dir -u https://example.com -w /path/to/wordlist.txt
# DNS subdomain enumeration
gobuster dns -do example.com -w /path/to/wordlist.txt
# Virtual host discovery
gobuster vhost -u https://example.com -w /path/to/wordlist.txt
# S3 bucket enumeration
gobuster s3 -w /path/to/bucket-names.txt
```
## 📦 Installation
### Quick Install (Recommended)
```bash
go install github.com/OJ/gobuster/v3@latest
```
**Requirements**: Go 1.24 or higher
### Alternative Installation Methods
#### Using Binary Releases
Download pre-compiled binaries from the [releases page](https://github.com/OJ/gobuster/releases).
#### Using Docker
```bash
# Pull the latest image
docker pull ghcr.io/oj/gobuster:latest
# Run gobuster in Docker
docker run --rm -it ghcr.io/oj/gobuster:latest dir -u https://example.com -w /usr/share/wordlists/dirb/common.txt
```
#### Building from Source
```bash
git clone https://github.com/OJ/gobuster.git
cd gobuster
go mod tidy
go build
```
### Troubleshooting Installation
If you encounter issues:
- Ensure Go version 1.24+ is installed: `go version`
- Check your `$GOPATH` and `$GOBIN` environment variables
- Verify `$GOPATH/bin` is in your `$PATH`
## 🎯 Usage
Gobuster uses a mode-based approach. Each mode is designed for specific enumeration tasks:
```bash
gobuster [mode] [options]
```
### Getting Help
```bash
gobuster help # Show general help
gobuster help [mode] # Show help for specific mode
gobuster [mode] --help # Alternative help syntax
```
### 📊 Available Modes
#### 🌐 Directory Mode (`dir`)
Enumerate directories and files on web servers.
**Basic Usage:**
```bash
gobuster dir -u https://example.com -w wordlist.txt
```
**Advanced Options:**
```bash
# With file extensions
gobuster dir -u https://example.com -w wordlist.txt -x php,html,js,txt
# With custom headers and cookies
gobuster dir -u https://example.com -w wordlist.txt -H "Authorization: Bearer token" -c "session=value"
# Show response length
gobuster dir -u https://example.com -w wordlist.txt -l
# Filter by status codes
gobuster dir -u https://example.com -w wordlist.txt -s 200,301,302
```
#### 🔍 DNS Mode (`dns`)
Discover subdomains through DNS resolution.
**Basic Usage:**
```bash
gobuster dns -do example.com -w wordlist.txt
```
**Advanced Options:**
```bash
# Use custom DNS server
gobuster dns -do example.com -w wordlist.txt -r 8.8.8.8:53
# Increase threads for faster scanning
gobuster dns -do example.com -w wordlist.txt -t 50
```
#### 🏠 Virtual Host Mode (`vhost`)
Discover virtual hosts on web servers.
**Basic Usage:**
```bash
gobuster vhost -u https://example.com --append-domain -w wordlist.txt
```
#### ☁️ S3 Mode (`s3`)
Enumerate Amazon S3 buckets.
**Basic Usage:**
```bash
gobuster s3 -w bucket-names.txt
```
**With Debug Output:**
```bash
gobuster s3 -w bucket-names.txt --debug
```
#### 🖥️ TFTP Mode (`tftp`)
Enumerate files on tftp servers.
**Basic Usage:**
```bash
gobuster tftp -s 10.0.0.1 -w wordlist.txt
```
#### ☁️ GCS Mode (`gcs`)
Enumerate Google Cloud Storage Buckets.
**Basic Usage:**
```bash
gobuster gcs -w bucket-names.txt
```
**With Debug Output:**
```bash
gobuster gcs -w bucket-names.txt --debug
```
#### 🔧 Fuzz Mode (`fuzz`)
Custom fuzzing with the `FUZZ` keyword.
**Basic Usage:**
```bash
gobuster fuzz -u https://example.com?FUZZ=test -w wordlist.txt
```
**Advanced Examples:**
```bash
# Fuzz URL parameters
gobuster fuzz -u https://example.com?param=FUZZ -w wordlist.txt
# Fuzz headers
gobuster fuzz -u https://example.com -H "X-Custom-Header: FUZZ" -w wordlist.txt
# Fuzz POST data
gobuster fuzz -u https://example.com -d "username=admin&password=FUZZ" -w passwords.txt
```
## 💰 Support
[](https://opencollective.com/gobuster) [](https://opencollective.com/gobuster)
### Love this tool? Back it!
If you're backing us already, you rock. If you're not, that's cool too! Want to back us? [Become a backer](https://opencollective.com/gobuster#backer)!
[](https://opencollective.com/gobuster#backers)
All funds that are donated to this project will be donated to charity. A full log of charity donations will be available in this repository as they are processed.
## 💡 Common Use Cases
### Web Application Security Testing
```bash
# Comprehensive directory enumeration
gobuster dir -u https://target.com -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php,html,js,txt,asp,aspx,jsp
# API endpoint discovery
gobuster dir -u https://api.target.com -w /usr/share/wordlists/dirb/common.txt -x json
# Admin panel discovery
gobuster dir -u https://target.com -w admin-panels.txt -s 200,301,302,403
```
### DNS Reconnaissance
```bash
# Comprehensive subdomain enumeration
gobuster dns -do target.com -w /usr/share/wordlists/dnsrecon/subdomains-top1mil-5000.txt -t 50
```
### Cloud Storage Assessment
```bash
# S3 bucket enumeration with patterns
gobuster s3 -w company-names.txt -v
# GCS bucket enumeration
gobuster gcs -w company-names.txt -v
```
## 🔧 Troubleshooting
### Common Issues
#### "Permission Denied" or "Access Denied"
- Try reducing thread count with `-t` flag
- Add delays between requests with `--delay`
- Use different user agent with `-a` flag
#### "Connection Timeout"
- Increase timeout with `--timeout` flag
- Reduce thread count for slower targets
- Check your internet connection
#### "No Results Found"
- Verify the target URL is accessible
- Try different wordlists
- Check status code filtering with `-s` flag
### Performance Issues
#### Slow Scanning
- Increase thread count with `-t` flag (but be careful not to overwhelm the target)
- Use smaller, more targeted wordlists
## 🎯 Best Practices
### Security Testing Guidelines
1. **Always get proper authorization** before testing any target
2. **Start with low thread counts** to avoid overwhelming servers
3. **Use appropriate wordlists** for the target technology
4. **Respect rate limits** and implement delays if needed
5. **Monitor your network traffic** to avoid detection
### Wordlist Selection
- **For web applications**: Use technology-specific wordlists (PHP, ASP.NET, etc.)
- **For APIs**: Focus on common API endpoints and versioning patterns
- **For DNS**: Use subdomain-specific wordlists with common patterns
- **For cloud storage**: Use company/brand-specific patterns
### Output Management
```bash
# Save results to file
gobuster dir -u https://example.com -w wordlist.txt -o results.txt
# Use quiet mode for clean output
gobuster dir -u https://example.com -w wordlist.txt -q
```
## 📚 Additional Resources
### Recommended Wordlists
- **SecLists**: [https://github.com/danielmiessler/SecLists](https://github.com/danielmiessler/SecLists)
- **FuzzDB**: [https://github.com/fuzzdb-project/fuzzdb](https://github.com/fuzzdb-project/fuzzdb)
- **Seclists DNS**: [https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS](https://github.com/danielmiessler/SecLists/tree/master/Discovery/DNS)
---
**Happy hacking! 🚀**
_Remember: Always test responsibly and with proper authorization._
# Changes
<details>
<summary>3.8.2</summary>
## 3.8.2
- Fix expanded mode to show the full url again
</details>
<details>
<summary>3.8.1</summary>
## 3.8.1
- Fix expanded mode showing the entries twice
</details>
<details>
<summary>3.8</summary>
## 3.8
- Add exclude-hostname-length flag to dynamically adjust exclude-length by @0xyy66
- Fix Fuzzing query parameters
- Add `--force` flag in `dir` mode to continue execution if precheck errors occur
</details>
<details>
<summary>3.7</summary>
## 3.7
- use new cli library
- a lot more short options due to the new cli library
- more user friendly error messages
- clean up DNS mode
- renamed `show-cname` to `check-cname` in dns mode
- got rid of `verbose` flag and introduced `debug` instead
- the version command now also shows some build variables for more info
- switched to another pkcs12 library to support p12s generated with openssl3 that use SHA256 HMAC
- comments in wordlists (strings starting with #) are no longer ignored
- warn in vhost mode if the --append-domain switch might have been forgotten
- allow to exclude status code and length in vhost mode
- added automaxprocs for use in docker with cpu limits
- log http requests with debug enabled
- allow fuzzing of Host header in fuzz mode
- automatically disable progress output when output is redirected
- fix extra special characters when run with `--no-progress`
- warn when using vhost mode with a proxy and http based urls as this might not work as expected
- add `interface` and `local-ip` parameters to specify the outgoing interface for http requests
- add support for tls renegotiation
- fix progress with patterns by @acammack
- fix backup discovery by @acammack
- support tcp protocol on dns servers
- add support for URL query parameters
</details>
<details>
<summary>3.6</summary>
## 3.6
- Wordlist offset parameter to skip x lines from the wordlist
- prevent double slashes when building up an url in dir mode
- allow for multiple values and ranges on `--exclude-length`
- `no-fqdn` parameter on dns bruteforce to disable the use of the systems search domains. This should speed up the run if you have configured some search domains. [https://github.com/OJ/gobuster/pull/418](https://github.com/OJ/gobuster/pull/418)
</details>
<details>
<summary>3.5</summary>
## 3.5
- Allow Ranges in status code and status code blacklist. Example: 200,300-305,404
</details>
<details>
<summary>3.4</summary>
## 3.4
- Enable TLS1.0 and TLS1.1 support
- Add TFTP mode to search for files on tftp servers
</details>
<details>
<summary>3.3</summary>
## 3.3
- Support TLS client certificates / mtls
- support loading extensions from file
- support fuzzing POST body, HTTP headers and basic auth
- new option to not canonicalize header names
</details>
<details>
<summary>3.2</summary>
## 3.2
- Use go 1.19
- use contexts in the correct way
- get rid of the wildcard flag (except in DNS mode)
- color output
- retry on timeout
- google cloud bucket enumeration
- fix nil reference errors
</details>
<details>
<summary>3.1</summary>
## 3.1
- enumerate public AWS S3 buckets
- fuzzing mode
- specify HTTP method
- added support for patterns. You can now specify a file containing patterns that are applied to every word, one by line. Every occurrence of the term `{GOBUSTER}` in it will be replaced with the current wordlist item. Please use with caution as this can cause increase the number of requests issued a lot.
- The shorthand `p` flag which was assigned to proxy is now used by the pattern flag
</details>
<details>
<summary>3.0</summary>
## 3.0
- New CLI options so modes are strictly separated (`-m` is now gone!)
- Performance Optimizations and better connection handling
- Ability to enumerate vhost names
- Option to supply custom HTTP headers
</details>
================================================
FILE: Taskfile.yml
================================================
version: "3"
vars:
PROGRAM: gobuster
tasks:
deps:
cmds:
- go mod tidy -v
update:
cmds:
- go get -u
- go get -u tool
- go mod tidy -v
check:
cmds:
- go fmt ./...
- go tool gofumpt -l -w .
- go vet ./...
build:
aliases: [default]
deps: [deps, check]
cmds:
- go build -o {{.OUTPUT_FILE | default .PROGRAM}}
env:
CGO_ENABLED: 0
GOOS: '{{.GOOS | default "linux"}}'
GOARCH: '{{.GOARCH | default "amd64"}}'
linux:
cmds:
- task: build
vars:
GOOS: linux
GOARCH: amd64
windows:
cmds:
- task: build
vars:
OUTPUT_FILE: "{{.PROGRAM}}.exe"
GOOS: windows
GOARCH: amd64
test:
deps: [deps, check]
env:
CGO_ENABLED: 1
cmds:
- go test -race -cover ./...
lint:
cmds:
- golangci-lint run ./... --timeout=30m
- go mod tidy
lint-update:
cmds:
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b {{ .GOPATH }}/bin
- golangci-lint --version
vars:
GOPATH:
sh: go env GOPATH
tag:
cmds:
- git tag -a "${TAG}" -m "${TAG}"
- git push origin "${TAG}"
preconditions:
- sh: '[[ -n "${TAG}" ]]'
msg: "Please set the TAG environment variable"
================================================
FILE: cli/const.go
================================================
//go:build !windows
package cli
const (
TerminalClearLine = "\r\x1b[2K"
)
================================================
FILE: cli/const_windows.go
================================================
//go:build windows
package cli
const (
TerminalClearLine = "\r\r"
)
================================================
FILE: cli/dir/dir.go
================================================
package dir
import (
"errors"
"fmt"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterdir"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "dir",
Usage: "Uses directory/file enumeration mode",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, internalcli.CommonHTTPOptions()...)
flags = append(flags, internalcli.GlobalOptions()...)
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "status-codes", Aliases: []string{"s"}, Usage: "Positive status codes (will be overwritten with status-codes-blacklist if set). Can also handle ranges like 200,300-400,404"},
&cli.StringFlag{Name: "status-codes-blacklist", Aliases: []string{"b"}, Usage: "Negative status codes (will override status-codes if set). Can also handle ranges like 200,300-400,404.", Value: "404"},
&cli.StringFlag{Name: "extensions", Aliases: []string{"x"}, Usage: "File extension(s) to search for"},
&cli.StringFlag{Name: "extensions-file", Aliases: []string{"X"}, Usage: "Read file extension(s) to search from the file"},
&cli.BoolFlag{Name: "expanded", Aliases: []string{"e"}, Value: false, Usage: "Expanded mode, print full URLs"},
&cli.BoolFlag{Name: "no-status", Aliases: []string{"n"}, Value: false, Usage: "Don't print status codes"},
&cli.BoolFlag{Name: "hide-length", Aliases: []string{"hl"}, Value: false, Usage: "Hide the length of the body in the output"},
&cli.BoolFlag{Name: "add-slash", Aliases: []string{"f"}, Value: false, Usage: "Append / to each request"},
&cli.BoolFlag{Name: "discover-backup", Aliases: []string{"db"}, Value: false, Usage: "Upon finding a file search for backup files by appending multiple backup extensions"},
&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
&cli.BoolFlag{Name: "force", Value: false, Usage: "Continue even if the prechecks fail. Please only use this if you know what you are doing, it can lead to unexpected results."},
}...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobusterdir.NewOptions()
httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
if err != nil {
return err
}
pluginOpts.HTTPOptions = httpOptions
pluginOpts.Extensions = c.String("extensions")
ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions)
if err != nil {
return fmt.Errorf("invalid value for extensions: %w", err)
}
pluginOpts.ExtensionsParsed = ret
pluginOpts.ExtensionsFile = c.String("extensions-file")
if pluginOpts.ExtensionsFile != "" {
extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile)
if err != nil {
return fmt.Errorf("invalid value for extensions file: %w", err)
}
pluginOpts.ExtensionsParsed.AddRange(extensions)
}
pluginOpts.StatusCodes = c.String("status-codes")
ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes)
if err != nil {
return fmt.Errorf("invalid value for status-codes: %w", err)
}
pluginOpts.StatusCodesParsed = ret2
pluginOpts.StatusCodesBlacklist = c.String("status-codes-blacklist")
ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist)
if err != nil {
return fmt.Errorf("invalid value for status-codes-blacklist: %w", err)
}
pluginOpts.StatusCodesBlacklistParsed = ret3
if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" {
return 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",
pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist)
}
if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" {
return errors.New("status-codes and status-codes-blacklist are both not set, please set one")
}
pluginOpts.UseSlash = c.Bool("add-slash")
pluginOpts.Expanded = c.Bool("expanded")
pluginOpts.NoStatus = c.Bool("no-status")
pluginOpts.HideLength = c.Bool("hide-length")
pluginOpts.DiscoverBackup = c.Bool("discover-backup")
pluginOpts.Force = c.Bool("force")
pluginOpts.ExcludeLength = c.String("exclude-length")
ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret4
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
log := libgobuster.NewLogger(globalOpts.Debug)
plugin, err := gobusterdir.New(&globalOpts, pluginOpts, log)
if err != nil {
return fmt.Errorf("error on creating gobusterdir: %w", err)
}
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
var wErr *gobusterdir.WildcardError
if errors.As(err, &wErr) {
return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr)
}
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
}
return nil
}
================================================
FILE: cli/dir/dir_test.go
================================================
package dir
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterdir"
"github.com/OJ/gobuster/v3/libgobuster"
)
func httpServer(b *testing.B, content string) *httptest.Server {
b.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := fmt.Fprint(w, content); err != nil {
b.Fatalf("%v", err)
}
}))
return ts
}
func BenchmarkDirMode(b *testing.B) {
h := httpServer(b, "test")
defer h.Close()
u, err := url.Parse(h.URL)
if err != nil {
b.Fatalf("could not parse URL: %v", err)
}
pluginopts := gobusterdir.NewOptions()
pluginopts.URL = u
pluginopts.Timeout = 10 * time.Second
pluginopts.Extensions = ".php,.csv"
tmpExt, err := libgobuster.ParseExtensions(pluginopts.Extensions)
if err != nil {
b.Fatalf("could not parse extensions: %v", err)
}
pluginopts.ExtensionsParsed = tmpExt
pluginopts.StatusCodes = "200,204,301,302,307,401,403"
tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes)
if err != nil {
b.Fatalf("could not parse status codes: %v", err)
}
pluginopts.StatusCodesParsed = tmpStat
wordlist, err := os.CreateTemp(b.TempDir(), "")
if err != nil {
b.Fatalf("could not create tempfile: %v", err)
}
defer os.Remove(wordlist.Name())
for w := range 1000 {
_, _ = fmt.Fprintf(wordlist, "%d\n", w)
}
if err := wordlist.Close(); err != nil {
b.Fatalf("%v", err)
}
globalopts := libgobuster.Options{
Threads: 10,
Wordlist: wordlist.Name(),
NoProgress: true,
}
ctx := b.Context()
oldStdout := os.Stdout
oldStderr := os.Stderr
defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
devnull, err := os.Open(os.DevNull)
if err != nil {
b.Fatalf("could not get devnull %v", err)
}
defer devnull.Close()
log := libgobuster.NewLogger(false)
// Run the real benchmark
for b.Loop() {
os.Stdout = devnull
os.Stderr = devnull
plugin, err := gobusterdir.New(&globalopts, pluginopts, log)
if err != nil {
b.Fatalf("error on creating gobusterdir: %v", err)
}
if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
b.Fatalf("error on running gobuster: %v", err)
}
os.Stdout = oldStdout
os.Stderr = oldStderr
}
}
================================================
FILE: cli/dns/dns.go
================================================
package dns
import (
"errors"
"fmt"
"runtime"
"time"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterdns"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "dns",
Usage: "Uses DNS subdomain enumeration mode",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "domain", Aliases: []string{"do"}, Usage: "The target domain", Required: true},
&cli.BoolFlag{Name: "check-cname", Aliases: []string{"c"}, Value: false, Usage: "Also check CNAME records"},
&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 1 * time.Second, Usage: "DNS resolver timeout"},
&cli.BoolFlag{Name: "wildcard", Aliases: []string{"wc"}, Value: false, Usage: "Force continued operation when wildcard found"},
&cli.BoolFlag{Name: "no-fqdn", Aliases: []string{"nf"}, Value: false, Usage: "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain"},
&cli.StringFlag{Name: "resolver", Usage: "Use custom DNS server (format server.com or server.com:port)"},
&cli.StringFlag{Name: "protocol", Value: "udp", Usage: "Use either 'udp' or 'tcp' as protocol on the custom resolver"},
}...)
flags = append(flags, internalcli.GlobalOptions()...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobusterdns.NewOptions()
pluginOpts.Domain = c.String("domain")
pluginOpts.CheckCNAME = c.Bool("check-cname")
pluginOpts.Timeout = c.Duration("timeout")
pluginOpts.WildcardForced = c.Bool("wildcard")
pluginOpts.NoFQDN = c.Bool("no-fqdn")
pluginOpts.Resolver = c.String("resolver")
pluginOpts.Protocol = c.String("protocol")
if pluginOpts.Resolver != "" && runtime.GOOS == "windows" {
return errors.New("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution")
}
if pluginOpts.Protocol != "udp" && pluginOpts.Protocol != "tcp" {
return errors.New("protocol must be either 'udp' or 'tcp'")
}
if pluginOpts.Protocol != "udp" && pluginOpts.Resolver == "" {
return errors.New("custom protocol can only be set if a custom resolver is set")
}
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
plugin, err := gobusterdns.New(&globalOpts, pluginOpts)
if err != nil {
return fmt.Errorf("error on creating gobusterdns: %w", err)
}
log := libgobuster.NewLogger(globalOpts.Debug)
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
var wErr *gobusterdns.WildcardError
if errors.As(err, &wErr) {
return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr)
}
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Domain, err)
}
return nil
}
================================================
FILE: cli/fuzz/fuzz.go
================================================
package fuzz
import (
"fmt"
"strings"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusterfuzz"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "fuzz",
Usage: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword),
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, internalcli.CommonHTTPOptions()...)
flags = append(flags, internalcli.GlobalOptions()...)
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "exclude-statuscodes", Aliases: []string{"b"}, Usage: "Excluded status codes. Can also handle ranges like 200,300-400,404."},
&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206"},
&cli.StringFlag{Name: "body", Aliases: []string{"B"}, Usage: "Request body"},
}...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobusterfuzz.NewOptions()
httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
if err != nil {
return err
}
pluginOpts.HTTPOptions = httpOptions
pluginOpts.ExcludedStatusCodes = c.String("exclude-statuscodes")
ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes)
if err != nil {
return fmt.Errorf("invalid value for excludestatuscodes: %w", err)
}
pluginOpts.ExcludedStatusCodesParsed = ret
pluginOpts.ExcludeLength = c.String("exclude-length")
ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret2
pluginOpts.RequestBody = c.String("body")
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
if !containsFuzzKeyword(*pluginOpts) {
return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword)
}
log := libgobuster.NewLogger(globalOpts.Debug)
plugin, err := gobusterfuzz.New(&globalOpts, pluginOpts, log)
if err != nil {
return fmt.Errorf("error on creating gobusterfuzz: %w", err)
}
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
}
return nil
}
func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool {
if strings.Contains(pluginopts.URL.String(), gobusterfuzz.FuzzKeyword) {
return true
}
if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) {
return true
}
for _, h := range pluginopts.Headers {
if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) {
return true
}
}
if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) {
return true
}
if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) {
return true
}
return false
}
================================================
FILE: cli/gcs/gcs.go
================================================
package gcs
import (
"fmt"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobustergcs"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "gcs",
Usage: "Uses gcs bucket enumeration mode",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.IntFlag{Name: "max-files", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"},
&cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"},
}...)
flags = append(flags, internalcli.GlobalOptions()...)
flags = append(flags, internalcli.BasicHTTPOptions()...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobustergcs.NewOptions()
httpOptions, err := internalcli.ParseBasicHTTPOptions(c)
if err != nil {
return err
}
pluginOpts.BasicHTTPOptions = httpOptions
pluginOpts.MaxFilesToList = c.Int("max-files")
pluginOpts.ShowFiles = c.Bool("show-files")
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
log := libgobuster.NewLogger(globalOpts.Debug)
plugin, err := gobustergcs.New(&globalOpts, pluginOpts, log)
if err != nil {
return fmt.Errorf("error on creating gobustergcs: %w", err)
}
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
================================================
FILE: cli/gobuster.go
================================================
package cli
import (
"context"
"errors"
"fmt"
"math"
"os"
"strings"
"sync"
"time"
"github.com/OJ/gobuster/v3/libgobuster"
)
const (
ruler = "==============================================================="
cliProgressUpdate = 500 * time.Millisecond
)
// 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.
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
defer wg.Done()
var f *os.File
var err error
if filename != "" {
f, err = os.Create(filename)
if err != nil {
g.Logger.Fatalf("error on creating output file: %v", err)
}
defer f.Close()
}
for r := range g.Progress.ResultChan {
s, err := r.ResultToString()
if err != nil {
g.Logger.Fatal(err)
}
if s != "" {
s = strings.TrimSpace(s)
if g.Opts.NoProgress || g.Opts.Quiet {
_, _ = fmt.Printf("%s\n", s) // nolint forbidigo
} else {
// only print the clear line when progress output is enabled
_, _ = fmt.Printf("%s%s\n", TerminalClearLine, s) // nolint forbidigo
}
if f != nil {
err = writeToFile(f, s)
if err != nil {
g.Logger.Fatalf("error on writing output file: %v", err)
}
}
}
}
}
// 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.
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for e := range g.Progress.ErrorChan {
if !g.Opts.Quiet && !g.Opts.NoError {
g.Logger.Error(e.Error())
g.Logger.Debugf("%#v", e)
}
}
}
// messageWorker outputs messages 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.
func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for msg := range g.Progress.MessageChan {
if !g.Opts.Quiet {
switch msg.Level {
case libgobuster.LevelDebug:
g.Logger.Debug(msg.Message)
case libgobuster.LevelError:
g.Logger.Error(msg.Message)
case libgobuster.LevelWarn:
g.Logger.Warn(msg.Message)
case libgobuster.LevelInfo:
g.Logger.Info(msg.Message)
default:
panic(fmt.Sprintf("invalid level %d", msg.Level))
}
}
}
}
func printProgress(g *libgobuster.Gobuster) {
requestsIssued := g.Progress.RequestsIssued()
requestsExpected := g.Progress.RequestsExpected()
if requestsExpected == 0 {
requestsExpected = 1 // avoid division by zero
}
percent := float32(requestsIssued) * 100.0 / float32(requestsExpected)
if math.IsNaN(float64(percent)) {
percent = 0.0
}
s := fmt.Sprintf("%sProgress: %d / %d (%3.2f%%)", TerminalClearLine, requestsIssued, requestsExpected, percent)
_, _ = fmt.Fprint(os.Stderr, s)
}
// progressWorker outputs the progress every tick. It will stop once cancel() is called
// on the context
func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
tick := time.NewTicker(cliProgressUpdate)
for {
select {
case <-tick.C:
printProgress(g)
case <-ctx.Done():
// print the final progress so we end at 100%
printProgress(g)
fmt.Println() // nolint:forbidigo
return
}
}
}
func writeToFile(f *os.File, output string) error {
_, err := fmt.Fprintf(f, "%s\n", output)
if err != nil {
return fmt.Errorf("[!] Unable to write to file %w", err)
}
return nil
}
// Gobuster is the main entry point for the CLI
func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster.GobusterPlugin, log *libgobuster.Logger) error {
// Sanity checks
if opts == nil {
return errors.New("please provide valid options")
}
if plugin == nil {
return errors.New("please provide a valid plugin")
}
ctxCancel, cancel := context.WithCancel(ctx)
defer cancel()
gobuster, err := libgobuster.NewGobuster(opts, plugin, log)
if err != nil {
return err
}
if !opts.Quiet {
log.Println(ruler)
log.Printf("Gobuster v%s\n", libgobuster.VERSION)
log.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)")
log.Println(ruler)
c, err := gobuster.GetConfigString()
if err != nil {
return fmt.Errorf("error on creating config string: %w", err)
}
log.Println(c)
log.Println(ruler)
gobuster.Logger.Printf("Starting gobuster in %s mode", plugin.Name())
if opts.WordlistOffset > 0 {
gobuster.Logger.Printf("Skipping the first %d elements...", opts.WordlistOffset)
}
log.Println(ruler)
}
fi, err := os.Stdout.Stat()
if err != nil {
return err
}
// check if we are not in a terminal. If so, disable output
if (fi.Mode() & os.ModeCharDevice) != os.ModeCharDevice {
opts.NoProgress = true
}
// our waitgroup for all goroutines
// this ensures all goroutines are finished
// when we call wg.Wait()
var wg sync.WaitGroup
wg.Add(1)
go resultWorker(gobuster, opts.OutputFilename, &wg)
wg.Add(1)
go errorWorker(gobuster, &wg)
wg.Add(1)
go messageWorker(gobuster, &wg)
if !opts.Quiet && !opts.NoProgress {
// if not quiet add a new workgroup entry and start the goroutine
wg.Add(1)
go progressWorker(ctxCancel, gobuster, &wg)
}
err = gobuster.Run(ctxCancel)
// call cancel func so progressWorker will exit (the only goroutine in this
// file using the context) and to free resources
cancel()
// wait for all spun up goroutines to finish (all have to call wg.Done())
wg.Wait()
// Late error checking to finish all threads
if err != nil {
return err
}
if !opts.Quiet {
log.Println(ruler)
gobuster.Logger.Println("Finished")
log.Println(ruler)
}
return nil
}
================================================
FILE: cli/options.go
================================================
package cli
import (
"bufio"
"crypto/tls"
"errors"
"fmt"
"net"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/fatih/color"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"software.sslmate.com/src/go-pkcs12"
)
func BasicHTTPOptions() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "useragent", Aliases: []string{"a"}, Value: libgobuster.DefaultUserAgent(), Usage: "Set the User-Agent string"},
&cli.BoolFlag{Name: "random-agent", Aliases: []string{"rua"}, Value: false, Usage: "Use a random User-Agent string"},
&cli.StringFlag{Name: "proxy", Usage: "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]"},
&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 10 * time.Second, Usage: "HTTP Timeout"},
&cli.BoolFlag{Name: "no-tls-validation", Aliases: []string{"k"}, Value: false, Usage: "Skip TLS certificate verification"},
&cli.BoolFlag{Name: "retry", Value: false, Usage: "Should retry on request timeout"},
&cli.IntFlag{Name: "retry-attempts", Aliases: []string{"ra"}, Value: 3, Usage: "Times to retry on request timeout"},
&cli.StringFlag{Name: "client-cert-pem", Aliases: []string{"ccp"}, Usage: "public key in PEM format for optional TLS client certificates]"},
&cli.StringFlag{Name: "client-cert-pem-key", Aliases: []string{"ccpk"}, Usage: "private key in PEM format for optional TLS client certificates (this key needs to have no password)"},
&cli.StringFlag{Name: "client-cert-p12", Aliases: []string{"ccp12"}, Usage: "a p12 file to use for options TLS client certificates"},
&cli.StringFlag{Name: "client-cert-p12-password", Aliases: []string{"ccp12p"}, Usage: "the password to the p12 file"},
&cli.BoolFlag{Name: "tls-renegotiation", Value: false, Usage: "Enable TLS renegotiation"},
&cli.StringFlag{Name: "interface", Aliases: []string{"iface"}, Usage: "specify network interface to use. Can't be used with local-ip"},
&cli.StringFlag{Name: "local-ip", Usage: "specify local ip of network interface to use. Can't be used with interface"},
}
}
func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions, error) {
var opts libgobuster.BasicHTTPOptions
opts.UserAgent = c.String("useragent")
randomUA := c.Bool("random-agent")
if randomUA {
ua, err := libgobuster.GetRandomUserAgent()
if err != nil {
return opts, err
}
opts.UserAgent = ua
}
opts.Proxy = c.String("proxy")
opts.Timeout = c.Duration("timeout")
opts.NoTLSValidation = c.Bool("no-tls-validation")
opts.RetryOnTimeout = c.Bool("retry")
opts.RetryAttempts = c.Int("retry-attempts")
pemFile := c.String("client-cert-pem")
pemKeyFile := c.String("client-cert-pem-key")
p12File := c.String("client-cert-p12")
p12Pass := c.String("client-cert-p12-password")
if pemFile != "" && p12File != "" {
return opts, errors.New("please supply either a pem or a p12, not both")
}
if pemFile != "" {
cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile)
if err != nil {
return opts, fmt.Errorf("could not load supplied pem key: %w", err)
}
opts.TLSCertificate = &cert
} else if p12File != "" {
p12Content, err := os.ReadFile(p12File)
if err != nil {
return opts, fmt.Errorf("could not read p12 %s: %w", p12File, err)
}
privKey, pubKey, _, err := pkcs12.DecodeChain(p12Content, p12Pass)
if err != nil {
return opts, fmt.Errorf("could not load P12: %w", err)
}
opts.TLSCertificate = &tls.Certificate{
Certificate: [][]byte{pubKey.Raw},
PrivateKey: privKey,
}
}
opts.TLSRenegotiation = c.Bool("tls-renegotiation")
iface := c.String("interface")
localIP := c.String("local-ip")
if iface != "" && localIP != "" {
return opts, errors.New("can not set both interface and local-ip")
}
switch {
case iface != "":
a, err := getLocalAddrFromInterface(iface)
if err != nil {
return opts, err
}
opts.LocalAddr = a
case localIP != "":
if !strings.Contains(localIP, ":") {
localIP = fmt.Sprintf("%s:0", localIP)
}
a, err := net.ResolveIPAddr("ip", localIP)
if err != nil {
return opts, err
}
localTCPAddr := net.TCPAddr{
IP: a.IP,
}
opts.LocalAddr = &localTCPAddr
}
return opts, nil
}
func CommonHTTPOptions() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "url", Aliases: []string{"u"}, Usage: "The target URL", Required: true},
&cli.StringFlag{Name: "cookies", Aliases: []string{"c"}, Usage: "Cookies to use for the requests"},
&cli.StringFlag{Name: "username", Aliases: []string{"U"}, Usage: "Username for Basic Auth"},
&cli.StringFlag{Name: "password", Aliases: []string{"P"}, Usage: "Password for Basic Auth"},
&cli.BoolFlag{Name: "follow-redirect", Aliases: []string{"r"}, Value: false, Usage: "Follow redirects"},
&cli.StringSliceFlag{Name: "headers", Aliases: []string{"H"}, Usage: "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'"},
&cli.BoolFlag{Name: "no-canonicalize-headers", Aliases: []string{"nch"}, Value: false, Usage: "Do not canonicalize HTTP header names. If set header names are sent as is"},
&cli.StringFlag{Name: "method", Aliases: []string{"m"}, Value: "GET", Usage: "the password to the p12 file"},
}...)
flags = append(flags, BasicHTTPOptions()...)
return flags
}
func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, error) {
var opts libgobuster.HTTPOptions
basic, err := ParseBasicHTTPOptions(c)
if err != nil {
return opts, err
}
opts.BasicHTTPOptions = basic
urlInput := c.String("url")
if !strings.HasPrefix(urlInput, "http") {
// check to see if a port was specified
re := regexp.MustCompile(`^[^/]+:(\d+)`)
match := re.FindStringSubmatch(urlInput)
if len(match) < 2 {
// no port, default to http on 80
urlInput = fmt.Sprintf("http://%s", urlInput)
} else {
port, err2 := strconv.Atoi(match[1])
switch {
case err2 != nil || (port != 80 && port != 443):
return opts, errors.New("url scheme not specified")
case port == 80:
urlInput = fmt.Sprintf("http://%s", urlInput)
default:
urlInput = fmt.Sprintf("https://%s", urlInput)
}
}
}
if opts.URL, err = url.Parse(urlInput); err != nil {
return opts, fmt.Errorf("url %q is not valid: %w", urlInput, err)
}
opts.Cookies = c.String("cookies")
opts.Username = c.String("username")
opts.Password = c.String("password")
// Prompt for PW if not provided
if opts.Username != "" && opts.Password == "" {
fmt.Printf("[?] Auth Password: ") // nolint:forbidigo
// please don't remove the int cast here as it is sadly needed on windows :/
passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
// print a newline to simulate the newline that was entered
// this means that formatting/printing after doesn't look bad.
fmt.Println("") // nolint:forbidigo
if err != nil {
return opts, errors.New("username given but reading of password failed")
}
opts.Password = string(passBytes)
}
// if it's still empty bail out
if opts.Username != "" && opts.Password == "" {
return opts, errors.New("username was provided but password is missing")
}
opts.FollowRedirect = c.Bool("follow-redirect")
opts.NoCanonicalizeHeaders = c.Bool("no-canonicalize-headers")
opts.Method = c.String("method")
for _, h := range c.StringSlice("headers") {
keyAndValue := strings.SplitN(h, ":", 2)
if len(keyAndValue) != 2 {
return opts, fmt.Errorf("invalid header format for header %q", h)
}
key := strings.TrimSpace(keyAndValue[0])
value := strings.TrimSpace(keyAndValue[1])
if len(key) == 0 {
return opts, fmt.Errorf("invalid header format for header %q - name is empty", h)
}
header := libgobuster.HTTPHeader{Name: key, Value: value}
opts.Headers = append(opts.Headers, header)
}
return opts, nil
}
func GlobalOptions() []cli.Flag {
return []cli.Flag{
&cli.StringFlag{Name: "wordlist", Aliases: []string{"w"}, Usage: "Path to the wordlist. Set to - to use STDIN.", Required: true},
&cli.DurationFlag{Name: "delay", Aliases: []string{"d"}, Usage: "Time each thread waits between requests (e.g. 1500ms)"},
&cli.IntFlag{Name: "threads", Aliases: []string{"t"}, Value: 10, Usage: "Number of concurrent threads"},
&cli.IntFlag{Name: "wordlist-offset", Aliases: []string{"wo"}, Value: 0, Usage: "Resume from a given position in the wordlist"},
&cli.StringFlag{Name: "output", Aliases: []string{"o"}, Usage: "Output file to write results to (defaults to stdout)"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, Value: false, Usage: "Don't print the banner and other noise"},
&cli.BoolFlag{Name: "no-progress", Aliases: []string{"np"}, Value: false, Usage: "Don't display progress"},
&cli.BoolFlag{Name: "no-error", Aliases: []string{"ne"}, Value: false, Usage: "Don't display errors"},
&cli.StringFlag{Name: "pattern", Aliases: []string{"p"}, Usage: "File containing replacement patterns"},
&cli.StringFlag{Name: "discover-pattern", Aliases: []string{"pd"}, Usage: "File containing replacement patterns applied to successful guesses"},
&cli.BoolFlag{Name: "no-color", Aliases: []string{"nc"}, Value: false, Usage: "Disable color output"},
&cli.BoolFlag{Name: "debug", Value: false, Usage: "enable debug output"},
}
}
func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) {
var opts libgobuster.Options
opts.Wordlist = c.String("wordlist")
if opts.Wordlist == "-" { // nolint:revive
// STDIN
} else if _, err := os.Stat(opts.Wordlist); os.IsNotExist(err) {
return opts, fmt.Errorf("wordlist file %q does not exist: %w", opts.Wordlist, err)
}
opts.Delay = c.Duration("delay")
opts.Threads = c.Int("threads")
opts.WordlistOffset = c.Int("wordlist-offset")
if opts.Wordlist == "-" && opts.WordlistOffset > 0 {
return opts, errors.New("wordlist-offset is not supported when reading from STDIN")
} else if opts.WordlistOffset < 0 {
return opts, errors.New("wordlist-offset must be bigger or equal to 0")
}
opts.OutputFilename = c.String("output")
opts.Quiet = c.Bool("quiet")
opts.NoProgress = c.Bool("no-progress")
opts.NoError = c.Bool("no-error")
opts.PatternFile = c.String("pattern")
if opts.PatternFile != "" {
if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) {
return opts, fmt.Errorf("pattern file %q does not exist: %w", opts.PatternFile, err)
}
patternFile, err := os.Open(opts.PatternFile)
if err != nil {
return opts, fmt.Errorf("could not open pattern file %q: %w", opts.PatternFile, err)
}
defer patternFile.Close()
scanner := bufio.NewScanner(patternFile)
for scanner.Scan() {
opts.Patterns = append(opts.Patterns, scanner.Text())
}
if err := scanner.Err(); err != nil {
return opts, fmt.Errorf("could not read pattern file %q: %w", opts.PatternFile, err)
}
}
opts.DiscoverPatternFile = c.String("discover-pattern")
if opts.DiscoverPatternFile != "" {
if _, err := os.Stat(opts.PatternFile); os.IsNotExist(err) {
return opts, fmt.Errorf("discover pattern file %q does not exist: %w", opts.DiscoverPatternFile, err)
}
discoverPatternFile, err := os.Open(opts.DiscoverPatternFile)
if err != nil {
return opts, fmt.Errorf("could not open discover pattern file %q: %w", opts.DiscoverPatternFile, err)
}
defer discoverPatternFile.Close()
scanner := bufio.NewScanner(discoverPatternFile)
for scanner.Scan() {
opts.DiscoverPatterns = append(opts.DiscoverPatterns, scanner.Text())
}
if err := scanner.Err(); err != nil {
return opts, fmt.Errorf("could not read discover pattern file %q: %w", opts.DiscoverPatternFile, err)
}
}
if c.Bool("no-color") {
color.NoColor = true
}
opts.Debug = c.Bool("debug")
return opts, nil
}
func getLocalAddrFromInterface(iface string) (*net.TCPAddr, error) {
i, err := net.InterfaceByName(iface)
if err != nil {
return nil, fmt.Errorf("could not get interface %s: %w", iface, err)
}
addrs, err := i.Addrs()
if err != nil {
return nil, fmt.Errorf("could not get local addrs for iface %s: %w", i.Name, err)
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no ip addresses on interface %s", iface)
}
tmp, ok := addrs[0].(*net.IPNet)
if !ok {
return nil, fmt.Errorf("could not get ipnet address from interface %s", iface)
}
tcpAddr := &net.TCPAddr{
IP: tmp.IP,
}
return tcpAddr, err
}
================================================
FILE: cli/s3/s3.go
================================================
package s3
import (
"fmt"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobusters3"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "s3",
Usage: "Uses aws bucket enumeration mode",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.IntFlag{Name: "max-files", Aliases: []string{"m"}, Value: 5, Usage: "max files to list when listing buckets"},
&cli.BoolFlag{Name: "show-files", Aliases: []string{"s"}, Value: true, Usage: "show files from found buckets"},
}...)
flags = append(flags, internalcli.GlobalOptions()...)
flags = append(flags, internalcli.BasicHTTPOptions()...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobusters3.NewOptions()
httpOptions, err := internalcli.ParseBasicHTTPOptions(c)
if err != nil {
return err
}
pluginOpts.BasicHTTPOptions = httpOptions
pluginOpts.MaxFilesToList = c.Int("max-files")
pluginOpts.ShowFiles = c.Bool("show-files")
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
log := libgobuster.NewLogger(globalOpts.Debug)
plugin, err := gobusters3.New(&globalOpts, pluginOpts, log)
if err != nil {
return fmt.Errorf("error on creating gobusters3: %w", err)
}
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
================================================
FILE: cli/tftp/tftp.go
================================================
package tftp
import (
"fmt"
"strings"
"time"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobustertftp"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "tftp",
Usage: "Uses TFTP enumeration mode",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, []cli.Flag{
&cli.StringFlag{Name: "server", Aliases: []string{"s"}, Usage: "The target TFTP server", Required: true},
&cli.DurationFlag{Name: "timeout", Aliases: []string{"to"}, Value: 1 * time.Second, Usage: "TFTP timeout"},
}...)
flags = append(flags, internalcli.GlobalOptions()...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobustertftp.NewOptions()
pluginOpts.Server = c.String("server")
if !strings.Contains(pluginOpts.Server, ":") {
pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server)
}
pluginOpts.Timeout = c.Duration("timeout")
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
plugin, err := gobustertftp.New(&globalOpts, pluginOpts)
if err != nil {
return fmt.Errorf("error on creating gobustertftp: %w", err)
}
log := libgobuster.NewLogger(globalOpts.Debug)
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.Server, err)
}
return nil
}
================================================
FILE: cli/vhost/vhost.go
================================================
package vhost
import (
"errors"
"fmt"
"strings"
internalcli "github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobustervhost"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/urfave/cli/v2"
)
func Command() *cli.Command {
cmd := cli.Command{
Name: "vhost",
Usage: "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)",
Action: run,
Flags: getFlags(),
}
return &cmd
}
func getFlags() []cli.Flag {
var flags []cli.Flag
flags = append(flags, internalcli.CommonHTTPOptions()...)
flags = append(flags, internalcli.GlobalOptions()...)
flags = append(flags, []cli.Flag{
&cli.BoolFlag{Name: "append-domain", Aliases: []string{"ad"}, Value: false, Usage: "Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist."},
&cli.StringFlag{Name: "exclude-length", Aliases: []string{"xl"}, Usage: "exclude the following content lengths. You can separate multiple lengths by comma and it also supports ranges like 203-206"},
&cli.StringFlag{Name: "exclude-status", Aliases: []string{"xs"}, Usage: "exclude the following status codes. Can also handle ranges like 200,300-400,404.", Value: ""},
&cli.StringFlag{Name: "domain", Aliases: []string{"do"}, Usage: "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"},
&cli.BoolFlag{Name: "force", Value: false, Usage: "Force execution even when result is not guaranteed."},
&cli.BoolFlag{Name: "exclude-hostname-length", Aliases: []string{"xh"}, Value: false, Usage: "Automatically adjust exclude-length based on dynamic hostname length in responses"},
}...)
return flags
}
func run(c *cli.Context) error {
pluginOpts := gobustervhost.NewOptions()
httpOptions, err := internalcli.ParseCommonHTTPOptions(c)
if err != nil {
return err
}
pluginOpts.HTTPOptions = httpOptions
pluginOpts.AppendDomain = c.Bool("append-domain")
pluginOpts.ExcludeLength = c.String("exclude-length")
ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret
pluginOpts.ExcludeHostnameLength = c.Bool("exclude-hostname-length")
if pluginOpts.ExcludeHostnameLength && pluginOpts.ExcludeLengthParsed.Length() == 0 {
return errors.New("exclude-hostname-length requires exclude-length to be set")
}
pluginOpts.ExcludeStatus = c.String("exclude-status")
ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeStatus)
if err != nil {
return fmt.Errorf("invalid value for exclude-status: %w", err)
}
pluginOpts.ExcludeStatusParsed = ret2
pluginOpts.Domain = c.String("domain")
globalOpts, err := internalcli.ParseGlobalOptions(c)
if err != nil {
return err
}
force := c.Bool("force")
if !force &&
(strings.HasPrefix(pluginOpts.Proxy, "http://") || strings.HasPrefix(pluginOpts.Proxy, "https://")) &&
strings.EqualFold(pluginOpts.URL.Scheme, "http") {
return errors.New("VHOST mode does not work with a http proxy when using plain text http urls as golang strictly adheres to the http standard. This results in always sending the requests to the IP of the VHOST domain instead of the specified target. See https://github.com/golang/go/issues/30775 for example. You need to either disable the proxy, use a https based url or use the --force switch to continue. When using --force you may need to do some rewrites in your proxy to get the expected result")
}
log := libgobuster.NewLogger(globalOpts.Debug)
plugin, err := gobustervhost.New(&globalOpts, pluginOpts, log)
if err != nil {
return fmt.Errorf("error on creating gobustervhost: %w", err)
}
if err := internalcli.Gobuster(c.Context, &globalOpts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster on %s: %w", pluginOpts.URL, err)
}
return nil
}
================================================
FILE: cli/vhost/vhost_test.go
================================================
package vhost
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
"time"
"github.com/OJ/gobuster/v3/cli"
"github.com/OJ/gobuster/v3/gobustervhost"
"github.com/OJ/gobuster/v3/libgobuster"
)
func httpServer(b *testing.B, content string) *httptest.Server {
b.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := fmt.Fprint(w, content); err != nil {
b.Fatalf("%v", err)
}
}))
return ts
}
func BenchmarkVhostMode(b *testing.B) {
h := httpServer(b, "test")
defer h.Close()
u, err := url.Parse(h.URL)
if err != nil {
b.Fatalf("could not parse URL %v: %v", h.URL, err)
}
pluginopts := gobustervhost.NewOptions()
pluginopts.URL = u
pluginopts.Timeout = 10 * time.Second
wordlist, err := os.CreateTemp(b.TempDir(), "")
if err != nil {
b.Fatalf("could not create tempfile: %v", err)
}
defer os.Remove(wordlist.Name())
for w := range 1000 {
_, _ = fmt.Fprintf(wordlist, "%d\n", w)
}
if err := wordlist.Close(); err != nil {
b.Fatalf("%v", err)
}
globalopts := libgobuster.Options{
Threads: 10,
Wordlist: wordlist.Name(),
NoProgress: true,
}
ctx := b.Context()
oldStdout := os.Stdout
oldStderr := os.Stderr
defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
devnull, err := os.Open(os.DevNull)
if err != nil {
b.Fatalf("could not get devnull %v", err)
}
defer devnull.Close()
log := libgobuster.NewLogger(false)
// Run the real benchmark
for b.Loop() {
os.Stdout = devnull
os.Stderr = devnull
plugin, err := gobustervhost.New(&globalopts, pluginopts, log)
if err != nil {
b.Fatalf("error on creating gobusterdir: %v", err)
}
if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
b.Fatalf("error on running gobuster: %v", err)
}
os.Stdout = oldStdout
os.Stderr = oldStderr
}
}
================================================
FILE: go.mod
================================================
module github.com/OJ/gobuster/v3
go 1.25
require (
github.com/fatih/color v1.18.0
github.com/google/uuid v1.6.0
github.com/pin/tftp/v3 v3.1.0
github.com/urfave/cli/v2 v2.27.7
go.uber.org/automaxprocs v1.6.0
golang.org/x/term v0.37.0
software.sslmate.com/src/go-pkcs12 v0.6.0
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/tools v0.36.0 // indirect
mvdan.cc/gofumpt v0.9.0 // indirect
)
tool mvdan.cc/gofumpt
================================================
FILE: go.sum
================================================
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg=
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
mvdan.cc/gofumpt v0.9.0 h1:W0wNHMSvDBDIyZsm3nnGbVfgp5AknzBrGJnfLCy501w=
mvdan.cc/gofumpt v0.9.0/go.mod h1:3xYtNemnKiXaTh6R4VtlqDATFwBbdXI8lJvH/4qk7mw=
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
================================================
FILE: gobusterdir/gobusterdir.go
================================================
package gobusterdir
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"syscall"
"text/tabwriter"
"unicode/utf8"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/google/uuid"
)
// nolint:gochecknoglobals
var (
backupExtensions = []string{"~", ".bak", ".bak2", ".old", ".1"}
backupDotExtensions = []string{".swp"}
)
// WildcardError is returned if a wildcard response is found
type WildcardError struct {
url string
location string
statusCode int
length int64
}
// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
var addInfo string
if e.location != "" {
addInfo = fmt.Sprintf("%s => %d (redirect to %s) (Length: %d)", e.url, e.statusCode, e.location, e.length)
} else {
addInfo = fmt.Sprintf("%s => %d (Length: %d)", e.url, e.statusCode, e.length)
}
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s. Please exclude the response length or the status code or set the wildcard option.", addInfo)
}
// GobusterDir is the main type to implement the interface
type GobusterDir struct {
options *OptionsDir
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
}
// New creates a new initialized GobusterDir
func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libgobuster.Logger) (*GobusterDir, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
g := GobusterDir{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
TLSRenegotiation: opts.TLSRenegotiation,
LocalAddr: opts.LocalAddr,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterDir) Name() string {
return "directory enumeration"
}
// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Progress) error {
// add trailing slash
if !strings.HasSuffix(d.options.URL.Path, "/") {
d.options.URL.Path = fmt.Sprintf("%s/", d.options.URL.Path)
}
_, _, _, _, err := d.http.Request(ctx, *d.options.URL, libgobuster.RequestOptions{})
if err != nil {
var retErr error
switch {
case errors.Is(err, io.EOF):
retErr = libgobuster.ErrEOF
case os.IsTimeout(err):
retErr = libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
retErr = libgobuster.ErrConnectionRefused
default:
retErr = fmt.Errorf("unable to connect to %s: %w", d.options.URL, err)
}
if !d.options.Force {
return retErr
}
// if force is set, we continue even if the preRun fails
pr.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelWarn,
Message: fmt.Sprintf("PreRun failed with error: %s. Continuing because force is set.", retErr),
}
}
guid := uuid.New()
url := *d.options.URL
url.Path = fmt.Sprintf("%s%s", url.Path, guid)
if d.options.UseSlash {
url.Path = fmt.Sprintf("%s/", url.Path)
}
wildcardResp, wildcardLength, wildcardHeader, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
if err != nil {
var retErr error
switch {
case errors.Is(err, io.EOF):
retErr = libgobuster.ErrEOF
case os.IsTimeout(err):
retErr = libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
retErr = libgobuster.ErrConnectionRefused
default:
retErr = fmt.Errorf("unable to connect to %s: %w", url.String(), err)
}
if !d.options.Force {
return retErr
}
// if force is set, we continue even if the preRun fails
pr.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelWarn,
Message: fmt.Sprintf("PreRun failed with error: %s. Continuing because force is set.", retErr),
}
}
if d.options.ExcludeLengthParsed.Contains(int(wildcardLength)) {
// we are done and ignore the request as the length is excluded
return nil
}
switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(wildcardResp) {
return &WildcardError{url: url.String(), statusCode: wildcardResp, length: wildcardLength, location: wildcardHeader.Get("Location")}
}
default:
return errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
}
return nil
}
func (d *GobusterDir) AdditionalSuccessWords(word string) []string {
if d.options.DiscoverBackup {
ret := make([]string, len(backupExtensions)+len(backupDotExtensions))
i := 0
for _, b := range backupExtensions {
ret[i] = fmt.Sprintf("%s%s", word, b)
i++
}
for _, b := range backupDotExtensions {
ret[i] = fmt.Sprintf(".%s%s", word, b)
i++
}
return ret
}
return []string{}
}
func (d *GobusterDir) AdditionalWordsLen() int {
return len(d.options.ExtensionsParsed.Set)
}
func (d *GobusterDir) AdditionalWords(word string) []string {
words := make([]string, 0, d.AdditionalWordsLen())
// build list of urls to check
// 1: No extension
// 2: With extension
for ext := range d.options.ExtensionsParsed.Set {
filename := fmt.Sprintf("%s.%s", word, ext)
words = append(words, filename)
}
return words
}
// ProcessWord is the process implementation of gobusterdir
func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
suffix := ""
if d.options.UseSlash {
suffix = "/"
}
entity := fmt.Sprintf("%s%s", word, suffix)
// prevent double slashes by removing leading /
if strings.HasPrefix(entity, "/") {
// get size of first rune and trim it
_, i := utf8.DecodeRuneInString(entity)
entity = entity[i:]
}
url := *d.options.URL
url.Path = fmt.Sprintf("%s%s", url.Path, entity)
// add some debug output
if d.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying %s", entity),
}
}
tries := 1
if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += d.options.RetryAttempts
}
var statusCode int
var size int64
var header http.Header
for i := 1; i <= tries; i++ {
var err error
statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{})
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
if os.IsTimeout(err) && 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
}
switch {
case errors.Is(err, io.EOF):
return nil, libgobuster.ErrEOF
case os.IsTimeout(err):
return nil, libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return nil, libgobuster.ErrConnectionRefused
}
return nil, err
}
break
}
if statusCode != 0 {
resultStatus := false
switch {
case d.options.StatusCodesBlacklistParsed.Length() > 0:
if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
resultStatus = true
}
case d.options.StatusCodesParsed.Length() > 0:
if d.options.StatusCodesParsed.Contains(statusCode) {
resultStatus = true
}
default:
return nil, errors.New("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
}
if resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size)) {
path := fmt.Sprintf("%-20s", entity)
if d.options.Expanded {
// expanded mode should show the full url
path = url.String()
}
r := Result{
Path: path,
Header: header,
StatusCode: -1,
Size: -1,
}
if !d.options.NoStatus {
r.StatusCode = statusCode
}
if !d.options.HideLength {
r.Size = size
}
return r, nil
}
}
return nil, nil // nolint:nilnil
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDir) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.StatusCodesBlacklistParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Negative Status codes:\t%s\n", o.StatusCodesBlacklistParsed.Stringify()); err != nil {
return "", err
}
} else if o.StatusCodesParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Status codes:\t%s\n", o.StatusCodesParsed.Stringify()); err != nil {
return "", err
}
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.LocalAddr != nil {
if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
return "", err
}
}
if o.HideLength {
if _, err := fmt.Fprintf(tw, "[+] Show length:\tfalse\n"); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if o.Extensions != "" || o.ExtensionsFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Extensions:\t%s\n", o.ExtensionsParsed.Stringify()); err != nil {
return "", err
}
}
if o.ExtensionsFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Extensions file:\t%s\n", o.ExtensionsFile); err != nil {
return "", err
}
}
if o.UseSlash {
if _, err := fmt.Fprintf(tw, "[+] Add Slash:\ttrue\n"); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
return "", err
}
}
if o.Expanded {
if _, err := fmt.Fprintf(tw, "[+] Expanded:\ttrue\n"); err != nil {
return "", err
}
}
if o.NoStatus {
if _, err := fmt.Fprintf(tw, "[+] No status:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}
================================================
FILE: gobusterdir/gobusterdir_test.go
================================================
package gobusterdir
import (
"testing"
"github.com/OJ/gobuster/v3/libgobuster"
)
func TestAdditionalWordsLen(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
extensions map[string]bool
}{
{"No extensions", map[string]bool{}},
{"Some extensions", map[string]bool{"htm": true, "html": true, "php": true}},
}
globalOpts := libgobuster.Options{}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
opts := OptionsDir{}
opts.ExtensionsParsed.Set = x.extensions
d, err := New(&globalOpts, &opts, nil)
if err != nil {
t.Fatalf("got error creating gobusterdir: %v", err)
}
calculatedLen := d.AdditionalWordsLen()
wordsLen := len(d.AdditionalWords("dummy"))
if calculatedLen != wordsLen {
t.Fatalf("Mismatched additional words length: %d got %d generated words %v", calculatedLen, wordsLen, d.AdditionalWords("dummy"))
}
})
}
}
================================================
FILE: gobusterdir/options.go
================================================
package gobusterdir
import (
"github.com/OJ/gobuster/v3/libgobuster"
)
// OptionsDir is the struct to hold all options for this plugin
type OptionsDir struct {
libgobuster.HTTPOptions
Extensions string
ExtensionsParsed libgobuster.Set[string]
ExtensionsFile string
StatusCodes string
StatusCodesParsed libgobuster.Set[int]
StatusCodesBlacklist string
StatusCodesBlacklistParsed libgobuster.Set[int]
UseSlash bool
HideLength bool
Expanded bool
NoStatus bool
DiscoverBackup bool
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
Force bool
}
// NewOptions returns a new initialized OptionsDir
func NewOptions() *OptionsDir {
return &OptionsDir{
StatusCodesParsed: libgobuster.NewSet[int](),
StatusCodesBlacklistParsed: libgobuster.NewSet[int](),
ExtensionsParsed: libgobuster.NewSet[string](),
ExcludeLengthParsed: libgobuster.NewSet[int](),
}
}
================================================
FILE: gobusterdir/options_test.go
================================================
package gobusterdir
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptions()
if o.StatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
if o.ExtensionsParsed.Set == nil {
t.Fatal("ExtensionsParsed not initialized")
}
}
================================================
FILE: gobusterdir/result.go
================================================
package gobusterdir
import (
"bytes"
"fmt"
"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
type Result struct {
Path string
Header http.Header
StatusCode int
Size int64
}
// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
if _, err := buf.WriteString(r.Path); err != nil {
return "", err
}
if r.StatusCode >= 0 {
textColor := white
switch {
case r.StatusCode == http.StatusOK:
textColor = green
case r.StatusCode >= 300 && r.StatusCode < 400:
textColor = cyan
case r.StatusCode >= 400 && r.StatusCode < 500:
textColor = yellow
case r.StatusCode >= 500 && r.StatusCode < 600:
textColor = red
}
textColor(buf, " (Status: %d)", r.StatusCode)
}
if r.Size >= 0 {
if _, err := fmt.Fprintf(buf, " [Size: %d]", r.Size); err != nil {
return "", err
}
}
location := r.Header.Get("Location")
if location != "" {
blue(buf, " [--> %s]", location)
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String()
return s, nil
}
================================================
FILE: gobusterdns/gobusterdns.go
================================================
package gobusterdns
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/google/uuid"
)
// WildcardError is returned if a wildcard response is found
type WildcardError struct {
wildcardIps libgobuster.Set[netip.Addr]
}
// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
return fmt.Sprintf("the DNS Server returned the same IP for every domain. IP address(es) returned: %s", e.wildcardIps.Stringify())
}
// GobusterDNS is the main type to implement the interface
type GobusterDNS struct {
resolver *net.Resolver
globalopts *libgobuster.Options
options *OptionsDNS
isWildcard bool
wildcardIps libgobuster.Set[netip.Addr]
}
func newCustomDialer(server string, protocol string) func(ctx context.Context, network, address string) (net.Conn, error) {
return func(ctx context.Context, _, _ string) (net.Conn, error) {
d := net.Dialer{}
if !strings.Contains(server, ":") {
server = fmt.Sprintf("%s:53", server)
}
return d.DialContext(ctx, protocol, server)
}
}
// New creates a new initialized GobusterDNS
func New(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
resolver := net.DefaultResolver
if opts.Resolver != "" {
resolver = &net.Resolver{
PreferGo: true,
Dial: newCustomDialer(opts.Resolver, opts.Protocol),
}
}
g := GobusterDNS{
options: opts,
globalopts: globalopts,
wildcardIps: libgobuster.NewSet[netip.Addr](),
resolver: resolver,
}
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterDNS) Name() string {
return "DNS enumeration"
}
// PreRun is the pre run implementation of gobusterdns
func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
// Resolve a subdomain that probably shouldn't exist
guid := uuid.New()
wildcardIps, err := d.dnsLookup(ctx, fmt.Sprintf("%s.%s", guid, d.options.Domain))
if err == nil {
d.isWildcard = true
d.wildcardIps.AddRange(wildcardIps)
if !d.options.WildcardForced {
return &WildcardError{wildcardIps: d.wildcardIps}
}
}
if !d.globalopts.Quiet {
// Provide a warning if the base domain doesn't resolve (in case of typo)
_, err = d.dnsLookup(ctx, d.options.Domain)
if err != nil {
// Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.yp.to` does!
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelInfo,
Message: fmt.Sprintf("[-] Unable to validate base domain: %s (%v)", d.options.Domain, err),
}
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("%#v", err),
}
}
}
return nil
}
// ProcessWord is the process implementation of gobusterdns
func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain)
if !d.options.NoFQDN && !strings.HasSuffix(subdomain, ".") {
// add a . to indicate this is the full domain, and we do not want to traverse the search domains on the system
subdomain = fmt.Sprintf("%s.", subdomain)
}
// add some debug output
if d.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying subdomain %s", subdomain),
}
}
ips, err := d.dnsLookup(ctx, subdomain)
if err != nil {
var wErr *net.DNSError
if errors.As(err, &wErr) && wErr.IsNotFound {
// host not found is the expected error here
return nil, nil // nolint:nilnil
}
return nil, err
}
if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) {
result := Result{
Subdomain: strings.TrimSuffix(subdomain, "."),
}
result.IPs = ips
if d.options.CheckCNAME {
cname, err := d.dnsLookupCname(ctx, subdomain)
if err == nil {
result.CNAME = cname
} else {
var wErr *net.DNSError
if !errors.As(err, &wErr) && !wErr.IsNotFound {
// host not found is the expected error here, send all other errors to the error channel
progress.ErrorChan <- err
}
}
}
return result, nil
}
return nil, nil // nolint:nilnil
}
func (d *GobusterDNS) AdditionalWordsLen() int {
return 0
}
func (d *GobusterDNS) AdditionalWords(_ string) []string {
return []string{}
}
func (d *GobusterDNS) AdditionalSuccessWords(_ string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDNS) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Domain:\t%s\n", o.Domain); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
if o.Resolver != "" {
if _, err := fmt.Fprintf(tw, "[+] Resolver:\t%s\n", o.Resolver); err != nil {
return "", err
}
}
if o.CheckCNAME {
if _, err := fmt.Fprintf(tw, "[+] Check CNAME:\ttrue\n"); err != nil {
return "", err
}
}
if o.WildcardForced {
if _, err := fmt.Fprintf(tw, "[+] Wildcard forced:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if 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
}
func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) {
ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
defer cancel()
return d.resolver.LookupNetIP(ctx2, "ip", domain)
}
func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) {
ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
defer cancel()
return d.resolver.LookupCNAME(ctx2, domain)
}
================================================
FILE: gobusterdns/options.go
================================================
package gobusterdns
import (
"time"
)
// OptionsDNS holds all options for the dns plugin
type OptionsDNS struct {
Domain string
CheckCNAME bool
WildcardForced bool
Resolver string
Protocol string
NoFQDN bool
Timeout time.Duration
}
// NewOptions returns a new initialized OptionsDNS
func NewOptions() *OptionsDNS {
return &OptionsDNS{}
}
================================================
FILE: gobusterdns/result.go
================================================
package gobusterdns
import (
"bytes"
"fmt"
"net/netip"
"strings"
"github.com/fatih/color"
)
var green = color.New(color.FgGreen).FprintfFunc()
// Result represents a single result
type Result struct {
Subdomain string
IPs []netip.Addr
CNAME string
}
// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
if _, err := fmt.Fprintf(buf, "%s", r.Subdomain); err != nil {
return "", err
}
if len(r.IPs) > 0 {
ips := make([]string, len(r.IPs))
for i := range r.IPs {
ips[i] = r.IPs[i].String()
}
green(buf, " %s", strings.Join(ips, ","))
}
if r.CNAME != "" {
green(buf, " CNAME: %s", r.CNAME)
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String()
return s, nil
}
================================================
FILE: gobusterfuzz/gobusterfuzz.go
================================================
package gobusterfuzz
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"syscall"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
)
const FuzzKeyword = "FUZZ"
// WildcardError is returned if a wildcard response is found
type WildcardError struct {
url string
statusCode int
}
// Error is the implementation of the error interface
func (e *WildcardError) Error() string {
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d. Please exclude the response length or the status code or set the wildcard option.", e.url, e.statusCode)
}
// GobusterFuzz is the main type to implement the interface
type GobusterFuzz struct {
options *OptionsFuzz
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
}
// New creates a new initialized GobusterFuzz
func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *libgobuster.Logger) (*GobusterFuzz, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
g := GobusterFuzz{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterFuzz) Name() string {
return "fuzzing"
}
// PreRun is the pre run implementation of gobusterfuzz
func (d *GobusterFuzz) PreRun(_ context.Context, _ *libgobuster.Progress) error {
return nil
}
// ProcessWord is the process implementation of gobusterfuzz
func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
url := *d.options.URL
url.Fragment = strings.ReplaceAll(url.Fragment, FuzzKeyword, word)
url.Host = strings.ReplaceAll(url.Host, FuzzKeyword, word)
url.Path = strings.ReplaceAll(url.Path, FuzzKeyword, word)
url.Scheme = strings.ReplaceAll(url.Scheme, FuzzKeyword, word)
query := url.Query()
for key, value := range query {
query.Del(key)
key = strings.ReplaceAll(key, FuzzKeyword, word)
for _, v := range value {
// replace the FuzzKeyword in the value
query.Add(key, strings.ReplaceAll(v, FuzzKeyword, word))
}
}
url.RawQuery = query.Encode()
requestOptions := libgobuster.RequestOptions{}
if len(d.options.Headers) > 0 {
requestOptions.ModifiedHeaders = make([]libgobuster.HTTPHeader, len(d.options.Headers))
for i := range d.options.Headers {
// Host header can't be set via Headers, needs to be a separate field
if http.CanonicalHeaderKey(d.options.Headers[i].Name) == "Host" {
requestOptions.Host = strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word)
continue
}
requestOptions.ModifiedHeaders[i] = libgobuster.HTTPHeader{
Name: strings.ReplaceAll(d.options.Headers[i].Name, FuzzKeyword, word),
Value: strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word),
}
}
}
if d.options.RequestBody != "" {
data := strings.ReplaceAll(d.options.RequestBody, FuzzKeyword, word)
buffer := strings.NewReader(data)
requestOptions.Body = buffer
}
// fuzzing of basic auth
if strings.Contains(d.options.Username, FuzzKeyword) || strings.Contains(d.options.Password, FuzzKeyword) {
requestOptions.UpdatedBasicAuthUsername = strings.ReplaceAll(d.options.Username, FuzzKeyword, word)
requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word)
}
// add some debug output
if d.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying word %s", word),
}
}
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 responseHeaders http.Header
for i := 1; i <= tries; i++ {
var err error
statusCode, size, responseHeaders, _, err = d.http.Request(ctx, url, requestOptions)
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
switch {
case os.IsTimeout(err) && i != tries:
continue
case 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
default:
switch {
case errors.Is(err, io.EOF):
return nil, libgobuster.ErrEOF
case os.IsTimeout(err):
return nil, libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return nil, libgobuster.ErrConnectionRefused
}
return nil, err
}
}
break
}
if statusCode != 0 {
resultStatus := true
if d.options.ExcludeLengthParsed.Contains(int(size)) {
resultStatus = false
}
if d.options.ExcludedStatusCodesParsed.Length() > 0 {
if d.options.ExcludedStatusCodesParsed.Contains(statusCode) {
resultStatus = false
}
}
if resultStatus {
r := Result{
Path: url.String(),
StatusCode: statusCode,
Size: size,
Word: word,
Header: responseHeaders,
}
return r, nil
}
}
return nil, nil // nolint:nilnil
}
func (d *GobusterFuzz) AdditionalWordsLen() int {
return 0
}
func (d *GobusterFuzz) AdditionalWords(_ string) []string {
return []string{}
}
func (d *GobusterFuzz) AdditionalSuccessWords(_ string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterFuzz) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.ExcludedStatusCodesParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Excluded Status codes:\t%s\n", o.ExcludedStatusCodesParsed.Stringify()); err != nil {
return "", err
}
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.LocalAddr != nil {
if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}
================================================
FILE: gobusterfuzz/options.go
================================================
package gobusterfuzz
import (
"github.com/OJ/gobuster/v3/libgobuster"
)
// OptionsFuzz is the struct to hold all options for this plugin
type OptionsFuzz struct {
libgobuster.HTTPOptions
ExcludedStatusCodes string
ExcludedStatusCodesParsed libgobuster.Set[int]
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
RequestBody string
}
// NewOptions returns a new initialized OptionsFuzz
func NewOptions() *OptionsFuzz {
return &OptionsFuzz{
ExcludedStatusCodesParsed: libgobuster.NewSet[int](),
ExcludeLengthParsed: libgobuster.NewSet[int](),
}
}
================================================
FILE: gobusterfuzz/options_test.go
================================================
package gobusterfuzz
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptions()
if o.ExcludedStatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
}
================================================
FILE: gobusterfuzz/result.go
================================================
package gobusterfuzz
import (
"bytes"
"fmt"
"net/http"
"github.com/fatih/color"
)
var (
green = color.New(color.FgGreen).FprintfFunc()
blue = color.New(color.FgBlue).FprintfFunc()
)
// Result represents a single result
type Result struct {
Word string
Path string
StatusCode int
Size int64
Header http.Header
}
// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
green(buf, "[Status=%d] [Length=%d] [Word=%s] %s", r.StatusCode, r.Size, r.Word, r.Path)
location := r.Header.Get("Location")
if location != "" {
blue(buf, " [--> %s]", location)
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String()
return s, nil
}
================================================
FILE: gobustergcs/gobustersgcs.go
================================================
package gobustergcs
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"syscall"
"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
}
// New creates a new initialized GobusterGCS
func New(globalopts *libgobuster.Options, opts *OptionsGCS, logger *libgobuster.Logger) (*GobusterGCS, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("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,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
// needed so we can list bucket contents
FollowRedirect: true,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
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(_ context.Context, _ *libgobuster.Progress) error {
return nil
}
// ProcessWord is the process implementation of GobusterS3
func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
// only check for valid bucket names
if !s.isValidBucketName(word) {
return nil, nil // nolint:nilnil
}
bucketURL := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o?maxResults=%d", url.PathEscape(word), s.options.MaxFilesToList)
u, err := url.Parse(bucketURL)
if err != nil {
return nil, fmt.Errorf("could not parse bucket URL %s: %w", bucketURL, err)
}
// add some debug output
if s.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying word %s", word),
}
}
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, *u, 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
switch {
case os.IsTimeout(err) && i != tries:
continue
case 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
default:
switch {
case errors.Is(err, io.EOF):
return nil, libgobuster.ErrEOF
case os.IsTimeout(err):
return nil, libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return nil, libgobuster.ErrConnectionRefused
}
return nil, err
}
}
break
}
if statusCode == 0 || body == nil {
return nil, nil // nolint:nilnil
}
// looks like 401, 403, and 404 are the only negative status codes
var found bool
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, nil // nolint:nilnil
}
var extraStrBuilder strings.Builder
if s.options.ShowFiles {
// get status
var result map[string]interface{}
err := json.Unmarshal(body, &result)
if err != nil {
return nil, 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 nil, fmt.Errorf("could not parse error json: %w", err)
}
_, err = fmt.Fprintf(&extraStrBuilder, "Error: %s (%d)", gcsError.Error.Message, gcsError.Error.Code)
if err != nil {
return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
}
} 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 nil, fmt.Errorf("could not parse result json: %w", err)
}
extraStrBuilder.WriteString("Bucket Listing enabled: ")
for i, x := range gcsListing.Items {
if i > 0 {
extraStrBuilder.WriteString(", ")
}
_, err := fmt.Fprintf(&extraStrBuilder, "%s (%sb)", x.Name, x.Size)
if err != nil {
return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
}
}
}
}
r := Result{
Found: found,
BucketName: word,
Status: extraStrBuilder.String(),
}
return r, nil
}
func (s *GobusterGCS) AdditionalWordsLen() int {
return 0
}
func (s *GobusterGCS) AdditionalWords(_ string) []string {
return []string{}
}
func (s *GobusterGCS) AdditionalSuccessWords(_ 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 o.LocalAddr != nil {
if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if s.options.ShowFiles {
if _, err := fmt.Fprintf(tw, "[+] Show Files:\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
}
================================================
FILE: gobustergcs/options.go
================================================
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
ShowFiles bool
}
// NewOptions returns a new initialized OptionsS3
func NewOptions() *OptionsGCS {
return &OptionsGCS{}
}
================================================
FILE: gobustergcs/result.go
================================================
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 its 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
}
================================================
FILE: gobustergcs/types.go
================================================
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"`
}
================================================
FILE: gobusters3/gobusters3.go
================================================
package gobusters3
import (
"bufio"
"bytes"
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"syscall"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
)
// GobusterS3 is the main type to implement the interface
type GobusterS3 struct {
options *OptionsS3
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
bucketRegex *regexp.Regexp
}
// New creates a new initialized GobusterS3
func New(globalopts *libgobuster.Options, opts *OptionsS3, logger *libgobuster.Logger) (*GobusterS3, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
g := GobusterS3{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
// needed so we can list bucket contents
FollowRedirect: true,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
if err != nil {
return nil, err
}
g.http = h
g.bucketRegex = regexp.MustCompile(`^[a-z0-9\-.]{3,63}$`)
return &g, nil
}
// Name should return the name of the plugin
func (s *GobusterS3) Name() string {
return "S3 bucket enumeration"
}
// PreRun is the pre run implementation of GobusterS3
func (s *GobusterS3) PreRun(_ context.Context, _ *libgobuster.Progress) error {
return nil
}
// ProcessWord is the process implementation of GobusterS3
func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
// only check for valid bucket names
if !s.isValidBucketName(word) {
return nil, nil // nolint:nilnil
}
bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", url.PathEscape(word), s.options.MaxFilesToList)
u, err := url.Parse(bucketURL)
if err != nil {
return nil, fmt.Errorf("could not parse bucket URL %s: %w", bucketURL, err)
}
// add some debug output
if s.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying word %s", word),
}
}
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, *u, 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 os.IsTimeout(err) && 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
}
switch {
case errors.Is(err, io.EOF):
return nil, libgobuster.ErrEOF
case os.IsTimeout(err):
return nil, libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return nil, libgobuster.ErrConnectionRefused
}
return nil, err
}
break
}
if statusCode == 0 || body == nil {
return nil, nil // nolint:nilnil
}
// looks like 404 and 400 are the only negative status codes
found := false
switch statusCode {
case http.StatusBadRequest:
case http.StatusNotFound:
found = false
case http.StatusOK:
// listing enabled
found = true
// parse xml
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, nil // nolint:nilnil
}
var extraStrBuilder strings.Builder
if s.options.ShowFiles {
// get status
if bytes.Contains(body, []byte("<Error>")) {
awsError := AWSError{}
err := xml.Unmarshal(body, &awsError)
if err != nil {
return nil, fmt.Errorf("could not parse error xml: %w", err)
}
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
_, err = fmt.Fprintf(&extraStrBuilder, "Error: %s (%s)", awsError.Message, awsError.Code)
if err != nil {
return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
}
} else if bytes.Contains(body, []byte("<ListBucketResult ")) {
// bucket listing enabled
awsListing := AWSListing{}
err := xml.Unmarshal(body, &awsListing)
if err != nil {
return nil, fmt.Errorf("could not parse result xml: %w", err)
}
extraStrBuilder.WriteString("Bucket Listing enabled: ")
for i, x := range awsListing.Contents {
if i > 0 {
extraStrBuilder.WriteString(", ")
}
_, err := fmt.Fprintf(&extraStrBuilder, "%s (%db)", x.Key, x.Size)
if err != nil {
return nil, fmt.Errorf("fmt.Fprintf to strings.Builder failed: %w", err)
}
}
}
}
r := Result{
Found: found,
BucketName: word,
Status: extraStrBuilder.String(),
}
return r, nil
}
func (s *GobusterS3) AdditionalWordsLen() int {
return 0
}
func (s *GobusterS3) AdditionalWords(_ string) []string {
return []string{}
}
func (s *GobusterS3) AdditionalSuccessWords(_ string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (s *GobusterS3) 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 o.LocalAddr != nil {
if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if s.options.ShowFiles {
if _, err := fmt.Fprintf(tw, "[+] Show Files:\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 *GobusterS3) isValidBucketName(bucketName string) bool {
if !s.bucketRegex.MatchString(bucketName) {
return false
}
if strings.HasSuffix(bucketName, "-") ||
strings.HasPrefix(bucketName, ".") ||
strings.HasPrefix(bucketName, "-") ||
strings.Contains(bucketName, "..") ||
strings.Contains(bucketName, ".-") ||
strings.Contains(bucketName, "-.") {
return false
}
return true
}
================================================
FILE: gobusters3/options.go
================================================
package gobusters3
import (
"github.com/OJ/gobuster/v3/libgobuster"
)
// OptionsS3 is the struct to hold all options for this plugin
type OptionsS3 struct {
libgobuster.BasicHTTPOptions
MaxFilesToList int
ShowFiles bool
}
// NewOptions returns a new initialized OptionsS3
func NewOptions() *OptionsS3 {
return &OptionsS3{}
}
================================================
FILE: gobusters3/result.go
================================================
package gobusters3
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 its textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
c := green
c(buf, "http://%s.s3.amazonaws.com/", r.BucketName)
if r.Status != "" {
c(buf, " [%s]", r.Status)
}
c(buf, "\n")
str := buf.String()
return str, nil
}
================================================
FILE: gobusters3/types.go
================================================
package gobusters3
import "encoding/xml"
// AWSError represents a returned error from AWS
type AWSError struct {
XMLName xml.Name `xml:"Error"`
Code string `xml:"Code"`
Message string `xml:"Message"`
RequestID string `xml:"RequestId"`
HostID string `xml:"HostId"`
}
// AWSListing contains only a subset of returned properties
type AWSListing struct {
XMLName xml.Name `xml:"ListBucketResult"`
Name string `xml:"Name"`
IsTruncated string `xml:"IsTruncated"`
Contents []struct {
Key string `xml:"Key"`
LastModified string `xml:"LastModified"`
Size int `xml:"Size"`
} `xml:"Contents"`
}
================================================
FILE: gobustertftp/gobustertftp.go
================================================
package gobustertftp
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"strings"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/pin/tftp/v3"
)
// GobusterTFTP is the main type to implement the interface
type GobusterTFTP struct {
globalopts *libgobuster.Options
options *OptionsTFTP
}
// New creates a new initialized NewGobusterTFTP
func New(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterTFTP, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
g := GobusterTFTP{
options: opts,
globalopts: globalopts,
}
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterTFTP) Name() string {
return "TFTP enumeration"
}
// PreRun is the pre run implementation of gobustertftp
func (d *GobusterTFTP) PreRun(_ context.Context, _ *libgobuster.Progress) error {
_, err := tftp.NewClient(d.options.Server)
if err != nil {
return err
}
return nil
}
// ProcessWord is the process implementation of gobustertftp
func (d *GobusterTFTP) ProcessWord(_ context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
// add some debug output
if d.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying word %s", word),
}
}
c, err := tftp.NewClient(d.options.Server)
if err != nil {
return nil, err
}
c.SetTimeout(d.options.Timeout)
wt, err := c.Receive(word, "octet")
if err != nil {
// file not found
return nil, nil // nolint:nilerr,nilnil
}
result := Result{
Filename: word,
}
wt2, ok := wt.(tftp.IncomingTransfer)
if !ok {
return nil, errors.New("could not cast to IncomingTransfer")
}
if n, ok := wt2.Size(); ok {
result.Size = n
}
return result, nil
}
func (d *GobusterTFTP) AdditionalWordsLen() int {
return 0
}
func (d *GobusterTFTP) AdditionalWords(_ string) []string {
return []string{}
}
func (d *GobusterTFTP) AdditionalSuccessWords(_ string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterTFTP) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Server:\t%s\n", o.Server); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if 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
}
================================================
FILE: gobustertftp/options.go
================================================
package gobustertftp
import (
"time"
)
// OptionsTFTP holds all options for the tftp plugin
type OptionsTFTP struct {
Server string
Timeout time.Duration
}
// NewOptions returns a new initialized OptionsTFTP
func NewOptions() *OptionsTFTP {
return &OptionsTFTP{}
}
================================================
FILE: gobustertftp/result.go
================================================
package gobustertftp
import (
"bytes"
"fmt"
"github.com/fatih/color"
)
var green = color.New(color.FgGreen).FprintfFunc()
// Result represents a single result
type Result struct {
Filename string
Size int64
ErrorMessage string
}
// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
green(buf, "%s", r.Filename)
if r.Size > 0 {
if _, err := fmt.Fprintf(buf, " [%d]", r.Size); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String()
return s, nil
}
================================================
FILE: gobustervhost/gobustervhost.go
================================================
package gobustervhost
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"sync"
"syscall"
"text/tabwriter"
"github.com/OJ/gobuster/v3/libgobuster"
"github.com/google/uuid"
)
// GobusterVhost is the main type to implement the interface
type GobusterVhost struct {
options *OptionsVhost
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
domain string
normalBody []byte
abnormalBody []byte
once sync.Once
}
// New creates a new initialized GobusterDir
func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *libgobuster.Logger) (*GobusterVhost, error) {
if globalopts == nil {
return nil, errors.New("please provide valid global options")
}
if opts == nil {
return nil, errors.New("please provide valid plugin options")
}
g := GobusterVhost{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts, logger)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (v *GobusterVhost) Name() string {
return "VHOST enumeration"
}
// PreRun is the pre run implementation of gobusterdir
func (v *GobusterVhost) PreRun(ctx context.Context, _ *libgobuster.Progress) error {
// add trailing slash
if !strings.HasSuffix(v.options.URL.Path, "/") {
v.options.URL.Path = fmt.Sprintf("%s/", v.options.URL.Path)
}
if v.options.Domain != "" {
v.domain = v.options.Domain
} else {
v.domain = v.options.URL.Host
}
// request default vhost for normalBody
_, _, _, body, err := v.http.Request(ctx, *v.options.URL, libgobuster.RequestOptions{ReturnBody: true})
if err != nil {
switch {
case errors.Is(err, io.EOF):
return libgobuster.ErrEOF
case os.IsTimeout(err):
return libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return libgobuster.ErrConnectionRefused
}
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
}
v.normalBody = body
// request non existent vhost for abnormalBody
subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain)
_, _, _, body, err = v.http.Request(ctx, *v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
if err != nil {
switch {
case errors.Is(err, io.EOF):
return libgobuster.ErrEOF
case os.IsTimeout(err):
return libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return libgobuster.ErrConnectionRefused
}
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
}
v.abnormalBody = body
return nil
}
// ProcessWord is the process implementation of gobusterdir
func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) (libgobuster.Result, error) {
var subdomain string
var hostnameLength int
if v.options.AppendDomain {
subdomain = fmt.Sprintf("%s.%s", word, v.domain)
} else {
// wordlist needs to include full domains
subdomain = word
}
if v.options.ExcludeHostnameLength {
hostnameLength = len(subdomain)
} else {
hostnameLength = 0
}
// warn people when there is no . detected so they might want to use the other options
v.once.Do(func() {
if !strings.Contains(subdomain, ".") {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelWarn,
Message: fmt.Sprintf("the first subdomain to try does not contain a dot (%s). You might want to use the option to append the base domain otherwise the vhost will be tried as is", subdomain),
}
}
})
// add some debug output
if v.globalopts.Debug {
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("trying vhost %s", subdomain),
}
}
tries := 1
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
switch {
case os.IsTimeout(err) && i != tries:
continue
case 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
default:
switch {
case errors.Is(err, io.EOF):
return nil, libgobuster.ErrEOF
case os.IsTimeout(err):
return nil, libgobuster.ErrTimeout
case errors.Is(err, syscall.ECONNREFUSED):
return nil, libgobuster.ErrConnectionRefused
}
return nil, err
}
}
break
}
// subdomain must not match default vhost and non existent vhost
// or verbose mode is enabled
found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody)
if found && !v.options.ExcludeLengthParsed.Contains(int(size)-hostnameLength) && !v.options.ExcludeStatusParsed.Contains(statusCode) {
r := Result{
Vhost: subdomain,
StatusCode: statusCode,
Size: size,
Header: header,
}
return r, nil
}
return nil, nil // nolint:nilnil
}
func (v *GobusterVhost) AdditionalWordsLen() int {
return 0
}
func (v *GobusterVhost) AdditionalWords(_ string) []string {
return []string{}
}
func (v *GobusterVhost) AdditionalSuccessWords(_ string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (v *GobusterVhost) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := v.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", v.globalopts.Threads); err != nil {
return "", err
}
if v.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", v.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if v.globalopts.Wordlist != "-" {
wordlist = v.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if v.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", v.globalopts.PatternFile, len(v.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.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.LocalAddr != nil {
if _, err := fmt.Fprintf(tw, "[+] Local IP:\t%s\n", o.LocalAddr); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Append Domain:\t%t\n", v.options.AppendDomain); err != nil {
return "", err
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", v.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Exclude Hostname Length:\t%t\n", v.options.ExcludeHostnameLength); 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
}
================================================
FILE: gobustervhost/options.go
================================================
package gobustervhost
import (
"github.com/OJ/gobuster/v3/libgobuster"
)
// OptionsVhost is the struct to hold all options for this plugin
type OptionsVhost struct {
libgobuster.HTTPOptions
AppendDomain bool
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
ExcludeStatus string
ExcludeStatusParsed libgobuster.Set[int]
Domain string
ExcludeHostnameLength bool
}
// NewOptions returns a new initialized OptionsVhost
func NewOptions() *OptionsVhost {
return &OptionsVhost{
ExcludeLengthParsed: libgobuster.NewSet[int](),
ExcludeStatusParsed: libgobuster.NewSet[int](),
}
}
================================================
FILE: gobustervhost/result.go
================================================
package gobustervhost
import (
"fmt"
"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
type Result struct {
Vhost string
StatusCode int
Size int64
Header http.Header
}
// ResultToString converts the Result to its textual representation
func (r Result) ResultToString() (string, error) {
statusCodeColor := white
switch {
case r.StatusCode == http.StatusOK:
statusCodeColor = green
case r.StatusCode >= 300 && r.StatusCode < 400:
statusCodeColor = cyan
case r.StatusCode >= 400 && r.StatusCode < 500:
statusCodeColor = yellow
case r.StatusCode >= 500 && r.StatusCode < 600:
statusCodeColor = red
}
statusCode := statusCodeColor(fmt.Sprintf("Status: %d", r.StatusCode))
location := r.Header.Get("Location")
locationString := ""
if location != "" {
locationString = blue(fmt.Sprintf(" [--> %s]", location))
}
return fmt.Sprintf("%s %s [Size: %d]%s\n", r.Vhost, statusCode, r.Size, locationString), nil
}
================================================
FILE: libgobuster/errors.go
================================================
package libgobuster
import "errors"
var (
ErrTimeout = errors.New("timeout occurred during the request")
ErrEOF = errors.New("server closed connection without sending any data back. Maybe you are connecting via https to on http port or vice versa?")
ErrConnectionRefused = errors.New("connection refused")
)
================================================
FILE: libgobuster/helpers.go
================================================
package libgobuster
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
)
// Set is a set of Ts
type Set[T comparable] struct {
Set map[T]bool
}
// NewSet creates a new initialized Set
func NewSet[T comparable]() Set[T] {
return Set[T]{Set: map[T]bool{}}
}
// Add an element to a set
func (set *Set[T]) Add(s T) bool {
_, found := set.Set[s]
set.Set[s] = true
return !found
}
// AddRange adds a list of elements to a set
func (set *Set[T]) AddRange(ss []T) {
for _, s := range ss {
set.Set[s] = true
}
}
// Contains tests if an element is in a set
func (set *Set[T]) Contains(s T) bool {
_, found := set.Set[s]
return found
}
// ContainsAny checks if any of the elements exist
func (set *Set[T]) ContainsAny(ss []T) bool {
for _, s := range ss {
if set.Set[s] {
return true
}
}
return false
}
// Length returns the length of the Set
func (set *Set[T]) Length() int {
return len(set.Set)
}
// Stringify the set
func (set *Set[T]) Stringify() string {
values := make([]string, len(set.Set))
i := 0
for s := range set.Set {
values[i] = fmt.Sprint(s)
i++
}
return strings.Join(values, ",")
}
// this method is much faster than lineCounter_slow but has the following errors:
// - empty files are reported as 1 line
// - files only containing a newline are reported as 1 line
// - also counts lines with comments
func lineCounter(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
count := 1
lineSep := []byte{'\n'}
var lastChar byte
for {
c, err := r.Read(buf)
count += bytes.Count(buf[:c], lineSep)
// store last character received if we got any bytes
if c > 0 {
lastChar = buf[c-1]
}
switch {
case errors.Is(err, io.EOF):
// account for trailing new line
if lastChar == '\n' {
count--
}
return count, nil
case err != nil:
return -1, err
}
}
}
func lineCounterSlow(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
var count int
for scanner.Scan() {
w := scanner.Text()
if w == "" {
continue
}
count++
}
if err := scanner.Err(); err != nil {
return -1, err
}
return count, nil
}
// DefaultUserAgent returns the default user agent to use in HTTP requests
func DefaultUserAgent() string {
return fmt.Sprintf("gobuster/%s", VERSION)
}
// ParseExtensions parses the extensions provided as a comma separated list
func ParseExtensions(extensions string) (Set[string], error) {
ret := NewSet[string]()
if extensions == "" {
return ret, nil
}
for _, e := range strings.Split(extensions, ",") {
e = strings.TrimSpace(e)
// remove leading . from extensions
ret.Add(strings.TrimPrefix(e, "."))
}
return ret, nil
}
func ParseExtensionsFile(file string) ([]string, error) {
var ret []string
stream, err := os.Open(file)
if err != nil {
return ret, err
}
defer stream.Close()
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
e := scanner.Text()
e = strings.TrimSpace(e)
// remove leading . from extensions
ret = append(ret, strings.TrimPrefix(e, "."))
}
if err := scanner.Err(); err != nil {
return nil, err
}
return ret, nil
}
// ParseCommaSeparatedInt parses the status codes provided as a comma separated list
func ParseCommaSeparatedInt(inputString string) (Set[int], error) {
ret := NewSet[int]()
if inputString == "" {
return ret, nil
}
for _, part := range strings.Split(inputString, ",") {
part = strings.TrimSpace(part)
// check for range
if strings.Contains(part, "-") {
re := regexp.MustCompile(`^\s*(\d+)\s*-\s*(\d+)\s*$`)
match := re.FindStringSubmatch(part)
if match == nil || len(match) != 3 {
return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
}
from := strings.TrimSpace(match[1])
to := strings.TrimSpace(match[2])
fromI, err := strconv.Atoi(from)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, from)
}
toI, err := strconv.Atoi(to)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, to)
}
if toI < fromI {
return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
}
for i := fromI; i <= toI; i++ {
ret.Add(i)
}
} else {
i, err := strconv.Atoi(part)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string given: %s", part)
}
ret.Add(i)
}
}
return ret, nil
}
================================================
FILE: libgobuster/helpers_test.go
================================================
package libgobuster
import (
"errors"
"io"
"os"
"reflect"
"strconv"
"strings"
"testing"
"testing/iotest"
)
func TestNewSet(t *testing.T) {
t.Parallel()
if NewSet[string]().Set == nil {
t.Fatal("NewSet[string] returned nil Set")
}
if NewSet[int]().Set == nil {
t.Fatal("NewSet[int] returned nil Set")
}
}
func TestSetAdd(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.Add("test")
if len(x.Set) != 1 {
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 TestSetAddDouble(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.Add("test")
x.Add("test")
if len(x.Set) != 1 {
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 TestSetAddRange(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.AddRange([]string{"string1", "string2"})
if len(x.Set) != 2 {
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 TestSetAddRangeDouble(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.AddRange([]string{"string1", "string2", "string1", "string2"})
if len(x.Set) != 2 {
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 TestSetContains(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
for _, i := range v {
if !x.Contains(i) {
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 TestSetContainsAny(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
if !x.ContainsAny(v) {
t.Fatalf("Did not find any")
}
// test not found
if x.ContainsAny([]string{"mmmm", "nnnnn"}) {
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 TestSetStringify(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
z := x.Stringify()
// order is random
for _, i := range v {
if !strings.Contains(z, i) {
t.Fatalf("Did not find value %q in %q", i, z)
}
}
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
z = y.Stringify()
// order is random
for _, i := range v2 {
if !strings.Contains(z, strconv.Itoa(i)) {
t.Fatalf("Did not find value %q in %q", i, z)
}
}
}
func TestLineCounter(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
s string
expected int
}{
{"One Line", "test", 1},
{"3 Lines", "TestString\nTest\n1234", 3},
{"Trailing newline", "TestString\nTest\n1234\n", 3},
{"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3},
{"Empty", "", 1}, // these are wrong, but I've found no good way to handle those
{"Empty 2", "\n", 1}, // these are wrong, but I've found no good way to handle those
{"Empty 3", "\r\n", 1}, // these are wrong, but I've found no good way to handle those
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
r := strings.NewReader(x.s)
l, err := lineCounter(r)
if err != nil {
t.Fatalf("Got error: %v", err)
}
if l != x.expected {
t.Fatalf("wrong line count! Got %d expected %d", l, x.expected)
}
})
}
}
func TestLineCounterSlow(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
s string
expected int
}{
{"One Line", "test", 1},
{"3 Lines", "TestString\nTest\n1234", 3},
{"Trailing newline", "TestString\nTest\n1234\n", 3},
{"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3},
{"Empty", "", 0},
{"Empty 2", "\n", 0},
{"Empty 3", "\r\n", 0},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
r := strings.NewReader(x.s)
l, err := lineCounterSlow(r)
if err != nil {
t.Fatalf("Got error: %v", err)
}
if l != x.expected {
t.Fatalf("wrong line count! Got %d expected %d", l, x.expected)
}
})
}
}
func BenchmarkLineCounter(b *testing.B) {
r, err := os.Open("../rockyou.txt")
if err != nil {
b.Fatalf("Got error: %v", err)
}
defer r.Close()
for b.Loop() {
_, err := r.Seek(0, io.SeekStart)
if err != nil {
b.Fatalf("Got error: %v", err)
}
c, err := lineCounter(r)
if err != nil {
b.Fatalf("Got error: %v", err)
}
if c != 14344391 {
b.Errorf("invalid count. Expected 14344391, got %d", c)
}
}
}
func BenchmarkLineCounterSlow(b *testing.B) {
r, err := os.Open("../rockyou.txt")
if err != nil {
b.Fatalf("Got error: %v", err)
}
defer r.Close()
for b.Loop() {
_, err := r.Seek(0, io.SeekStart)
if err != nil {
b.Fatalf("Got error: %v", err)
}
c, err := lineCounterSlow(r)
if err != nil {
b.Fatalf("Got error: %v", err)
}
if c != 14336792 {
b.Errorf("invalid count. Expected 14336792, got %d", c)
}
}
}
func TestLineCounterError(t *testing.T) {
t.Parallel()
r := iotest.TimeoutReader(strings.NewReader("test"))
_, err := lineCounter(r)
if !errors.Is(err, iotest.ErrTimeout) {
t.Fatalf("Got wrong error! %v", err)
}
}
func TestParseExtensions(t *testing.T) {
t.Parallel()
tt := []struct {
testName string
extensions string
expectedExtensions Set[string]
expectedError string
}{
{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", NewSet[string](), "invalid extension string provided"},
}
for _, x := range tt {
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
ret, err := ParseExtensions(x.extensions)
if x.expectedError != "" {
if err != nil && err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedExtensions, ret) {
t.Fatalf("Expected %v but got %v", x.expectedExtensions, ret)
}
})
}
}
func TestParseCommaSeparatedInt(t *testing.T) {
t.Parallel()
tt := []struct {
stringCodes string
expectedCodes []int
expectedError string
}{
{"200,100,202", []int{200, 100, 202}, ""},
{"200, 100 , 202", []int{200, 100, 202}, ""},
{"200, 100, 202, 100", []int{200, 100, 202}, ""},
{"200,AAA", []int{}, "invalid string given: AAA"},
{"2000000000000000000000000000000", []int{}, "invalid string given: 2000000000000000000000000000000"},
{"", []int{}, "invalid string provided"},
{"200-205", []int{200, 201, 202, 203, 204, 205}, ""},
{"200-202,203-205", []int{200, 201, 202, 203, 204, 205}, ""},
{"200-202,204-205", []int{200, 201, 202, 204, 205}, ""},
{"200-202,205", []int{200, 201, 202, 205}, ""},
{"205,200,100-101,103-105", []int{100, 101, 103, 104, 105, 200, 205}, ""},
{"200-200", []int{200}, ""},
{"200 - 202", []int{200, 201, 202}, ""},
{"200 -202", []int{200, 201, 202}, ""},
{"200- 202", []int{200, 201, 202}, ""},
{"200 - 202", []int{200, 201, 202}, ""},
{"230-200", []int{}, "invalid range given: 230-200"},
{"A-200", []int{}, "invalid range given: A-200"},
{"230-A", []int{}, "invalid range given: 230-A"},
{"200,202-205,A,206-210", []int{}, "invalid string given: A"},
{"200,202-205,A-1,206-210", []int{}, "invalid range given: A-1"},
{"200,202-205,1-A,206-210", []int{}, "invalid range given: 1-A"},
}
for _, x := range tt {
t.Run(x.stringCodes, func(t *testing.T) {
t.Parallel()
want := NewSet[int]()
want.AddRange(x.expectedCodes)
ret, err := ParseCommaSeparatedInt(x.stringCodes)
if x.expectedError != "" {
if err != nil && err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(want, ret) {
t.Fatalf("Expected %v but got %v", want, ret)
}
})
}
}
func BenchmarkParseExtensions(b *testing.B) {
tt := []struct {
testName string
extensions string
expectedExtensions Set[string]
expectedError string
}{
{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", NewSet[string](), "invalid extension string provided"},
}
for _, x := range tt {
b.Run(x.testName, func(b2 *testing.B) {
for b2.Loop() {
_, _ = ParseExtensions(x.extensions)
}
})
}
}
func BenchmarkParseCommaSeparatedInt(b *testing.B) {
tt := []struct {
testName string
stringCodes string
expectedCodes Set[int]
expectedError string
}{
{"Valid codes", "200,100,202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", NewSet[int](), "invalid string given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", NewSet[int](), "invalid string given: 2000000000000000000000000000000"},
{"Empty string", "", NewSet[int](), "invalid string string provided"},
}
for _, x := range tt {
b.Run(x.testName, func(b2 *testing.B) {
for b2.Loop() {
_, _ = ParseCommaSeparatedInt(x.stringCodes)
}
})
}
}
================================================
FILE: libgobuster/http.go
================================================
package libgobuster
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// HTTPHeader holds a single key value pair of a HTTP header
type HTTPHeader struct {
Name string
Value string
}
// HTTPClient represents a http object
type HTTPClient struct {
client *http.Client
userAgent string
defaultUserAgent string
username string
password string
headers []HTTPHeader
noCanonicalizeHeaders bool
cookies string
method string
host string
logger *Logger
}
// RequestOptions is used to pass options to a single individual request
type RequestOptions struct {
Host string
Body io.Reader
ReturnBody bool
ModifiedHeaders []HTTPHeader
UpdatedBasicAuthUsername string
UpdatedBasicAuthPassword string
}
// NewHTTPClient returns a new HTTPClient
func NewHTTPClient(opt *HTTPOptions, logger *Logger) (*HTTPClient, error) {
var proxyURLFunc func(*http.Request) (*url.URL, error)
var client HTTPClient
proxyURLFunc = http.ProxyFromEnvironment
if opt == nil {
return nil, errors.New("options is nil")
}
if opt.Proxy != "" {
proxyURL, err := url.Parse(opt.Proxy)
if err != nil {
return nil, fmt.Errorf("proxy URL is invalid (%w)", err)
}
proxyURLFunc = http.ProxyURL(proxyURL)
}
var redirectFunc func(req *http.Request, via []*http.Request) error
if !opt.FollowRedirect {
redirectFunc = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
}
} else {
redirectFunc = nil
}
tlsConfig := tls.Config{
InsecureSkipVerify: opt.NoTLSValidation, // nolint:gosec
// enable TLS1.0 and TLS1.1 support
MinVersion: tls.VersionTLS10,
}
if opt.TLSCertificate != nil {
tlsConfig.Certificates = []tls.Certificate{*opt.TLSCertificate}
}
if opt.TLSRenegotiation {
tlsConfig.Renegotiation = tls.RenegotiateOnceAsClient
}
transport := &http.Transport{
Proxy: proxyURLFunc,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
TLSClientConfig: &tlsConfig,
}
// set specific network interface
if opt.LocalAddr != nil {
logger.Debugf("Setting local address to %s", opt.LocalAddr.String())
dialer := &net.Dialer{
Timeout: opt.Timeout,
LocalAddr: opt.LocalAddr,
}
transport.DialContext = dialer.DialContext
}
client.client = &http.Client{
Timeout: opt.Timeout,
CheckRedirect: redirectFunc,
Transport: transport,
}
client.username = opt.Username
client.password = opt.Password
client.userAgent = opt.UserAgent
client.defaultUserAgent = DefaultUserAgent()
client.headers = opt.Headers
client.noCanonicalizeHeaders = opt.NoCanonicalizeHeaders
client.cookies = opt.Cookies
client.method = opt.Method
if client.method == "" {
client.method = http.MethodGet
}
// Host header needs to be set separately
for _, h := range opt.Headers {
if h.Name == "Host" {
client.host = h.Value
break
}
}
client.logger = logger
return &client, nil
}
// Request makes a 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
func (client *HTTPClient) Request(ctx context.Context, fullURL url.URL, opts RequestOptions) (int, int64, http.Header, []byte, error) {
resp, err := client.makeRequest(ctx, fullURL, opts)
if err != nil {
// ignore context canceled errors
if errors.Is(ctx.Err(), context.Canceled) {
return 0, 0, nil, nil, nil
}
return 0, 0, nil, nil, err
}
defer resp.Body.Close()
var body []byte
var length int64
if opts.ReturnBody {
body, err = io.ReadAll(resp.Body)
if err != nil {
return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err)
}
length = int64(len(body))
} else {
// DO NOT REMOVE!
// absolutely needed so golang will reuse connections!
length, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return 0, 0, nil, nil, err
}
}
return resp.StatusCode, length, resp.Header, body, nil
}
func (client *HTTPClient) makeRequest(ctx context.Context, fullURL url.URL, opts RequestOptions) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, client.method, fullURL.String(), opts.Body)
if err != nil {
return nil, err
}
if client.cookies != "" {
req.Header.Set("Cookie", client.cookies)
}
// Use host for VHOST mode on a per-request basis, otherwise the one provided from headers
if opts.Host != "" {
req.Host = opts.Host
} else if client.host != "" {
req.Host = client.host
}
if client.userAgent != "" {
req.Header.Set("User-Agent", client.userAgent)
} else {
req.Header.Set("User-Agent", client.defaultUserAgent)
}
// add custom headers
// if ModifiedHeaders are supplied use those, otherwise use the original ones
// currently only relevant on fuzzing
if len(opts.ModifiedHeaders) > 0 {
for _, h := range opts.ModifiedHeaders {
// empty headers are not valid (happens when fuzzing the host header for example because the slice is initialized with the provided header length)
if h.Name == "" {
continue
}
if client.noCanonicalizeHeaders {
// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
req.Header[h.Name] = []string{h.Value}
} else {
req.Header.Set(h.Name, h.Value)
}
}
} else {
for _, h := range client.headers {
if client.noCanonicalizeHeaders {
// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
req.Header[h.Name] = []string{h.Value}
} else {
req.Header.Set(h.Name, h.Value)
}
}
}
if opts.UpdatedBasicAuthUsername != "" {
req.SetBasicAuth(opts.UpdatedBasicAuthUsername, opts.UpdatedBasicAuthPassword)
} else if client.username != "" {
req.SetBasicAuth(client.username, client.password)
}
if client.logger.debug {
dump, err := httputil.DumpRequestOut(req, false)
if err != nil {
return nil, err
}
client.logger.Debugf("%s", dump)
}
resp, err := client.client.Do(req)
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
if strings.HasPrefix(ue.Err.Error(), "x509") {
return nil, fmt.Errorf("invalid certificate: %w", ue.Err)
}
}
return nil, err
}
return resp, nil
}
================================================
FILE: libgobuster/http_test.go
================================================
package libgobuster
import (
"bytes"
"crypto/rand"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func httpServerB(b *testing.B, content string) *httptest.Server {
b.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := fmt.Fprint(w, content); err != nil {
b.Fatalf("%v", err)
}
}))
return ts
}
func httpServerT(t *testing.T, content string) *httptest.Server {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
if _, err := fmt.Fprint(w, content); err != nil {
t.Fatalf("%v", err)
}
}))
return ts
}
func randomString(length int) (string, error) {
letter := []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
letterLen := len(letter)
b := make([]byte, length)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(letterLen)))
if err != nil {
return "", err
}
b[i] = letter[n.Int64()]
}
return string(b), nil
}
func TestRequest(t *testing.T) {
t.Parallel()
ret, err := randomString(100)
if err != nil {
t.Fatal(err)
}
h := httpServerT(t, ret)
defer h.Close()
var o HTTPOptions
log := NewLogger(false)
c, err := NewHTTPClient(&o, log)
if err != nil {
t.Fatalf("Got Error: %v", err)
}
u, err := url.Parse(h.URL)
if err != nil {
t.Fatalf("could not parse URL %v: %v", h.URL, err)
}
status, length, _, body, err := c.Request(t.Context(), *u, RequestOptions{ReturnBody: true})
if err != nil {
t.Fatalf("Got Error: %v", err)
}
if status != 200 {
t.Fatalf("Invalid status returned: %d", status)
}
if length != int64(len(ret)) {
t.Fatalf("Invalid length returned: %d", length)
}
if body == nil || !bytes.Equal(body, []byte(ret)) {
t.Fatalf("Invalid body returned: %d", body)
}
}
func BenchmarkRequestWithoutBody(b *testing.B) {
r, err := randomString(10000)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
log := NewLogger(false)
c, err := NewHTTPClient(&o, log)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
u, err := url.Parse(h.URL)
if err != nil {
b.Fatalf("could not parse URL %v: %v", h.URL, err)
}
for b.Loop() {
_, _, _, _, err := c.Request(b.Context(), *u, RequestOptions{ReturnBody: false})
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}
func BenchmarkRequestWitBody(b *testing.B) {
r, err := randomString(10000)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
log := NewLogger(false)
c, err := NewHTTPClient(&o, log)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
u, err := url.Parse(h.URL)
if err != nil {
b.Fatalf("could not parse URL %v: %v", h.URL, err)
}
for b.Loop() {
_, _, _, _, err := c.Request(b.Context(), *u, RequestOptions{ReturnBody: true})
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}
func BenchmarkNewHTTPClient(b *testing.B) {
r, err := randomString(500)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
log := NewLogger(false)
for b.Loop() {
_, err := NewHTTPClient(&o, log)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}
================================================
FILE: libgobuster/interfaces.go
================================================
package libgobuster
import "context"
// GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface {
Name() string
PreRun(context.Context, *Progress) error
ProcessWord(context.Context, string, *Progress) (Result, error)
AdditionalWords(string) []string
AdditionalWordsLen() int
AdditionalSuccessWords(string) []string
GetConfigString() (string, error)
}
// Result is an interface for the Result object
type Result interface {
ResultToString() (string, error)
}
================================================
FILE: libgobuster/libgobuster.go
================================================
package libgobuster
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"
)
// PATTERN is the pattern for wordlist replacements in pattern file
const PATTERN = "{GOBUSTER}"
// SetupFunc is the "setup" function prototype for implementations
type SetupFunc func(*Gobuster) error
// ProcessFunc is the "process" function prototype for implementations
type ProcessFunc func(*Gobuster, string) ([]Result, error)
// ResultToStringFunc is the "to string" function prototype for implementations
type ResultToStringFunc func(*Gobuster, *Result) (*string, error)
// Gobuster is the main object when creating a new run
type Gobuster struct {
Opts *Options
Logger *Logger
plugin GobusterPlugin
Progress *Progress
}
type Guess struct {
word string
discoverOnSuccess bool
}
type Wordlist struct {
scanner *bufio.Scanner
guessesPerLine int
isStream bool
}
// NewGobuster returns a new Gobuster object
func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (*Gobuster, error) {
var g Gobuster
g.Opts = opts
g.plugin = plugin
g.Logger = logger
g.Progress = NewProgress()
return &g, nil
}
func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess, successChan chan<- *Guess, wg *sync.WaitGroup) {
defer wg.Done()
for {
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
select {
case <-ctx.Done():
return
case guess := <-guessChan:
// Mode-specific processing
res, err := g.plugin.ProcessWord(ctx, guess.word, g.Progress)
if err != nil {
// do not exit and continue
g.Progress.ErrorChan <- fmt.Errorf("error on word %s: %w", guess.word, err)
}
if res != nil {
g.Progress.ResultChan <- res
select {
case <-ctx.Done():
g.Progress.incrementRequests()
return
case successChan <- guess:
}
}
g.Progress.incrementRequests()
select {
case <-ctx.Done():
case <-time.After(g.Opts.Delay):
}
}
}
}
func feed(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool) {
for _, w := range words {
guess := &Guess{word: w, discoverOnSuccess: discoverOnSuccess}
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
select {
// need to check here too otherwise guessChan will block
case <-ctx.Done():
return
case guessChan <- guess:
}
}
}
func (g *Gobuster) feeder(ctx context.Context, guessChan chan<- *Guess, words []string, discoverOnSuccess bool, wg *sync.WaitGroup) {
defer wg.Done()
feed(ctx, guessChan, words, discoverOnSuccess)
}
func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- *Guess, wordlist *Wordlist, wg *sync.WaitGroup) {
defer wg.Done()
for wordlist.scanner.Scan() {
// Prioritize stopping when the context is done
select {
case <-ctx.Done():
return
default:
}
word := strings.TrimSpace(wordlist.scanner.Text())
switch {
case wordlist.isStream && len(word) != 0:
// Increment to keep track of expected work
g.Progress.IncrementTotalRequests(wordlist.guessesPerLine)
case wordlist.isStream && len(word) == 0:
// Skip empty lines without incrementing
continue
case len(word) == 0:
// Skip empty lines removing expected work
g.Progress.IncrementTotalRequests(-1 * wordlist.guessesPerLine)
continue
}
if len(g.Opts.Patterns) > 0 {
for _, w := range g.processPatterns(word) {
guess := &Guess{word: w, discoverOnSuccess: true}
select {
case <-ctx.Done():
return
case guessChan <- guess:
}
feed(ctx, guessChan, g.plugin.AdditionalWords(w), true)
}
} else {
guess := &Guess{word: word, discoverOnSuccess: true}
select {
case <-ctx.Done():
return
case guessChan <- guess:
}
feed(ctx, guessChan, g.plugin.AdditionalWords(word), true)
}
}
}
func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, error) {
// calculate expected requests
var guessesPerLine int
if len(g.Opts.Patterns) > 0 {
nPats := len(g.Opts.Patterns)
guessesPerLine = nPats + nPats*g.plugin.AdditionalWordsLen()
} else {
guessesPerLine = 1 + g.plugin.AdditionalWordsLen()
}
if g.Opts.Wordlist == "-" {
// Read directly from stdin
return &Wordlist{scanner: bufio.NewScanner(os.Stdin), guessesPerLine: guessesPerLine, isStream: true}, nil
}
lines, err := lineCounter(wordlist)
if err != nil {
return nil, fmt.Errorf("failed to
gitextract_sdpr052u/
├── .devcontainer/
│ └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── FUNDING.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── auto-merge-dependabot.yml
│ ├── docker.yml
│ ├── go.yml
│ ├── golangci-lint.yml
│ ├── hadolint.yml
│ ├── release.yml
│ ├── update.yml
│ ├── vhs.yml
│ └── yamllint.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yaml
├── .yamllint.yml
├── Dockerfile
├── LICENSE
├── README.md
├── Taskfile.yml
├── cli/
│ ├── const.go
│ ├── const_windows.go
│ ├── dir/
│ │ ├── dir.go
│ │ └── dir_test.go
│ ├── dns/
│ │ └── dns.go
│ ├── fuzz/
│ │ └── fuzz.go
│ ├── gcs/
│ │ └── gcs.go
│ ├── gobuster.go
│ ├── options.go
│ ├── s3/
│ │ └── s3.go
│ ├── tftp/
│ │ └── tftp.go
│ └── vhost/
│ ├── vhost.go
│ └── vhost_test.go
├── go.mod
├── go.sum
├── gobusterdir/
│ ├── gobusterdir.go
│ ├── gobusterdir_test.go
│ ├── options.go
│ ├── options_test.go
│ └── result.go
├── gobusterdns/
│ ├── gobusterdns.go
│ ├── options.go
│ └── result.go
├── gobusterfuzz/
│ ├── gobusterfuzz.go
│ ├── options.go
│ ├── options_test.go
│ └── result.go
├── gobustergcs/
│ ├── gobustersgcs.go
│ ├── options.go
│ ├── result.go
│ └── types.go
├── gobusters3/
│ ├── gobusters3.go
│ ├── options.go
│ ├── result.go
│ └── types.go
├── gobustertftp/
│ ├── gobustertftp.go
│ ├── options.go
│ └── result.go
├── gobustervhost/
│ ├── gobustervhost.go
│ ├── options.go
│ └── result.go
├── libgobuster/
│ ├── errors.go
│ ├── helpers.go
│ ├── helpers_test.go
│ ├── http.go
│ ├── http_test.go
│ ├── interfaces.go
│ ├── libgobuster.go
│ ├── logger.go
│ ├── options.go
│ ├── options_http.go
│ ├── progress.go
│ ├── useragents.go
│ └── version.go
├── main.go
└── vhs/
├── gobuster_dir.tape
└── server.go
SYMBOL INDEX (260 symbols across 53 files)
FILE: cli/const.go
constant TerminalClearLine (line 6) | TerminalClearLine = "\r\x1b[2K"
FILE: cli/const_windows.go
constant TerminalClearLine (line 6) | TerminalClearLine = "\r\r"
FILE: cli/dir/dir.go
function Command (line 13) | func Command() *cli.Command {
function getFlags (line 23) | func getFlags() []cli.Flag {
function run (line 43) | func run(c *cli.Context) error {
FILE: cli/dir/dir_test.go
function httpServer (line 17) | func httpServer(b *testing.B, content string) *httptest.Server {
function BenchmarkDirMode (line 27) | func BenchmarkDirMode(b *testing.B) {
FILE: cli/dns/dns.go
function Command (line 15) | func Command() *cli.Command {
function getFlags (line 25) | func getFlags() []cli.Flag {
function run (line 40) | func run(c *cli.Context) error {
FILE: cli/fuzz/fuzz.go
function Command (line 13) | func Command() *cli.Command {
function getFlags (line 23) | func getFlags() []cli.Flag {
function run (line 36) | func run(c *cli.Context) error {
function containsFuzzKeyword (line 84) | func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool {
FILE: cli/gcs/gcs.go
function Command (line 12) | func Command() *cli.Command {
function getFlags (line 22) | func getFlags() []cli.Flag {
function run (line 33) | func run(c *cli.Context) error {
FILE: cli/gobuster.go
constant ruler (line 17) | ruler = "===================================================...
constant cliProgressUpdate (line 18) | cliProgressUpdate = 500 * time.Millisecond
function resultWorker (line 23) | func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.Wai...
function errorWorker (line 61) | func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
function messageWorker (line 74) | func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
function printProgress (line 95) | func printProgress(g *libgobuster.Gobuster) {
function progressWorker (line 111) | func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sy...
function writeToFile (line 129) | func writeToFile(f *os.File, output string) error {
function Gobuster (line 138) | func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin lib...
FILE: cli/options.go
function BasicHTTPOptions (line 24) | func BasicHTTPOptions() []cli.Flag {
function ParseBasicHTTPOptions (line 43) | func ParseBasicHTTPOptions(c *cli.Context) (libgobuster.BasicHTTPOptions...
function CommonHTTPOptions (line 122) | func CommonHTTPOptions() []cli.Flag {
function ParseCommonHTTPOptions (line 138) | func ParseCommonHTTPOptions(c *cli.Context) (libgobuster.HTTPOptions, er...
function GlobalOptions (line 215) | func GlobalOptions() []cli.Flag {
function ParseGlobalOptions (line 232) | func ParseGlobalOptions(c *cli.Context) (libgobuster.Options, error) {
function getLocalAddrFromInterface (line 303) | func getLocalAddrFromInterface(iface string) (*net.TCPAddr, error) {
FILE: cli/s3/s3.go
function Command (line 12) | func Command() *cli.Command {
function getFlags (line 22) | func getFlags() []cli.Flag {
function run (line 33) | func run(c *cli.Context) error {
FILE: cli/tftp/tftp.go
function Command (line 14) | func Command() *cli.Command {
function getFlags (line 24) | func getFlags() []cli.Flag {
function run (line 34) | func run(c *cli.Context) error {
FILE: cli/vhost/vhost.go
function Command (line 14) | func Command() *cli.Command {
function getFlags (line 24) | func getFlags() []cli.Flag {
function run (line 40) | func run(c *cli.Context) error {
FILE: cli/vhost/vhost_test.go
function httpServer (line 17) | func httpServer(b *testing.B, content string) *httptest.Server {
function BenchmarkVhostMode (line 27) | func BenchmarkVhostMode(b *testing.B) {
FILE: gobusterdir/gobusterdir.go
type WildcardError (line 28) | type WildcardError struct
method Error (line 36) | func (e *WildcardError) Error() string {
type GobusterDir (line 47) | type GobusterDir struct
method Name (line 101) | func (d *GobusterDir) Name() string {
method PreRun (line 106) | func (d *GobusterDir) PreRun(ctx context.Context, pr *libgobuster.Prog...
method AdditionalSuccessWords (line 186) | func (d *GobusterDir) AdditionalSuccessWords(word string) []string {
method AdditionalWordsLen (line 205) | func (d *GobusterDir) AdditionalWordsLen() int {
method AdditionalWords (line 209) | func (d *GobusterDir) AdditionalWords(word string) []string {
method ProcessWord (line 222) | func (d *GobusterDir) ProcessWord(ctx context.Context, word string, pr...
method GetConfigString (line 326) | func (d *GobusterDir) GetConfigString() (string, error) {
function New (line 54) | func New(globalopts *libgobuster.Options, opts *OptionsDir, logger *libg...
FILE: gobusterdir/gobusterdir_test.go
function TestAdditionalWordsLen (line 9) | func TestAdditionalWordsLen(t *testing.T) {
FILE: gobusterdir/options.go
type OptionsDir (line 8) | type OptionsDir struct
function NewOptions (line 28) | func NewOptions() *OptionsDir {
FILE: gobusterdir/options_test.go
function TestNewOptions (line 5) | func TestNewOptions(t *testing.T) {
FILE: gobusterdir/result.go
type Result (line 21) | type Result struct
method ResultToString (line 29) | func (r Result) ResultToString() (string, error) {
FILE: gobusterdns/gobusterdns.go
type WildcardError (line 19) | type WildcardError struct
method Error (line 24) | func (e *WildcardError) Error() string {
type GobusterDNS (line 29) | type GobusterDNS struct
method Name (line 75) | func (d *GobusterDNS) Name() string {
method PreRun (line 80) | func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuste...
method ProcessWord (line 112) | func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, pr...
method AdditionalWordsLen (line 160) | func (d *GobusterDNS) AdditionalWordsLen() int {
method AdditionalWords (line 164) | func (d *GobusterDNS) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 168) | func (d *GobusterDNS) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 173) | func (d *GobusterDNS) GetConfigString() (string, error) {
method dnsLookup (line 240) | func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([...
method dnsLookupCname (line 246) | func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain strin...
function newCustomDialer (line 37) | func newCustomDialer(server string, protocol string) func(ctx context.Co...
function New (line 48) | func New(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDN...
FILE: gobusterdns/options.go
type OptionsDNS (line 8) | type OptionsDNS struct
function NewOptions (line 19) | func NewOptions() *OptionsDNS {
FILE: gobusterdns/result.go
type Result (line 15) | type Result struct
method ResultToString (line 22) | func (r Result) ResultToString() (string, error) {
FILE: gobusterfuzz/gobusterfuzz.go
constant FuzzKeyword (line 19) | FuzzKeyword = "FUZZ"
type WildcardError (line 22) | type WildcardError struct
method Error (line 28) | func (e *WildcardError) Error() string {
type GobusterFuzz (line 33) | type GobusterFuzz struct
method Name (line 84) | func (d *GobusterFuzz) Name() string {
method PreRun (line 89) | func (d *GobusterFuzz) PreRun(_ context.Context, _ *libgobuster.Progre...
method ProcessWord (line 94) | func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, p...
method AdditionalWordsLen (line 215) | func (d *GobusterFuzz) AdditionalWordsLen() int {
method AdditionalWords (line 219) | func (d *GobusterFuzz) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 223) | func (d *GobusterFuzz) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 228) | func (d *GobusterFuzz) GetConfigString() (string, error) {
function New (line 40) | func New(globalopts *libgobuster.Options, opts *OptionsFuzz, logger *lib...
FILE: gobusterfuzz/options.go
type OptionsFuzz (line 8) | type OptionsFuzz struct
function NewOptions (line 18) | func NewOptions() *OptionsFuzz {
FILE: gobusterfuzz/options_test.go
function TestNewOptions (line 5) | func TestNewOptions(t *testing.T) {
FILE: gobusterfuzz/result.go
type Result (line 17) | type Result struct
method ResultToString (line 26) | func (r Result) ResultToString() (string, error) {
FILE: gobustergcs/gobustersgcs.go
type GobusterGCS (line 23) | type GobusterGCS struct
method Name (line 73) | func (s *GobusterGCS) Name() string {
method PreRun (line 78) | func (s *GobusterGCS) PreRun(_ context.Context, _ *libgobuster.Progres...
method ProcessWord (line 83) | func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, pr...
method AdditionalWordsLen (line 216) | func (s *GobusterGCS) AdditionalWordsLen() int {
method AdditionalWords (line 220) | func (s *GobusterGCS) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 224) | func (s *GobusterGCS) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 229) | func (s *GobusterGCS) GetConfigString() (string, error) {
method isValidBucketName (line 303) | func (s *GobusterGCS) isValidBucketName(bucketName string) bool {
function New (line 31) | func New(globalopts *libgobuster.Options, opts *OptionsGCS, logger *libg...
FILE: gobustergcs/options.go
type OptionsGCS (line 8) | type OptionsGCS struct
function NewOptions (line 15) | func NewOptions() *OptionsGCS {
FILE: gobustergcs/result.go
type Result (line 12) | type Result struct
method ResultToString (line 19) | func (r Result) ResultToString() (string, error) {
FILE: gobustergcs/types.go
type GCSError (line 4) | type GCSError struct
type GCSListing (line 18) | type GCSListing struct
FILE: gobusters3/gobusters3.go
type GobusterS3 (line 23) | type GobusterS3 struct
method Name (line 72) | func (s *GobusterS3) Name() string {
method PreRun (line 77) | func (s *GobusterS3) PreRun(_ context.Context, _ *libgobuster.Progress...
method ProcessWord (line 82) | func (s *GobusterS3) ProcessWord(ctx context.Context, word string, pro...
method AdditionalWordsLen (line 207) | func (s *GobusterS3) AdditionalWordsLen() int {
method AdditionalWords (line 211) | func (s *GobusterS3) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 215) | func (s *GobusterS3) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 220) | func (s *GobusterS3) GetConfigString() (string, error) {
method isValidBucketName (line 294) | func (s *GobusterS3) isValidBucketName(bucketName string) bool {
function New (line 31) | func New(globalopts *libgobuster.Options, opts *OptionsS3, logger *libgo...
FILE: gobusters3/options.go
type OptionsS3 (line 8) | type OptionsS3 struct
function NewOptions (line 15) | func NewOptions() *OptionsS3 {
FILE: gobusters3/result.go
type Result (line 12) | type Result struct
method ResultToString (line 19) | func (r Result) ResultToString() (string, error) {
FILE: gobusters3/types.go
type AWSError (line 6) | type AWSError struct
type AWSListing (line 15) | type AWSListing struct
FILE: gobustertftp/gobustertftp.go
type GobusterTFTP (line 18) | type GobusterTFTP struct
method Name (line 41) | func (d *GobusterTFTP) Name() string {
method PreRun (line 46) | func (d *GobusterTFTP) PreRun(_ context.Context, _ *libgobuster.Progre...
method ProcessWord (line 55) | func (d *GobusterTFTP) ProcessWord(_ context.Context, word string, pro...
method AdditionalWordsLen (line 87) | func (d *GobusterTFTP) AdditionalWordsLen() int {
method AdditionalWords (line 91) | func (d *GobusterTFTP) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 95) | func (d *GobusterTFTP) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 100) | func (d *GobusterTFTP) GetConfigString() (string, error) {
function New (line 24) | func New(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterT...
FILE: gobustertftp/options.go
type OptionsTFTP (line 8) | type OptionsTFTP struct
function NewOptions (line 14) | func NewOptions() *OptionsTFTP {
FILE: gobustertftp/result.go
type Result (line 13) | type Result struct
method ResultToString (line 20) | func (r Result) ResultToString() (string, error) {
FILE: gobustervhost/gobustervhost.go
type GobusterVhost (line 22) | type GobusterVhost struct
method Name (line 77) | func (v *GobusterVhost) Name() string {
method PreRun (line 82) | func (v *GobusterVhost) PreRun(ctx context.Context, _ *libgobuster.Pro...
method ProcessWord (line 128) | func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, ...
method AdditionalWordsLen (line 215) | func (v *GobusterVhost) AdditionalWordsLen() int {
method AdditionalWords (line 219) | func (v *GobusterVhost) AdditionalWords(_ string) []string {
method AdditionalSuccessWords (line 223) | func (v *GobusterVhost) AdditionalSuccessWords(_ string) []string {
method GetConfigString (line 228) | func (v *GobusterVhost) GetConfigString() (string, error) {
function New (line 33) | func New(globalopts *libgobuster.Options, opts *OptionsVhost, logger *li...
FILE: gobustervhost/options.go
type OptionsVhost (line 8) | type OptionsVhost struct
function NewOptions (line 20) | func NewOptions() *OptionsVhost {
FILE: gobustervhost/result.go
type Result (line 20) | type Result struct
method ResultToString (line 28) | func (r Result) ResultToString() (string, error) {
FILE: libgobuster/helpers.go
type Set (line 16) | type Set struct
function NewSet (line 21) | func NewSet[T comparable]() Set[T] {
method Add (line 26) | func (set *Set[T]) Add(s T) bool {
method AddRange (line 33) | func (set *Set[T]) AddRange(ss []T) {
method Contains (line 40) | func (set *Set[T]) Contains(s T) bool {
method ContainsAny (line 46) | func (set *Set[T]) ContainsAny(ss []T) bool {
method Length (line 56) | func (set *Set[T]) Length() int {
method Stringify (line 61) | func (set *Set[T]) Stringify() string {
function lineCounter (line 75) | func lineCounter(r io.Reader) (int, error) {
function lineCounterSlow (line 104) | func lineCounterSlow(r io.Reader) (int, error) {
function DefaultUserAgent (line 123) | func DefaultUserAgent() string {
function ParseExtensions (line 128) | func ParseExtensions(extensions string) (Set[string], error) {
function ParseExtensionsFile (line 143) | func ParseExtensionsFile(file string) ([]string, error) {
function ParseCommaSeparatedInt (line 168) | func ParseCommaSeparatedInt(inputString string) (Set[int], error) {
FILE: libgobuster/helpers_test.go
function TestNewSet (line 14) | func TestNewSet(t *testing.T) {
function TestSetAdd (line 25) | func TestSetAdd(t *testing.T) {
function TestSetAddDouble (line 40) | func TestSetAddDouble(t *testing.T) {
function TestSetAddRange (line 57) | func TestSetAddRange(t *testing.T) {
function TestSetAddRangeDouble (line 72) | func TestSetAddRangeDouble(t *testing.T) {
function TestSetContains (line 87) | func TestSetContains(t *testing.T) {
function TestSetContainsAny (line 108) | func TestSetContainsAny(t *testing.T) {
function TestSetStringify (line 135) | func TestSetStringify(t *testing.T) {
function TestLineCounter (line 160) | func TestLineCounter(t *testing.T) {
function TestLineCounterSlow (line 190) | func TestLineCounterSlow(t *testing.T) {
function BenchmarkLineCounter (line 220) | func BenchmarkLineCounter(b *testing.B) {
function BenchmarkLineCounterSlow (line 241) | func BenchmarkLineCounterSlow(b *testing.B) {
function TestLineCounterError (line 262) | func TestLineCounterError(t *testing.T) {
function TestParseExtensions (line 271) | func TestParseExtensions(t *testing.T) {
function TestParseCommaSeparatedInt (line 301) | func TestParseCommaSeparatedInt(t *testing.T) {
function BenchmarkParseExtensions (line 349) | func BenchmarkParseExtensions(b *testing.B) {
function BenchmarkParseCommaSeparatedInt (line 372) | func BenchmarkParseCommaSeparatedInt(b *testing.B) {
FILE: libgobuster/http.go
type HTTPHeader (line 17) | type HTTPHeader struct
type HTTPClient (line 23) | type HTTPClient struct
method Request (line 133) | func (client *HTTPClient) Request(ctx context.Context, fullURL url.URL...
method makeRequest (line 164) | func (client *HTTPClient) makeRequest(ctx context.Context, fullURL url...
type RequestOptions (line 38) | type RequestOptions struct
function NewHTTPClient (line 48) | func NewHTTPClient(opt *HTTPOptions, logger *Logger) (*HTTPClient, error) {
FILE: libgobuster/http_test.go
function httpServerB (line 14) | func httpServerB(b *testing.B, content string) *httptest.Server {
function httpServerT (line 24) | func httpServerT(t *testing.T, content string) *httptest.Server {
function randomString (line 34) | func randomString(length int) (string, error) {
function TestRequest (line 49) | func TestRequest(t *testing.T) {
function BenchmarkRequestWithoutBody (line 82) | func BenchmarkRequestWithoutBody(b *testing.B) {
function BenchmarkRequestWitBody (line 107) | func BenchmarkRequestWitBody(b *testing.B) {
function BenchmarkNewHTTPClient (line 132) | func BenchmarkNewHTTPClient(b *testing.B) {
FILE: libgobuster/interfaces.go
type GobusterPlugin (line 6) | type GobusterPlugin interface
type Result (line 17) | type Result interface
FILE: libgobuster/libgobuster.go
constant PATTERN (line 16) | PATTERN = "{GOBUSTER}"
type SetupFunc (line 19) | type SetupFunc
type ProcessFunc (line 22) | type ProcessFunc
type ResultToStringFunc (line 25) | type ResultToStringFunc
type Gobuster (line 28) | type Gobuster struct
method worker (line 57) | func (g *Gobuster) worker(ctx context.Context, guessChan <-chan *Guess...
method feeder (line 117) | func (g *Gobuster) feeder(ctx context.Context, guessChan chan<- *Guess...
method feedWordlist (line 123) | func (g *Gobuster) feedWordlist(ctx context.Context, guessChan chan<- ...
method getWordlist (line 174) | func (g *Gobuster) getWordlist(wordlist io.ReadSeeker) (*Wordlist, err...
method Run (line 226) | func (g *Gobuster) Run(ctx context.Context) error {
method GetConfigString (line 327) | func (g *Gobuster) GetConfigString() (string, error) {
method processPatterns (line 331) | func (g *Gobuster) processPatterns(word string) []string {
method processDiscoverPatterns (line 339) | func (g *Gobuster) processDiscoverPatterns(word string) []string {
method applyPatterns (line 347) | func (g *Gobuster) applyPatterns(word string, patterns []string) []str...
type Guess (line 35) | type Guess struct
type Wordlist (line 40) | type Wordlist struct
function NewGobuster (line 47) | func NewGobuster(opts *Options, plugin GobusterPlugin, logger *Logger) (...
function feed (line 99) | func feed(ctx context.Context, guessChan chan<- *Guess, words []string, ...
FILE: libgobuster/logger.go
type Logger (line 10) | type Logger struct
method Debug (line 30) | func (l Logger) Debug(v ...any) {
method Debugf (line 37) | func (l Logger) Debugf(format string, v ...any) {
method Warn (line 44) | func (l Logger) Warn(v ...any) {
method Warnf (line 48) | func (l Logger) Warnf(format string, v ...any) {
method Info (line 52) | func (l Logger) Info(v ...any) {
method Infof (line 56) | func (l Logger) Infof(format string, v ...any) {
method Print (line 60) | func (l Logger) Print(v ...any) {
method Printf (line 64) | func (l Logger) Printf(format string, v ...any) {
method Println (line 68) | func (l Logger) Println(v ...any) {
method Error (line 72) | func (l Logger) Error(v ...any) {
method Errorf (line 76) | func (l Logger) Errorf(format string, v ...any) {
method Fatal (line 80) | func (l Logger) Fatal(v ...any) {
method Fatalf (line 84) | func (l Logger) Fatalf(format string, v ...any) {
method Fatalln (line 88) | func (l Logger) Fatalln(v ...any) {
function NewLogger (line 19) | func NewLogger(debug bool) *Logger {
FILE: libgobuster/options.go
type Options (line 8) | type Options struct
FILE: libgobuster/options_http.go
type BasicHTTPOptions (line 11) | type BasicHTTPOptions struct
type HTTPOptions (line 24) | type HTTPOptions struct
FILE: libgobuster/progress.go
type MessageLevel (line 5) | type MessageLevel
constant LevelDebug (line 8) | LevelDebug MessageLevel = iota
constant LevelInfo (line 9) | LevelInfo
constant LevelWarn (line 10) | LevelWarn
constant LevelError (line 11) | LevelError
type Message (line 14) | type Message struct
type Progress (line 19) | type Progress struct
method RequestsExpected (line 40) | func (p *Progress) RequestsExpected() int {
method RequestsIssued (line 46) | func (p *Progress) RequestsIssued() int {
method incrementRequestsIssues (line 52) | func (p *Progress) incrementRequestsIssues(by int) {
method incrementRequests (line 58) | func (p *Progress) incrementRequests() {
method IncrementTotalRequests (line 64) | func (p *Progress) IncrementTotalRequests(by int) {
function NewProgress (line 29) | func NewProgress() *Progress {
FILE: libgobuster/useragents.go
function GetRandomUserAgent (line 4670) | func GetRandomUserAgent() (string, error) {
FILE: libgobuster/version.go
constant VERSION (line 11) | VERSION = "3.8.2"
function GetVersion (line 14) | func GetVersion() string {
FILE: main.go
function main (line 22) | func main() {
FILE: vhs/server.go
type route (line 9) | type route struct
type RegexpHandler (line 14) | type RegexpHandler struct
method Handler (line 18) | func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.H...
method HandleFunc (line 22) | func (h *RegexpHandler) HandleFunc(pattern *regexp.Regexp, handler fun...
method ServeHTTP (line 26) | func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque...
function main (line 37) | func main() {
Condensed preview — 79 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (690K chars).
[
{
"path": ".devcontainer/devcontainer.json",
"chars": 888,
"preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
},
{
"path": ".dockerignore",
"chars": 68,
"preview": "*.exe\n*.out\n*.prof\n*.txt\n*.swp\n.vscode/\ngobuster\n.git/\nmitm*\n.idea/\n"
},
{
"path": ".gitattributes",
"chars": 2107,
"preview": "# https://github.com/gitattributes/gitattributes/blob/master/Common.gitattributes\n\n# Common settings that generally shou"
},
{
"path": ".github/FUNDING.yml",
"chars": 130,
"preview": "# These are supported funding model platforms\n\ngithub: [OJ, firefart]\npatreon: OJReeves\nopen_collective: gobuster\nko_fi:"
},
{
"path": ".github/dependabot.yml",
"chars": 815,
"preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
},
{
"path": ".github/workflows/auto-merge-dependabot.yml",
"chars": 858,
"preview": "name: Auto-merge dependabot updates\n\non:\n pull_request:\n branches: [main, dev]\n\npermissions:\n pull-requests: write\n"
},
{
"path": ".github/workflows/docker.yml",
"chars": 933,
"preview": "name: Build Docker Images\n\non:\n push:\n branches:\n - main\n workflow_dispatch:\n schedule:\n - cron: \"0 0 * * "
},
{
"path": ".github/workflows/go.yml",
"chars": 632,
"preview": "name: Go\non: [push, pull_request]\npermissions:\n contents: read\njobs:\n build:\n name: Build\n runs-on: ubuntu-lates"
},
{
"path": ".github/workflows/golangci-lint.yml",
"chars": 448,
"preview": "name: golangci-lint\non: [push, pull_request, workflow_dispatch]\npermissions:\n contents: read\njobs:\n golangci:\n name"
},
{
"path": ".github/workflows/hadolint.yml",
"chars": 600,
"preview": "name: Hadolint\non:\n push:\n paths:\n - \"**/Dockerfile\"\n pull_request:\n workflow_dispatch:\npermissions:\n conten"
},
{
"path": ".github/workflows/release.yml",
"chars": 663,
"preview": "name: goreleaser\n\non:\n push:\n tags:\n - \"*\"\n\npermissions:\n contents: write\n\njobs:\n goreleaser:\n runs-on: ub"
},
{
"path": ".github/workflows/update.yml",
"chars": 685,
"preview": "name: Update Dependencies\n\non:\n schedule:\n - cron: \"0 4 * * *\"\n workflow_dispatch:\n\npermissions:\n contents: write\n"
},
{
"path": ".github/workflows/vhs.yml",
"chars": 982,
"preview": "name: vhs\non:\n push:\n paths:\n - vhs/**.tape\n\npermissions:\n contents: write\n\njobs:\n vhs:\n runs-on: ubuntu-l"
},
{
"path": ".github/workflows/yamllint.yml",
"chars": 441,
"preview": "name: yamllint\non:\n push:\n paths:\n - \"**.yml\"\n - \"**.yaml\"\n pull_request:\n workflow_dispatch:\npermission"
},
{
"path": ".gitignore",
"chars": 618,
"preview": "# https://github.com/github/gitignore/blob/main/Go.gitignore\n\n# If you prefer the allow list template instead of the den"
},
{
"path": ".golangci.yml",
"chars": 4295,
"preview": "# based on https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322\nversion: \"2\"\nrun:\n relative-path-mode: gom"
},
{
"path": ".goreleaser.yaml",
"chars": 1185,
"preview": "# This is an example .goreleaser.yml file with some sensible defaults.\n# Make sure to check the documentation at https:/"
},
{
"path": ".yamllint.yml",
"chars": 96,
"preview": "---\nextends: default\n\nrules:\n truthy: disable\n line-length: disable\n document-start: disable\n"
},
{
"path": "Dockerfile",
"chars": 451,
"preview": "# syntax=docker/dockerfile:1\nFROM golang:latest AS build-env\nWORKDIR /src\nENV CGO_ENABLED=0\nCOPY go.mod /src/\nRUN go mod"
},
{
"path": "LICENSE",
"chars": 11357,
"preview": "\n Apache License\n Version 2.0, January 2004\n "
},
{
"path": "README.md",
"chars": 12926,
"preview": "# Gobuster\n\n[](https://goreportcard.com/repor"
},
{
"path": "Taskfile.yml",
"chars": 1379,
"preview": "version: \"3\"\n\nvars:\n PROGRAM: gobuster\n\ntasks:\n deps:\n cmds:\n - go mod tidy -v\n\n update:\n cmds:\n - go"
},
{
"path": "cli/const.go",
"chars": 77,
"preview": "//go:build !windows\n\npackage cli\n\nconst (\n\tTerminalClearLine = \"\\r\\x1b[2K\"\n)\n"
},
{
"path": "cli/const_windows.go",
"chars": 71,
"preview": "//go:build windows\n\npackage cli\n\nconst (\n\tTerminalClearLine = \"\\r\\r\"\n)\n"
},
{
"path": "cli/dir/dir.go",
"chars": 5265,
"preview": "package dir\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobuste"
},
{
"path": "cli/dir/dir_test.go",
"chars": 2336,
"preview": "package dir\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/OJ/gobus"
},
{
"path": "cli/dns/dns.go",
"chars": 2946,
"preview": "package dns\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"runtime\"\n\t\"time\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/"
},
{
"path": "cli/fuzz/fuzz.go",
"chars": 3168,
"preview": "package fuzz\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobus"
},
{
"path": "cli/gcs/gcs.go",
"chars": 1581,
"preview": "package gcs\n\nimport (\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobustergcs\"\n\t\"gi"
},
{
"path": "cli/gobuster.go",
"chars": 5741,
"preview": "package cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/OJ/gobuster/v3/"
},
{
"path": "cli/options.go",
"chars": 12326,
"preview": "package cli\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\""
},
{
"path": "cli/s3/s3.go",
"chars": 1575,
"preview": "package s3\n\nimport (\n\t\"fmt\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/v3/gobusters3\"\n\t\"gith"
},
{
"path": "cli/tftp/tftp.go",
"chars": 1517,
"preview": "package tftp\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobuster/"
},
{
"path": "cli/vhost/vhost.go",
"chars": 4009,
"preview": "package vhost\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\tinternalcli \"github.com/OJ/gobuster/v3/cli\"\n\t\"github.com/OJ/gobust"
},
{
"path": "cli/vhost/vhost_test.go",
"chars": 1898,
"preview": "package vhost\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/OJ/gob"
},
{
"path": "go.mod",
"chars": 919,
"preview": "module github.com/OJ/gobuster/v3\n\ngo 1.25\n\nrequire (\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/google/uuid v1.6.0\n\tgit"
},
{
"path": "go.sum",
"chars": 5364,
"preview": "github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=\ngithub.com/cpuguy83/go-md2man/v2"
},
{
"path": "gobusterdir/gobusterdir.go",
"chars": 12549,
"preview": "package gobusterdir\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall"
},
{
"path": "gobusterdir/gobusterdir_test.go",
"chars": 925,
"preview": "package gobusterdir\n\nimport (\n\t\"testing\"\n\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\nfunc TestAdditionalWordsLen(t *tes"
},
{
"path": "gobusterdir/options.go",
"chars": 1103,
"preview": "package gobusterdir\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsDir is the struct to hold all option"
},
{
"path": "gobusterdir/options_test.go",
"chars": 284,
"preview": "package gobusterdir\n\nimport \"testing\"\n\nfunc TestNewOptions(t *testing.T) {\n\tt.Parallel()\n\n\to := NewOptions()\n\tif o.Statu"
},
{
"path": "gobusterdir/result.go",
"chars": 1445,
"preview": "package gobusterdir\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\twhite = color.New(color."
},
{
"path": "gobusterdns/gobusterdns.go",
"chars": 6819,
"preview": "package gobusterdns\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/netip\"\n\t\"strings\"\n\t\"text/tabwri"
},
{
"path": "gobusterdns/options.go",
"chars": 389,
"preview": "package gobusterdns\n\nimport (\n\t\"time\"\n)\n\n// OptionsDNS holds all options for the dns plugin\ntype OptionsDNS struct {\n\tDo"
},
{
"path": "gobusterdns/result.go",
"chars": 834,
"preview": "package gobusterdns\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/netip\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.Ne"
},
{
"path": "gobusterfuzz/gobusterfuzz.go",
"chars": 8906,
"preview": "package gobusterfuzz\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"syscal"
},
{
"path": "gobusterfuzz/options.go",
"chars": 616,
"preview": "package gobusterfuzz\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsFuzz is the struct to hold all opti"
},
{
"path": "gobusterfuzz/options_test.go",
"chars": 207,
"preview": "package gobusterfuzz\n\nimport \"testing\"\n\nfunc TestNewOptions(t *testing.T) {\n\tt.Parallel()\n\n\to := NewOptions()\n\tif o.Excl"
},
{
"path": "gobusterfuzz/result.go",
"chars": 790,
"preview": "package gobusterfuzz\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\tgreen = color.New(color."
},
{
"path": "gobustergcs/gobustersgcs.go",
"chars": 8378,
"preview": "package gobustergcs\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url"
},
{
"path": "gobustergcs/options.go",
"chars": 343,
"preview": "package gobustergcs\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsGCS is the struct to hold all option"
},
{
"path": "gobustergcs/result.go",
"chars": 576,
"preview": "package gobustergcs\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).FprintfFunc()\n"
},
{
"path": "gobustergcs/types.go",
"chars": 684,
"preview": "package gobustergcs\n\n// GCSError represents a returned error from GCS\ntype GCSError struct {\n\tError struct {\n\t\tCode i"
},
{
"path": "gobusters3/gobusters3.go",
"chars": 7932,
"preview": "package gobusters3\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n"
},
{
"path": "gobusters3/options.go",
"chars": 338,
"preview": "package gobusters3\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsS3 is the struct to hold all options "
},
{
"path": "gobusters3/result.go",
"chars": 554,
"preview": "package gobusters3\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).FprintfFunc()\n\n"
},
{
"path": "gobusters3/types.go",
"chars": 661,
"preview": "package gobusters3\n\nimport \"encoding/xml\"\n\n// AWSError represents a returned error from AWS\ntype AWSError struct {\n\tXMLN"
},
{
"path": "gobustertftp/gobustertftp.go",
"chars": 3492,
"preview": "package gobustertftp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/"
},
{
"path": "gobustertftp/options.go",
"chars": 273,
"preview": "package gobustertftp\n\nimport (\n\t\"time\"\n)\n\n// OptionsTFTP holds all options for the tftp plugin\ntype OptionsTFTP struct {"
},
{
"path": "gobustertftp/result.go",
"chars": 640,
"preview": "package gobustertftp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar green = color.New(color.FgGreen).Fprint"
},
{
"path": "gobustervhost/gobustervhost.go",
"chars": 8808,
"preview": "package gobustervhost\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\""
},
{
"path": "gobustervhost/options.go",
"chars": 646,
"preview": "package gobustervhost\n\nimport (\n\t\"github.com/OJ/gobuster/v3/libgobuster\"\n)\n\n// OptionsVhost is the struct to hold all op"
},
{
"path": "gobustervhost/result.go",
"chars": 1272,
"preview": "package gobustervhost\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/fatih/color\"\n)\n\nvar (\n\twhite = color.New(color.FgWhite"
},
{
"path": "libgobuster/errors.go",
"chars": 337,
"preview": "package libgobuster\n\nimport \"errors\"\n\nvar (\n\tErrTimeout = errors.New(\"timeout occurred during the request\")\n\tE"
},
{
"path": "libgobuster/helpers.go",
"chars": 4425,
"preview": "package libgobuster\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Set i"
},
{
"path": "libgobuster/helpers_test.go",
"chars": 10784,
"preview": "package libgobuster\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"testing/iotest\"\n)\n\nfun"
},
{
"path": "libgobuster/http.go",
"chars": 6451,
"preview": "package libgobuster\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\""
},
{
"path": "libgobuster/http_test.go",
"chars": 3225,
"preview": "package libgobuster\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"math/big\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"t"
},
{
"path": "libgobuster/interfaces.go",
"chars": 503,
"preview": "package libgobuster\n\nimport \"context\"\n\n// GobusterPlugin is an interface which plugins must implement\ntype GobusterPlugi"
},
{
"path": "libgobuster/libgobuster.go",
"chars": 9039,
"preview": "package libgobuster\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// PATTERN "
},
{
"path": "libgobuster/logger.go",
"chars": 1716,
"preview": "package libgobuster\n\nimport (\n\t\"log\" // nolint:depguard\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n)\n\ntype Logger struct {\n\tlog "
},
{
"path": "libgobuster/options.go",
"chars": 489,
"preview": "package libgobuster\n\nimport (\n\t\"time\"\n)\n\n// Options holds all options that can be passed to libgobuster\ntype Options str"
},
{
"path": "libgobuster/options_http.go",
"chars": 756,
"preview": "package libgobuster\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n)\n\n// BasicHTTPOptions defines only core http opti"
},
{
"path": "libgobuster/progress.go",
"chars": 1444,
"preview": "package libgobuster\n\nimport \"sync\"\n\ntype MessageLevel int\n\nconst (\n\tLevelDebug MessageLevel = iota\n\tLevelInfo\n\tLevelWarn"
},
{
"path": "libgobuster/useragents.go",
"chars": 442570,
"preview": "package libgobuster\n\nimport (\n\t\"crypto/rand\"\n\t\"math/big\"\n)\n\n// molint:gochecknoglobals\nvar userAgents = [...]string{\n\t\"M"
},
{
"path": "libgobuster/version.go",
"chars": 849,
"preview": "package libgobuster\n\nimport (\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"strconv\"\n)\n\nconst (\n\t// VERSION contains the current gobuster ve"
},
{
"path": "main.go",
"chars": 1371,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"github.com/OJ/gobuster/v3/cli/dir\"\n\t\"github.com/OJ/gobust"
},
{
"path": "vhs/gobuster_dir.tape",
"chars": 3238,
"preview": "# VHS documentation\n#\n# Output:\n# Output <path>.gif Create a GIF output at the given <path>\n# Output <"
},
{
"path": "vhs/server.go",
"chars": 1335,
"preview": "package main\n\nimport (\n\t\"log\" // nolint:depguard\n\t\"net/http\"\n\t\"regexp\"\n)\n\ntype route struct {\n\tpattern *regexp.Regexp\n\th"
}
]
About this extraction
This page contains the full source code of the OJ/gobuster GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 79 files (634.1 KB), approximately 265.4k tokens, and a symbol index with 260 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.