├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── go.yml │ ├── golangci-lint.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cli ├── cmd │ ├── dir.go │ ├── dir_test.go │ ├── dns.go │ ├── fuzz.go │ ├── gcs.go │ ├── http.go │ ├── root.go │ ├── s3.go │ ├── tftp.go │ ├── version.go │ ├── vhost.go │ └── vhost_test.go ├── const.go ├── const_windows.go └── gobuster.go ├── cspell.json ├── go.mod ├── go.sum ├── gobusterdir ├── gobusterdir.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 ├── 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 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [OJ, firefart] 4 | patreon: OJReeves 5 | open_collective: gobuster 6 | ko_fi: OJReeves 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | target-branch: "dev" 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | target-branch: "dev" 17 | schedule: 18 | # Check for updates to GitHub Actions every weekday 19 | interval: "daily" 20 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker Images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | schedule: 9 | - cron: "0 0 * * *" 10 | 11 | jobs: 12 | Dockerhub: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: checkout sources 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up QEMU 23 | uses: docker/setup-qemu-action@v2 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | 28 | - name: Login to GitHub Container Registry 29 | uses: docker/login-action@v2 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.repository_owner }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Build and push 36 | uses: docker/build-push-action@v4 37 | with: 38 | push: true 39 | platforms: linux/amd64,linux/arm/v7,linux/arm64/v8,linux/386,linux/ppc64le 40 | tags: | 41 | ghcr.io/oj/gobuster:latest 42 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: ["1.18", "1.19", "stable"] 10 | steps: 11 | - name: Set up Go ${{ matrix.go }} 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version: ${{ matrix.go }} 15 | 16 | - name: Check out code 17 | uses: actions/checkout@v3.3.0 18 | 19 | - name: build cache 20 | uses: actions/cache@v3 21 | with: 22 | path: ~/go/pkg/mod 23 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 24 | restore-keys: | 25 | ${{ runner.os }}-go- 26 | 27 | - name: Get dependencies 28 | run: | 29 | go get -v -t -d ./... 30 | 31 | - name: Build linux 32 | run: make linux 33 | 34 | - name: Build windows 35 | run: make windows 36 | 37 | - name: Test 38 | run: make test 39 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: [push, pull_request] 3 | jobs: 4 | golangci: 5 | name: lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3.3.0 9 | 10 | - uses: actions/setup-go@v4 11 | with: 12 | go-version: "stable" 13 | 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@v3 16 | with: 17 | version: latest 18 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3.2.0 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Fetch all tags 21 | run: git fetch --force --tags 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: "stable" 27 | 28 | - name: Run GoReleaser 29 | uses: goreleaser/goreleaser-action@v4.4.0 30 | with: 31 | distribution: goreleaser 32 | version: latest 33 | args: release --clean 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | *.prof 18 | *.txt 19 | *.swp 20 | 21 | .vscode/ 22 | gobuster 23 | build 24 | 25 | dist/ 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - nonamedreturns 4 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | 17 | archives: 18 | - format: tar.gz 19 | # this name template makes the OS and Arch compatible with the results of uname. 20 | name_template: >- 21 | {{ .ProjectName }}_ 22 | {{- title .Os }}_ 23 | {{- if eq .Arch "amd64" }}x86_64 24 | {{- else if eq .Arch "386" }}i386 25 | {{- else }}{{ .Arch }}{{ end }} 26 | {{- if .Arm }}v{{ .Arm }}{{ end }} 27 | # use zip for windows archives 28 | format_overrides: 29 | - goos: windows 30 | format: zip 31 | checksum: 32 | name_template: 'checksums.txt' 33 | snapshot: 34 | name_template: "{{ incpatch .Version }}-next" 35 | changelog: 36 | sort: asc 37 | filters: 38 | exclude: 39 | - '^docs:' 40 | - '^test:' 41 | 42 | # The lines beneath this are called `modelines`. See `:help modeline` 43 | # Feel free to remove those if you don't want/use them. 44 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 45 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest AS build-env 2 | WORKDIR /src 3 | ENV CGO_ENABLED=0 4 | COPY go.mod /src/ 5 | RUN go mod download 6 | COPY . . 7 | RUN go build -a -o gobuster -trimpath 8 | 9 | FROM alpine:latest 10 | 11 | RUN apk add --no-cache ca-certificates \ 12 | && rm -rf /var/cache/* 13 | 14 | RUN mkdir -p /app \ 15 | && adduser -D gobuster \ 16 | && chown -R gobuster:gobuster /app 17 | 18 | USER gobuster 19 | WORKDIR /app 20 | 21 | COPY --from=build-env /src/gobuster . 22 | 23 | ENTRYPOINT [ "./gobuster" ] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := linux 2 | 3 | .PHONY: linux 4 | linux: 5 | go build -o ./gobuster 6 | 7 | .PHONY: windows 8 | windows: 9 | GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o ./gobuster.exe 10 | 11 | .PHONY: fmt 12 | fmt: 13 | go fmt ./... 14 | 15 | .PHONY: update 16 | update: 17 | go get -u 18 | go mod tidy -v 19 | 20 | .PHONY: all 21 | all: fmt update linux windows test lint 22 | 23 | .PHONY: test 24 | test: 25 | go test -v -race ./... 26 | 27 | .PHONY: lint 28 | lint: 29 | "$$(go env GOPATH)/bin/golangci-lint" run ./... 30 | go mod tidy 31 | 32 | .PHONY: lint-update 33 | lint-update: 34 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin 35 | $$(go env GOPATH)/bin/golangci-lint --version 36 | 37 | .PHONY: tag 38 | tag: 39 | @[ "${TAG}" ] && echo "Tagging a new version ${TAG}" || ( echo "TAG is not set"; exit 1 ) 40 | git tag -a "${TAG}" -m "${TAG}" 41 | git push origin "${TAG}" 42 | -------------------------------------------------------------------------------- /cli/cmd/dir.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/OJ/gobuster/v3/cli" 9 | "github.com/OJ/gobuster/v3/gobusterdir" 10 | "github.com/OJ/gobuster/v3/libgobuster" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | // nolint:gochecknoglobals 15 | var cmdDir *cobra.Command 16 | 17 | func runDir(cmd *cobra.Command, args []string) error { 18 | globalopts, pluginopts, err := parseDirOptions() 19 | if err != nil { 20 | return fmt.Errorf("error on parsing arguments: %w", err) 21 | } 22 | 23 | plugin, err := gobusterdir.NewGobusterDir(globalopts, pluginopts) 24 | if err != nil { 25 | return fmt.Errorf("error on creating gobusterdir: %w", err) 26 | } 27 | 28 | log := libgobuster.NewLogger(globalopts.Debug) 29 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 30 | var wErr *gobusterdir.ErrWildcard 31 | if errors.As(err, &wErr) { 32 | return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr) 33 | } 34 | log.Debugf("%#v", err) 35 | return fmt.Errorf("error on running gobuster: %w", err) 36 | } 37 | return nil 38 | } 39 | 40 | func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) { 41 | globalopts, err := parseGlobalOptions() 42 | if err != nil { 43 | return nil, nil, err 44 | } 45 | 46 | pluginOpts := gobusterdir.NewOptionsDir() 47 | 48 | httpOpts, err := parseCommonHTTPOptions(cmdDir) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | pluginOpts.Password = httpOpts.Password 53 | pluginOpts.URL = httpOpts.URL 54 | pluginOpts.UserAgent = httpOpts.UserAgent 55 | pluginOpts.Username = httpOpts.Username 56 | pluginOpts.Proxy = httpOpts.Proxy 57 | pluginOpts.Cookies = httpOpts.Cookies 58 | pluginOpts.Timeout = httpOpts.Timeout 59 | pluginOpts.FollowRedirect = httpOpts.FollowRedirect 60 | pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation 61 | pluginOpts.Headers = httpOpts.Headers 62 | pluginOpts.Method = httpOpts.Method 63 | pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout 64 | pluginOpts.RetryAttempts = httpOpts.RetryAttempts 65 | pluginOpts.TLSCertificate = httpOpts.TLSCertificate 66 | pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders 67 | 68 | pluginOpts.Extensions, err = cmdDir.Flags().GetString("extensions") 69 | if err != nil { 70 | return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) 71 | } 72 | 73 | ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions) 74 | if err != nil { 75 | return nil, nil, fmt.Errorf("invalid value for extensions: %w", err) 76 | } 77 | pluginOpts.ExtensionsParsed = ret 78 | 79 | pluginOpts.ExtensionsFile, err = cmdDir.Flags().GetString("extensions-file") 80 | if err != nil { 81 | return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err) 82 | } 83 | 84 | if pluginOpts.ExtensionsFile != "" { 85 | extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile) 86 | if err != nil { 87 | return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err) 88 | } 89 | pluginOpts.ExtensionsParsed.AddRange(extensions) 90 | } 91 | 92 | // parse normal status codes 93 | pluginOpts.StatusCodes, err = cmdDir.Flags().GetString("status-codes") 94 | if err != nil { 95 | return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err) 96 | } 97 | ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes) 98 | if err != nil { 99 | return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err) 100 | } 101 | pluginOpts.StatusCodesParsed = ret2 102 | 103 | // blacklist will override the normal status codes 104 | pluginOpts.StatusCodesBlacklist, err = cmdDir.Flags().GetString("status-codes-blacklist") 105 | if err != nil { 106 | return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err) 107 | } 108 | ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist) 109 | if err != nil { 110 | return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err) 111 | } 112 | pluginOpts.StatusCodesBlacklistParsed = ret3 113 | 114 | if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" { 115 | return nil, nil, fmt.Errorf("status-codes (%q) and status-codes-blacklist (%q) are both set - please set only one. status-codes-blacklist is set by default so you might want to disable it by supplying an empty string.", 116 | pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist) 117 | } 118 | 119 | if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" { 120 | return nil, nil, fmt.Errorf("status-codes and status-codes-blacklist are both not set, please set one") 121 | } 122 | 123 | pluginOpts.UseSlash, err = cmdDir.Flags().GetBool("add-slash") 124 | if err != nil { 125 | return nil, nil, fmt.Errorf("invalid value for add-slash: %w", err) 126 | } 127 | 128 | pluginOpts.Expanded, err = cmdDir.Flags().GetBool("expanded") 129 | if err != nil { 130 | return nil, nil, fmt.Errorf("invalid value for expanded: %w", err) 131 | } 132 | 133 | pluginOpts.NoStatus, err = cmdDir.Flags().GetBool("no-status") 134 | if err != nil { 135 | return nil, nil, fmt.Errorf("invalid value for no-status: %w", err) 136 | } 137 | 138 | pluginOpts.HideLength, err = cmdDir.Flags().GetBool("hide-length") 139 | if err != nil { 140 | return nil, nil, fmt.Errorf("invalid value for hide-length: %w", err) 141 | } 142 | 143 | pluginOpts.DiscoverBackup, err = cmdDir.Flags().GetBool("discover-backup") 144 | if err != nil { 145 | return nil, nil, fmt.Errorf("invalid value for discover-backup: %w", err) 146 | } 147 | 148 | pluginOpts.ExcludeLength, err = cmdDir.Flags().GetString("exclude-length") 149 | if err != nil { 150 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 151 | } 152 | ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) 153 | if err != nil { 154 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 155 | } 156 | pluginOpts.ExcludeLengthParsed = ret4 157 | 158 | return globalopts, pluginOpts, nil 159 | } 160 | 161 | // nolint:gochecknoinits 162 | func init() { 163 | cmdDir = &cobra.Command{ 164 | Use: "dir", 165 | Short: "Uses directory/file enumeration mode", 166 | RunE: runDir, 167 | } 168 | 169 | if err := addCommonHTTPOptions(cmdDir); err != nil { 170 | log.Fatalf("%v", err) 171 | } 172 | cmdDir.Flags().StringP("status-codes", "s", "", "Positive status codes (will be overwritten with status-codes-blacklist if set). Can also handle ranges like 200,300-400,404.") 173 | cmdDir.Flags().StringP("status-codes-blacklist", "b", "404", "Negative status codes (will override status-codes if set). Can also handle ranges like 200,300-400,404.") 174 | cmdDir.Flags().StringP("extensions", "x", "", "File extension(s) to search for") 175 | cmdDir.Flags().StringP("extensions-file", "X", "", "Read file extension(s) to search from the file") 176 | cmdDir.Flags().BoolP("expanded", "e", false, "Expanded mode, print full URLs") 177 | cmdDir.Flags().BoolP("no-status", "n", false, "Don't print status codes") 178 | cmdDir.Flags().Bool("hide-length", false, "Hide the length of the body in the output") 179 | cmdDir.Flags().BoolP("add-slash", "f", false, "Append / to each request") 180 | cmdDir.Flags().BoolP("discover-backup", "d", false, "Also search for backup files by appending multiple backup extensions") 181 | cmdDir.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") 182 | 183 | cmdDir.PersistentPreRun = func(cmd *cobra.Command, args []string) { 184 | configureGlobalOptions() 185 | } 186 | 187 | rootCmd.AddCommand(cmdDir) 188 | } 189 | -------------------------------------------------------------------------------- /cli/cmd/dir_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/OJ/gobuster/v3/cli" 13 | "github.com/OJ/gobuster/v3/gobusterdir" 14 | "github.com/OJ/gobuster/v3/libgobuster" 15 | ) 16 | 17 | func httpServer(b *testing.B, content string) *httptest.Server { 18 | b.Helper() 19 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 20 | fmt.Fprint(w, content) 21 | })) 22 | return ts 23 | } 24 | func BenchmarkDirMode(b *testing.B) { 25 | h := httpServer(b, "test") 26 | defer h.Close() 27 | 28 | pluginopts := gobusterdir.NewOptionsDir() 29 | pluginopts.URL = h.URL 30 | pluginopts.Timeout = 10 * time.Second 31 | 32 | pluginopts.Extensions = ".php,.csv" 33 | tmpExt, err := libgobuster.ParseExtensions(pluginopts.Extensions) 34 | if err != nil { 35 | b.Fatalf("could not parse extensions: %v", err) 36 | } 37 | pluginopts.ExtensionsParsed = tmpExt 38 | 39 | pluginopts.StatusCodes = "200,204,301,302,307,401,403" 40 | tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes) 41 | if err != nil { 42 | b.Fatalf("could not parse status codes: %v", err) 43 | } 44 | pluginopts.StatusCodesParsed = tmpStat 45 | 46 | wordlist, err := os.CreateTemp("", "") 47 | if err != nil { 48 | b.Fatalf("could not create tempfile: %v", err) 49 | } 50 | defer os.Remove(wordlist.Name()) 51 | for w := 0; w < 1000; w++ { 52 | _, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w)) 53 | } 54 | wordlist.Close() 55 | 56 | globalopts := libgobuster.Options{ 57 | Threads: 10, 58 | Wordlist: wordlist.Name(), 59 | NoProgress: true, 60 | } 61 | 62 | ctx := context.Background() 63 | oldStdout := os.Stdout 64 | oldStderr := os.Stderr 65 | defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr) 66 | devnull, err := os.Open(os.DevNull) 67 | if err != nil { 68 | b.Fatalf("could not get devnull %v", err) 69 | } 70 | defer devnull.Close() 71 | log := libgobuster.NewLogger(false) 72 | 73 | // Run the real benchmark 74 | for x := 0; x < b.N; x++ { 75 | os.Stdout = devnull 76 | os.Stderr = devnull 77 | plugin, err := gobusterdir.NewGobusterDir(&globalopts, pluginopts) 78 | if err != nil { 79 | b.Fatalf("error on creating gobusterdir: %v", err) 80 | } 81 | 82 | if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil { 83 | b.Fatalf("error on running gobuster: %v", err) 84 | } 85 | os.Stdout = oldStdout 86 | os.Stderr = oldStderr 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /cli/cmd/dns.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "runtime" 8 | "time" 9 | 10 | "github.com/OJ/gobuster/v3/cli" 11 | "github.com/OJ/gobuster/v3/gobusterdns" 12 | "github.com/OJ/gobuster/v3/libgobuster" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // nolint:gochecknoglobals 17 | var cmdDNS *cobra.Command 18 | 19 | func runDNS(cmd *cobra.Command, args []string) error { 20 | globalopts, pluginopts, err := parseDNSOptions() 21 | if err != nil { 22 | return fmt.Errorf("error on parsing arguments: %w", err) 23 | } 24 | 25 | plugin, err := gobusterdns.NewGobusterDNS(globalopts, pluginopts) 26 | if err != nil { 27 | return fmt.Errorf("error on creating gobusterdns: %w", err) 28 | } 29 | 30 | log := libgobuster.NewLogger(globalopts.Debug) 31 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 32 | var wErr *gobusterdns.ErrWildcard 33 | if errors.As(err, &wErr) { 34 | return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr) 35 | } 36 | log.Debugf("%#v", err) 37 | return fmt.Errorf("error on running gobuster: %w", err) 38 | } 39 | return nil 40 | } 41 | 42 | func parseDNSOptions() (*libgobuster.Options, *gobusterdns.OptionsDNS, error) { 43 | globalopts, err := parseGlobalOptions() 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | pluginOpts := gobusterdns.NewOptionsDNS() 48 | 49 | pluginOpts.Domain, err = cmdDNS.Flags().GetString("domain") 50 | if err != nil { 51 | return nil, nil, fmt.Errorf("invalid value for domain: %w", err) 52 | } 53 | 54 | pluginOpts.ShowIPs, err = cmdDNS.Flags().GetBool("show-ips") 55 | if err != nil { 56 | return nil, nil, fmt.Errorf("invalid value for show-ips: %w", err) 57 | } 58 | 59 | pluginOpts.ShowCNAME, err = cmdDNS.Flags().GetBool("show-cname") 60 | if err != nil { 61 | return nil, nil, fmt.Errorf("invalid value for show-cname: %w", err) 62 | } 63 | 64 | pluginOpts.WildcardForced, err = cmdDNS.Flags().GetBool("wildcard") 65 | if err != nil { 66 | return nil, nil, fmt.Errorf("invalid value for wildcard: %w", err) 67 | } 68 | 69 | pluginOpts.Timeout, err = cmdDNS.Flags().GetDuration("timeout") 70 | if err != nil { 71 | return nil, nil, fmt.Errorf("invalid value for timeout: %w", err) 72 | } 73 | 74 | pluginOpts.Resolver, err = cmdDNS.Flags().GetString("resolver") 75 | if err != nil { 76 | return nil, nil, fmt.Errorf("invalid value for resolver: %w", err) 77 | } 78 | 79 | pluginOpts.NoFQDN, err = cmdDNS.Flags().GetBool("no-fqdn") 80 | if err != nil { 81 | return nil, nil, fmt.Errorf("invalid value for no-fqdn: %w", err) 82 | } 83 | 84 | if pluginOpts.Resolver != "" && runtime.GOOS == "windows" { 85 | return nil, nil, fmt.Errorf("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution") 86 | } 87 | 88 | return globalopts, pluginOpts, nil 89 | } 90 | 91 | // nolint:gochecknoinits 92 | func init() { 93 | cmdDNS = &cobra.Command{ 94 | Use: "dns", 95 | Short: "Uses DNS subdomain enumeration mode", 96 | RunE: runDNS, 97 | } 98 | 99 | cmdDNS.Flags().StringP("domain", "d", "", "The target domain") 100 | cmdDNS.Flags().BoolP("show-ips", "i", false, "Show IP addresses") 101 | cmdDNS.Flags().BoolP("show-cname", "c", false, "Show CNAME records (cannot be used with '-i' option)") 102 | cmdDNS.Flags().DurationP("timeout", "", time.Second, "DNS resolver timeout") 103 | cmdDNS.Flags().BoolP("wildcard", "", false, "Force continued operation when wildcard found") 104 | cmdDNS.Flags().BoolP("no-fqdn", "", false, "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain") 105 | cmdDNS.Flags().StringP("resolver", "r", "", "Use custom DNS server (format server.com or server.com:port)") 106 | if err := cmdDNS.MarkFlagRequired("domain"); err != nil { 107 | log.Fatalf("error on marking flag as required: %v", err) 108 | } 109 | 110 | cmdDNS.PersistentPreRun = func(cmd *cobra.Command, args []string) { 111 | configureGlobalOptions() 112 | } 113 | 114 | rootCmd.AddCommand(cmdDNS) 115 | } 116 | -------------------------------------------------------------------------------- /cli/cmd/fuzz.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/OJ/gobuster/v3/cli" 10 | "github.com/OJ/gobuster/v3/gobusterfuzz" 11 | "github.com/OJ/gobuster/v3/libgobuster" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // nolint:gochecknoglobals 16 | var cmdFuzz *cobra.Command 17 | 18 | func runFuzz(cmd *cobra.Command, args []string) error { 19 | globalopts, pluginopts, err := parseFuzzOptions() 20 | if err != nil { 21 | return fmt.Errorf("error on parsing arguments: %w", err) 22 | } 23 | 24 | if !containsFuzzKeyword(*pluginopts) { 25 | return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword) 26 | } 27 | 28 | plugin, err := gobusterfuzz.NewGobusterFuzz(globalopts, pluginopts) 29 | if err != nil { 30 | return fmt.Errorf("error on creating gobusterfuzz: %w", err) 31 | } 32 | 33 | log := libgobuster.NewLogger(globalopts.Debug) 34 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 35 | var wErr *gobusterfuzz.ErrWildcard 36 | if errors.As(err, &wErr) { 37 | return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr) 38 | } 39 | log.Debugf("%#v", err) 40 | return fmt.Errorf("error on running gobuster: %w", err) 41 | } 42 | return nil 43 | } 44 | 45 | func parseFuzzOptions() (*libgobuster.Options, *gobusterfuzz.OptionsFuzz, error) { 46 | globalopts, err := parseGlobalOptions() 47 | if err != nil { 48 | return nil, nil, err 49 | } 50 | 51 | pluginOpts := gobusterfuzz.NewOptionsFuzz() 52 | 53 | httpOpts, err := parseCommonHTTPOptions(cmdFuzz) 54 | if err != nil { 55 | return nil, nil, err 56 | } 57 | pluginOpts.Password = httpOpts.Password 58 | pluginOpts.URL = httpOpts.URL 59 | pluginOpts.UserAgent = httpOpts.UserAgent 60 | pluginOpts.Username = httpOpts.Username 61 | pluginOpts.Proxy = httpOpts.Proxy 62 | pluginOpts.Cookies = httpOpts.Cookies 63 | pluginOpts.Timeout = httpOpts.Timeout 64 | pluginOpts.FollowRedirect = httpOpts.FollowRedirect 65 | pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation 66 | pluginOpts.Headers = httpOpts.Headers 67 | pluginOpts.Method = httpOpts.Method 68 | pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout 69 | pluginOpts.RetryAttempts = httpOpts.RetryAttempts 70 | pluginOpts.TLSCertificate = httpOpts.TLSCertificate 71 | pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders 72 | 73 | // blacklist will override the normal status codes 74 | pluginOpts.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes") 75 | if err != nil { 76 | return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) 77 | } 78 | ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes) 79 | if err != nil { 80 | return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err) 81 | } 82 | pluginOpts.ExcludedStatusCodesParsed = ret 83 | 84 | pluginOpts.ExcludeLength, err = cmdFuzz.Flags().GetString("exclude-length") 85 | if err != nil { 86 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 87 | } 88 | ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) 89 | if err != nil { 90 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 91 | } 92 | pluginOpts.ExcludeLengthParsed = ret2 93 | 94 | pluginOpts.RequestBody, err = cmdFuzz.Flags().GetString("body") 95 | if err != nil { 96 | return nil, nil, fmt.Errorf("invalid value for body: %w", err) 97 | } 98 | 99 | return globalopts, pluginOpts, nil 100 | } 101 | 102 | // nolint:gochecknoinits 103 | func init() { 104 | cmdFuzz = &cobra.Command{ 105 | Use: "fuzz", 106 | Short: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword), 107 | RunE: runFuzz, 108 | } 109 | 110 | if err := addCommonHTTPOptions(cmdFuzz); err != nil { 111 | log.Fatalf("%v", err) 112 | } 113 | cmdFuzz.Flags().StringP("excludestatuscodes", "b", "", "Excluded status codes. Can also handle ranges like 200,300-400,404.") 114 | cmdFuzz.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") 115 | cmdFuzz.Flags().StringP("body", "B", "", "Request body") 116 | 117 | cmdFuzz.PersistentPreRun = func(cmd *cobra.Command, args []string) { 118 | configureGlobalOptions() 119 | } 120 | 121 | rootCmd.AddCommand(cmdFuzz) 122 | } 123 | 124 | func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool { 125 | if strings.Contains(pluginopts.URL, gobusterfuzz.FuzzKeyword) { 126 | return true 127 | } 128 | 129 | if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) { 130 | return true 131 | } 132 | 133 | for _, h := range pluginopts.Headers { 134 | if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) { 135 | return true 136 | } 137 | } 138 | 139 | if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) { 140 | return true 141 | } 142 | 143 | if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) { 144 | return true 145 | } 146 | 147 | return false 148 | } 149 | -------------------------------------------------------------------------------- /cli/cmd/gcs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/OJ/gobuster/v3/cli" 7 | "github.com/OJ/gobuster/v3/gobustergcs" 8 | "github.com/OJ/gobuster/v3/libgobuster" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // nolint:gochecknoglobals 13 | var cmdGCS *cobra.Command 14 | 15 | func runGCS(cmd *cobra.Command, args []string) error { 16 | globalopts, pluginopts, err := parseGCSOptions() 17 | if err != nil { 18 | return fmt.Errorf("error on parsing arguments: %w", err) 19 | } 20 | 21 | plugin, err := gobustergcs.NewGobusterGCS(globalopts, pluginopts) 22 | if err != nil { 23 | return fmt.Errorf("error on creating gobustergcs: %w", err) 24 | } 25 | 26 | log := libgobuster.NewLogger(globalopts.Debug) 27 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 28 | log.Debugf("%#v", err) 29 | return fmt.Errorf("error on running gobuster: %w", err) 30 | } 31 | return nil 32 | } 33 | 34 | func parseGCSOptions() (*libgobuster.Options, *gobustergcs.OptionsGCS, error) { 35 | globalopts, err := parseGlobalOptions() 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | pluginopts := gobustergcs.NewOptionsGCS() 41 | 42 | httpOpts, err := parseBasicHTTPOptions(cmdGCS) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | pluginopts.UserAgent = httpOpts.UserAgent 48 | pluginopts.Proxy = httpOpts.Proxy 49 | pluginopts.Timeout = httpOpts.Timeout 50 | pluginopts.NoTLSValidation = httpOpts.NoTLSValidation 51 | pluginopts.RetryOnTimeout = httpOpts.RetryOnTimeout 52 | pluginopts.RetryAttempts = httpOpts.RetryAttempts 53 | pluginopts.TLSCertificate = httpOpts.TLSCertificate 54 | 55 | pluginopts.MaxFilesToList, err = cmdGCS.Flags().GetInt("maxfiles") 56 | if err != nil { 57 | return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err) 58 | } 59 | 60 | return globalopts, pluginopts, nil 61 | } 62 | 63 | // nolint:gochecknoinits 64 | func init() { 65 | cmdGCS = &cobra.Command{ 66 | Use: "gcs", 67 | Short: "Uses gcs bucket enumeration mode", 68 | RunE: runGCS, 69 | } 70 | 71 | addBasicHTTPOptions(cmdGCS) 72 | cmdGCS.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets (only shown in verbose mode)") 73 | 74 | cmdGCS.PersistentPreRun = func(cmd *cobra.Command, args []string) { 75 | configureGlobalOptions() 76 | } 77 | 78 | rootCmd.AddCommand(cmdGCS) 79 | } 80 | -------------------------------------------------------------------------------- /cli/cmd/http.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/pem" 6 | "fmt" 7 | "os" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/OJ/gobuster/v3/libgobuster" 15 | "github.com/spf13/cobra" 16 | "golang.org/x/crypto/pkcs12" 17 | "golang.org/x/term" 18 | ) 19 | 20 | func addBasicHTTPOptions(cmd *cobra.Command) { 21 | cmd.Flags().StringP("useragent", "a", libgobuster.DefaultUserAgent(), "Set the User-Agent string") 22 | cmd.Flags().BoolP("random-agent", "", false, "Use a random User-Agent string") 23 | cmd.Flags().StringP("proxy", "", "", "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]") 24 | cmd.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout") 25 | cmd.Flags().BoolP("no-tls-validation", "k", false, "Skip TLS certificate verification") 26 | cmd.Flags().BoolP("retry", "", false, "Should retry on request timeout") 27 | cmd.Flags().IntP("retry-attempts", "", 3, "Times to retry on request timeout") 28 | // client certificates, either pem or p12 29 | cmd.Flags().StringP("client-cert-pem", "", "", "public key in PEM format for optional TLS client certificates") 30 | cmd.Flags().StringP("client-cert-pem-key", "", "", "private key in PEM format for optional TLS client certificates (this key needs to have no password)") 31 | cmd.Flags().StringP("client-cert-p12", "", "", "a p12 file to use for options TLS client certificates") 32 | cmd.Flags().StringP("client-cert-p12-password", "", "", "the password to the p12 file") 33 | } 34 | 35 | func addCommonHTTPOptions(cmd *cobra.Command) error { 36 | addBasicHTTPOptions(cmd) 37 | cmd.Flags().StringP("url", "u", "", "The target URL") 38 | cmd.Flags().StringP("cookies", "c", "", "Cookies to use for the requests") 39 | cmd.Flags().StringP("username", "U", "", "Username for Basic Auth") 40 | cmd.Flags().StringP("password", "P", "", "Password for Basic Auth") 41 | cmd.Flags().BoolP("follow-redirect", "r", false, "Follow redirects") 42 | cmd.Flags().StringArrayP("headers", "H", []string{""}, "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'") 43 | cmd.Flags().BoolP("no-canonicalize-headers", "", false, "Do not canonicalize HTTP header names. If set header names are sent as is.") 44 | cmd.Flags().StringP("method", "m", "GET", "Use the following HTTP method") 45 | 46 | if err := cmd.MarkFlagRequired("url"); err != nil { 47 | return fmt.Errorf("error on marking flag as required: %w", err) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | func parseBasicHTTPOptions(cmd *cobra.Command) (libgobuster.BasicHTTPOptions, error) { 54 | options := libgobuster.BasicHTTPOptions{} 55 | var err error 56 | 57 | options.UserAgent, err = cmd.Flags().GetString("useragent") 58 | if err != nil { 59 | return options, fmt.Errorf("invalid value for useragent: %w", err) 60 | } 61 | randomUA, err := cmd.Flags().GetBool("random-agent") 62 | if err != nil { 63 | return options, fmt.Errorf("invalid value for random-agent: %w", err) 64 | } 65 | if randomUA { 66 | ua, err := libgobuster.GetRandomUserAgent() 67 | if err != nil { 68 | return options, err 69 | } 70 | options.UserAgent = ua 71 | } 72 | 73 | options.Proxy, err = cmd.Flags().GetString("proxy") 74 | if err != nil { 75 | return options, fmt.Errorf("invalid value for proxy: %w", err) 76 | } 77 | 78 | options.Timeout, err = cmd.Flags().GetDuration("timeout") 79 | if err != nil { 80 | return options, fmt.Errorf("invalid value for timeout: %w", err) 81 | } 82 | 83 | options.RetryOnTimeout, err = cmd.Flags().GetBool("retry") 84 | if err != nil { 85 | return options, fmt.Errorf("invalid value for retry: %w", err) 86 | } 87 | 88 | options.RetryAttempts, err = cmd.Flags().GetInt("retry-attempts") 89 | if err != nil { 90 | return options, fmt.Errorf("invalid value for retry-attempts: %w", err) 91 | } 92 | 93 | options.NoTLSValidation, err = cmd.Flags().GetBool("no-tls-validation") 94 | if err != nil { 95 | return options, fmt.Errorf("invalid value for no-tls-validation: %w", err) 96 | } 97 | 98 | pemFile, err := cmd.Flags().GetString("client-cert-pem") 99 | if err != nil { 100 | return options, fmt.Errorf("invalid value for client-cert-pem: %w", err) 101 | } 102 | pemKeyFile, err := cmd.Flags().GetString("client-cert-pem-key") 103 | if err != nil { 104 | return options, fmt.Errorf("invalid value for client-cert-pem-key: %w", err) 105 | } 106 | p12File, err := cmd.Flags().GetString("client-cert-p12") 107 | if err != nil { 108 | return options, fmt.Errorf("invalid value for client-cert-p12: %w", err) 109 | } 110 | p12Pass, err := cmd.Flags().GetString("client-cert-p12-password") 111 | if err != nil { 112 | return options, fmt.Errorf("invalid value for client-cert-p12-password: %w", err) 113 | } 114 | 115 | if pemFile != "" && p12File != "" { 116 | return options, fmt.Errorf("please supply either a pem or a p12, not both") 117 | } 118 | 119 | if pemFile != "" { 120 | cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile) 121 | if err != nil { 122 | return options, fmt.Errorf("could not load supplied pem key: %w", err) 123 | } 124 | options.TLSCertificate = &cert 125 | } else if p12File != "" { 126 | p12Content, err := os.ReadFile(p12File) 127 | if err != nil { 128 | return options, fmt.Errorf("could not read p12 %s: %w", p12File, err) 129 | } 130 | blocks, err := pkcs12.ToPEM(p12Content, p12Pass) 131 | if err != nil { 132 | return options, fmt.Errorf("could not load P12: %w", err) 133 | } 134 | var pemData []byte 135 | for _, b := range blocks { 136 | pemData = append(pemData, pem.EncodeToMemory(b)...) 137 | } 138 | cert, err := tls.X509KeyPair(pemData, pemData) 139 | if err != nil { 140 | return options, fmt.Errorf("could not load certificate from P12: %w", err) 141 | } 142 | options.TLSCertificate = &cert 143 | } 144 | 145 | return options, nil 146 | } 147 | 148 | func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error) { 149 | options := libgobuster.HTTPOptions{} 150 | var err error 151 | 152 | basic, err := parseBasicHTTPOptions(cmd) 153 | if err != nil { 154 | return options, err 155 | } 156 | options.Proxy = basic.Proxy 157 | options.Timeout = basic.Timeout 158 | options.UserAgent = basic.UserAgent 159 | options.NoTLSValidation = basic.NoTLSValidation 160 | options.RetryOnTimeout = basic.RetryOnTimeout 161 | options.RetryAttempts = basic.RetryAttempts 162 | options.TLSCertificate = basic.TLSCertificate 163 | 164 | options.URL, err = cmd.Flags().GetString("url") 165 | if err != nil { 166 | return options, fmt.Errorf("invalid value for url: %w", err) 167 | } 168 | 169 | if !strings.HasPrefix(options.URL, "http") { 170 | // check to see if a port was specified 171 | re := regexp.MustCompile(`^[^/]+:(\d+)`) 172 | match := re.FindStringSubmatch(options.URL) 173 | 174 | if len(match) < 2 { 175 | // no port, default to http on 80 176 | options.URL = fmt.Sprintf("http://%s", options.URL) 177 | } else { 178 | port, err2 := strconv.Atoi(match[1]) 179 | if err2 != nil || (port != 80 && port != 443) { 180 | return options, fmt.Errorf("url scheme not specified") 181 | } else if port == 80 { 182 | options.URL = fmt.Sprintf("http://%s", options.URL) 183 | } else { 184 | options.URL = fmt.Sprintf("https://%s", options.URL) 185 | } 186 | } 187 | } 188 | 189 | options.Cookies, err = cmd.Flags().GetString("cookies") 190 | if err != nil { 191 | return options, fmt.Errorf("invalid value for cookies: %w", err) 192 | } 193 | 194 | options.Username, err = cmd.Flags().GetString("username") 195 | if err != nil { 196 | return options, fmt.Errorf("invalid value for username: %w", err) 197 | } 198 | 199 | options.Password, err = cmd.Flags().GetString("password") 200 | if err != nil { 201 | return options, fmt.Errorf("invalid value for password: %w", err) 202 | } 203 | 204 | options.FollowRedirect, err = cmd.Flags().GetBool("follow-redirect") 205 | if err != nil { 206 | return options, fmt.Errorf("invalid value for follow-redirect: %w", err) 207 | } 208 | 209 | options.Method, err = cmd.Flags().GetString("method") 210 | if err != nil { 211 | return options, fmt.Errorf("invalid value for method: %w", err) 212 | } 213 | 214 | headers, err := cmd.Flags().GetStringArray("headers") 215 | if err != nil { 216 | return options, fmt.Errorf("invalid value for headers: %w", err) 217 | } 218 | 219 | for _, h := range headers { 220 | keyAndValue := strings.SplitN(h, ":", 2) 221 | if len(keyAndValue) != 2 { 222 | return options, fmt.Errorf("invalid header format for header %q", h) 223 | } 224 | key := strings.TrimSpace(keyAndValue[0]) 225 | value := strings.TrimSpace(keyAndValue[1]) 226 | if len(key) == 0 { 227 | return options, fmt.Errorf("invalid header format for header %q - name is empty", h) 228 | } 229 | header := libgobuster.HTTPHeader{Name: key, Value: value} 230 | options.Headers = append(options.Headers, header) 231 | } 232 | 233 | noCanonHeaders, err := cmd.Flags().GetBool("no-canonicalize-headers") 234 | if err != nil { 235 | return options, fmt.Errorf("invalid value for no-canonicalize-headers: %w", err) 236 | } 237 | options.NoCanonicalizeHeaders = noCanonHeaders 238 | 239 | // Prompt for PW if not provided 240 | if options.Username != "" && options.Password == "" { 241 | fmt.Printf("[?] Auth Password: ") 242 | // please don't remove the int cast here as it is sadly needed on windows :/ 243 | passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert 244 | // print a newline to simulate the newline that was entered 245 | // this means that formatting/printing after doesn't look bad. 246 | fmt.Println("") 247 | if err != nil { 248 | return options, fmt.Errorf("username given but reading of password failed") 249 | } 250 | options.Password = string(passBytes) 251 | } 252 | // if it's still empty bail out 253 | if options.Username != "" && options.Password == "" { 254 | return options, fmt.Errorf("username was provided but password is missing") 255 | } 256 | 257 | return options, nil 258 | } 259 | -------------------------------------------------------------------------------- /cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/signal" 10 | 11 | "github.com/OJ/gobuster/v3/libgobuster" 12 | "github.com/fatih/color" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // nolint:gochecknoglobals 17 | var rootCmd = &cobra.Command{ 18 | Use: "gobuster", 19 | SilenceUsage: true, 20 | } 21 | 22 | // nolint:gochecknoglobals 23 | var mainContext context.Context 24 | 25 | // Execute is the main cobra method 26 | func Execute() { 27 | var cancel context.CancelFunc 28 | mainContext, cancel = context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | signalChan := make(chan os.Signal, 1) 32 | signal.Notify(signalChan, os.Interrupt) 33 | defer func() { 34 | signal.Stop(signalChan) 35 | cancel() 36 | }() 37 | go func() { 38 | select { 39 | case <-signalChan: 40 | // caught CTRL+C 41 | fmt.Println("\n[!] Keyboard interrupt detected, terminating.") 42 | cancel() 43 | case <-mainContext.Done(): 44 | } 45 | }() 46 | 47 | if err := rootCmd.Execute(); err != nil { 48 | // Leaving this in results in the same error appearing twice 49 | // Once before and once after the help output. Not sure if 50 | // this is going to be needed to output other errors that 51 | // aren't automatically outputted. 52 | // fmt.Println(err) 53 | os.Exit(1) 54 | } 55 | } 56 | 57 | func parseGlobalOptions() (*libgobuster.Options, error) { 58 | globalopts := libgobuster.NewOptions() 59 | 60 | threads, err := rootCmd.Flags().GetInt("threads") 61 | if err != nil { 62 | return nil, fmt.Errorf("invalid value for threads: %w", err) 63 | } 64 | 65 | if threads <= 0 { 66 | return nil, fmt.Errorf("threads must be bigger than 0") 67 | } 68 | globalopts.Threads = threads 69 | 70 | delay, err := rootCmd.Flags().GetDuration("delay") 71 | if err != nil { 72 | return nil, fmt.Errorf("invalid value for delay: %w", err) 73 | } 74 | 75 | if delay < 0 { 76 | return nil, fmt.Errorf("delay must be positive") 77 | } 78 | globalopts.Delay = delay 79 | 80 | globalopts.Wordlist, err = rootCmd.Flags().GetString("wordlist") 81 | if err != nil { 82 | return nil, fmt.Errorf("invalid value for wordlist: %w", err) 83 | } 84 | 85 | if globalopts.Wordlist == "-" { 86 | // STDIN 87 | } else if _, err2 := os.Stat(globalopts.Wordlist); os.IsNotExist(err2) { 88 | return nil, fmt.Errorf("wordlist file %q does not exist: %w", globalopts.Wordlist, err2) 89 | } 90 | 91 | offset, err := rootCmd.Flags().GetInt("wordlist-offset") 92 | if err != nil { 93 | return nil, fmt.Errorf("invalid value for wordlist-offset: %w", err) 94 | } 95 | 96 | if offset < 0 { 97 | return nil, fmt.Errorf("wordlist-offset must be bigger or equal to 0") 98 | } 99 | globalopts.WordlistOffset = offset 100 | 101 | if globalopts.Wordlist == "-" && globalopts.WordlistOffset > 0 { 102 | return nil, fmt.Errorf("wordlist-offset is not supported when reading from STDIN") 103 | } 104 | 105 | globalopts.PatternFile, err = rootCmd.Flags().GetString("pattern") 106 | if err != nil { 107 | return nil, fmt.Errorf("invalid value for pattern: %w", err) 108 | } 109 | 110 | if globalopts.PatternFile != "" { 111 | if _, err = os.Stat(globalopts.PatternFile); os.IsNotExist(err) { 112 | return nil, fmt.Errorf("pattern file %q does not exist: %w", globalopts.PatternFile, err) 113 | } 114 | patternFile, err := os.Open(globalopts.PatternFile) 115 | if err != nil { 116 | return nil, fmt.Errorf("could not open pattern file %q: %w", globalopts.PatternFile, err) 117 | } 118 | defer patternFile.Close() 119 | 120 | scanner := bufio.NewScanner(patternFile) 121 | for scanner.Scan() { 122 | globalopts.Patterns = append(globalopts.Patterns, scanner.Text()) 123 | } 124 | if err := scanner.Err(); err != nil { 125 | return nil, fmt.Errorf("could not read pattern file %q: %w", globalopts.PatternFile, err) 126 | } 127 | } 128 | 129 | globalopts.OutputFilename, err = rootCmd.Flags().GetString("output") 130 | if err != nil { 131 | return nil, fmt.Errorf("invalid value for output filename: %w", err) 132 | } 133 | 134 | globalopts.Verbose, err = rootCmd.Flags().GetBool("verbose") 135 | if err != nil { 136 | return nil, fmt.Errorf("invalid value for verbose: %w", err) 137 | } 138 | 139 | globalopts.Quiet, err = rootCmd.Flags().GetBool("quiet") 140 | if err != nil { 141 | return nil, fmt.Errorf("invalid value for quiet: %w", err) 142 | } 143 | 144 | globalopts.NoProgress, err = rootCmd.Flags().GetBool("no-progress") 145 | if err != nil { 146 | return nil, fmt.Errorf("invalid value for no-progress: %w", err) 147 | } 148 | 149 | globalopts.NoError, err = rootCmd.Flags().GetBool("no-error") 150 | if err != nil { 151 | return nil, fmt.Errorf("invalid value for no-error: %w", err) 152 | } 153 | 154 | noColor, err := rootCmd.Flags().GetBool("no-color") 155 | if err != nil { 156 | return nil, fmt.Errorf("invalid value for no-color: %w", err) 157 | } 158 | if noColor { 159 | color.NoColor = true 160 | } 161 | 162 | globalopts.Debug, err = rootCmd.Flags().GetBool("debug") 163 | if err != nil { 164 | return nil, fmt.Errorf("invalid value for debug: %w", err) 165 | } 166 | 167 | return globalopts, nil 168 | } 169 | 170 | // This has to be called as part of the pre-run for sub commands. Including 171 | // this in the init() function results in the built-in `help` command not 172 | // working as intended. The required flags should only be marked as required 173 | // on the global flags when one of the non-help commands is used. 174 | func configureGlobalOptions() { 175 | if err := rootCmd.MarkPersistentFlagRequired("wordlist"); err != nil { 176 | log.Fatalf("error on marking flag as required: %v", err) 177 | } 178 | } 179 | 180 | // nolint:gochecknoinits 181 | func init() { 182 | rootCmd.PersistentFlags().DurationP("delay", "", 0, "Time each thread waits between requests (e.g. 1500ms)") 183 | rootCmd.PersistentFlags().IntP("threads", "t", 10, "Number of concurrent threads") 184 | rootCmd.PersistentFlags().StringP("wordlist", "w", "", "Path to the wordlist. Set to - to use STDIN.") 185 | rootCmd.PersistentFlags().IntP("wordlist-offset", "", 0, "Resume from a given position in the wordlist (defaults to 0)") 186 | rootCmd.PersistentFlags().StringP("output", "o", "", "Output file to write results to (defaults to stdout)") 187 | rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output (errors)") 188 | rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Don't print the banner and other noise") 189 | rootCmd.PersistentFlags().BoolP("no-progress", "z", false, "Don't display progress") 190 | rootCmd.PersistentFlags().Bool("no-error", false, "Don't display errors") 191 | rootCmd.PersistentFlags().StringP("pattern", "p", "", "File containing replacement patterns") 192 | rootCmd.PersistentFlags().Bool("no-color", false, "Disable color output") 193 | rootCmd.PersistentFlags().Bool("debug", false, "Enable debug output") 194 | } 195 | -------------------------------------------------------------------------------- /cli/cmd/s3.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/OJ/gobuster/v3/cli" 7 | "github.com/OJ/gobuster/v3/gobusters3" 8 | "github.com/OJ/gobuster/v3/libgobuster" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // nolint:gochecknoglobals 13 | var cmdS3 *cobra.Command 14 | 15 | func runS3(cmd *cobra.Command, args []string) error { 16 | globalopts, pluginopts, err := parseS3Options() 17 | if err != nil { 18 | return fmt.Errorf("error on parsing arguments: %w", err) 19 | } 20 | 21 | plugin, err := gobusters3.NewGobusterS3(globalopts, pluginopts) 22 | if err != nil { 23 | return fmt.Errorf("error on creating gobusters3: %w", err) 24 | } 25 | 26 | log := libgobuster.NewLogger(globalopts.Debug) 27 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 28 | log.Debugf("%#v", err) 29 | return fmt.Errorf("error on running gobuster: %w", err) 30 | } 31 | return nil 32 | } 33 | 34 | func parseS3Options() (*libgobuster.Options, *gobusters3.OptionsS3, error) { 35 | globalopts, err := parseGlobalOptions() 36 | if err != nil { 37 | return nil, nil, err 38 | } 39 | 40 | pluginOpts := gobusters3.NewOptionsS3() 41 | 42 | httpOpts, err := parseBasicHTTPOptions(cmdS3) 43 | if err != nil { 44 | return nil, nil, err 45 | } 46 | 47 | pluginOpts.UserAgent = httpOpts.UserAgent 48 | pluginOpts.Proxy = httpOpts.Proxy 49 | pluginOpts.Timeout = httpOpts.Timeout 50 | pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation 51 | pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout 52 | pluginOpts.RetryAttempts = httpOpts.RetryAttempts 53 | pluginOpts.TLSCertificate = httpOpts.TLSCertificate 54 | 55 | pluginOpts.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles") 56 | if err != nil { 57 | return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err) 58 | } 59 | 60 | return globalopts, pluginOpts, nil 61 | } 62 | 63 | // nolint:gochecknoinits 64 | func init() { 65 | cmdS3 = &cobra.Command{ 66 | Use: "s3", 67 | Short: "Uses aws bucket enumeration mode", 68 | RunE: runS3, 69 | } 70 | 71 | addBasicHTTPOptions(cmdS3) 72 | cmdS3.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets (only shown in verbose mode)") 73 | 74 | cmdS3.PersistentPreRun = func(cmd *cobra.Command, args []string) { 75 | configureGlobalOptions() 76 | } 77 | 78 | rootCmd.AddCommand(cmdS3) 79 | } 80 | -------------------------------------------------------------------------------- /cli/cmd/tftp.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "time" 8 | 9 | "github.com/OJ/gobuster/v3/cli" 10 | "github.com/OJ/gobuster/v3/gobustertftp" 11 | "github.com/OJ/gobuster/v3/libgobuster" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // nolint:gochecknoglobals 16 | var cmdTFTP *cobra.Command 17 | 18 | func runTFTP(cmd *cobra.Command, args []string) error { 19 | globalopts, pluginopts, err := parseTFTPOptions() 20 | if err != nil { 21 | return fmt.Errorf("error on parsing arguments: %w", err) 22 | } 23 | 24 | plugin, err := gobustertftp.NewGobusterTFTP(globalopts, pluginopts) 25 | if err != nil { 26 | return fmt.Errorf("error on creating gobustertftp: %w", err) 27 | } 28 | 29 | log := libgobuster.NewLogger(globalopts.Debug) 30 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 31 | log.Debugf("%#v", err) 32 | return fmt.Errorf("error on running gobuster: %w", err) 33 | } 34 | return nil 35 | } 36 | 37 | func parseTFTPOptions() (*libgobuster.Options, *gobustertftp.OptionsTFTP, error) { 38 | globalopts, err := parseGlobalOptions() 39 | if err != nil { 40 | return nil, nil, err 41 | } 42 | pluginOpts := gobustertftp.NewOptionsTFTP() 43 | 44 | pluginOpts.Server, err = cmdTFTP.Flags().GetString("server") 45 | if err != nil { 46 | return nil, nil, fmt.Errorf("invalid value for domain: %w", err) 47 | } 48 | 49 | if !strings.Contains(pluginOpts.Server, ":") { 50 | pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server) 51 | } 52 | 53 | pluginOpts.Timeout, err = cmdTFTP.Flags().GetDuration("timeout") 54 | if err != nil { 55 | return nil, nil, fmt.Errorf("invalid value for timeout: %w", err) 56 | } 57 | 58 | return globalopts, pluginOpts, nil 59 | } 60 | 61 | // nolint:gochecknoinits 62 | func init() { 63 | cmdTFTP = &cobra.Command{ 64 | Use: "tftp", 65 | Short: "Uses TFTP enumeration mode", 66 | RunE: runTFTP, 67 | } 68 | 69 | cmdTFTP.Flags().StringP("server", "s", "", "The target TFTP server") 70 | cmdTFTP.Flags().DurationP("timeout", "", time.Second, "TFTP timeout") 71 | if err := cmdTFTP.MarkFlagRequired("server"); err != nil { 72 | log.Fatalf("error on marking flag as required: %v", err) 73 | } 74 | 75 | cmdTFTP.PersistentPreRun = func(cmd *cobra.Command, args []string) { 76 | configureGlobalOptions() 77 | } 78 | 79 | rootCmd.AddCommand(cmdTFTP) 80 | } 81 | -------------------------------------------------------------------------------- /cli/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/OJ/gobuster/v3/libgobuster" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // nolint:gochecknoglobals 11 | var cmdVersion *cobra.Command 12 | 13 | func runVersion(cmd *cobra.Command, args []string) error { 14 | fmt.Println(libgobuster.VERSION) 15 | return nil 16 | } 17 | 18 | // nolint:gochecknoinits 19 | func init() { 20 | cmdVersion = &cobra.Command{ 21 | Use: "version", 22 | Short: "shows the current version", 23 | RunE: runVersion, 24 | } 25 | 26 | rootCmd.AddCommand(cmdVersion) 27 | } 28 | -------------------------------------------------------------------------------- /cli/cmd/vhost.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/OJ/gobuster/v3/cli" 8 | "github.com/OJ/gobuster/v3/gobustervhost" 9 | "github.com/OJ/gobuster/v3/libgobuster" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | // nolint:gochecknoglobals 14 | var cmdVhost *cobra.Command 15 | 16 | func runVhost(cmd *cobra.Command, args []string) error { 17 | globalopts, pluginopts, err := parseVhostOptions() 18 | if err != nil { 19 | return fmt.Errorf("error on parsing arguments: %w", err) 20 | } 21 | 22 | plugin, err := gobustervhost.NewGobusterVhost(globalopts, pluginopts) 23 | if err != nil { 24 | return fmt.Errorf("error on creating gobustervhost: %w", err) 25 | } 26 | 27 | log := libgobuster.NewLogger(globalopts.Debug) 28 | if err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil { 29 | log.Debugf("%#v", err) 30 | return fmt.Errorf("error on running gobuster: %w", err) 31 | } 32 | return nil 33 | } 34 | 35 | func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, error) { 36 | globalopts, err := parseGlobalOptions() 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | pluginOpts := gobustervhost.NewOptionsVhost() 42 | 43 | httpOpts, err := parseCommonHTTPOptions(cmdVhost) 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | pluginOpts.Password = httpOpts.Password 48 | pluginOpts.URL = httpOpts.URL 49 | pluginOpts.UserAgent = httpOpts.UserAgent 50 | pluginOpts.Username = httpOpts.Username 51 | pluginOpts.Proxy = httpOpts.Proxy 52 | pluginOpts.Cookies = httpOpts.Cookies 53 | pluginOpts.Timeout = httpOpts.Timeout 54 | pluginOpts.FollowRedirect = httpOpts.FollowRedirect 55 | pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation 56 | pluginOpts.Headers = httpOpts.Headers 57 | pluginOpts.Method = httpOpts.Method 58 | pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout 59 | pluginOpts.RetryAttempts = httpOpts.RetryAttempts 60 | pluginOpts.TLSCertificate = httpOpts.TLSCertificate 61 | pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders 62 | 63 | pluginOpts.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain") 64 | if err != nil { 65 | return nil, nil, fmt.Errorf("invalid value for append-domain: %w", err) 66 | } 67 | 68 | pluginOpts.ExcludeLength, err = cmdVhost.Flags().GetString("exclude-length") 69 | if err != nil { 70 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 71 | } 72 | ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength) 73 | if err != nil { 74 | return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err) 75 | } 76 | pluginOpts.ExcludeLengthParsed = ret 77 | 78 | pluginOpts.Domain, err = cmdVhost.Flags().GetString("domain") 79 | if err != nil { 80 | return nil, nil, fmt.Errorf("invalid value for domain: %w", err) 81 | } 82 | 83 | return globalopts, pluginOpts, nil 84 | } 85 | 86 | // nolint:gochecknoinits 87 | func init() { 88 | cmdVhost = &cobra.Command{ 89 | Use: "vhost", 90 | Short: "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)", 91 | RunE: runVhost, 92 | } 93 | if err := addCommonHTTPOptions(cmdVhost); err != nil { 94 | log.Fatalf("%v", err) 95 | } 96 | cmdVhost.Flags().BoolP("append-domain", "", false, "Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.") 97 | cmdVhost.Flags().String("exclude-length", "", "exclude the following content lengths (completely ignores the status). You can separate multiple lengths by comma and it also supports ranges like 203-206") 98 | cmdVhost.Flags().String("domain", "", "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") 99 | 100 | cmdVhost.PersistentPreRun = func(cmd *cobra.Command, args []string) { 101 | configureGlobalOptions() 102 | } 103 | 104 | rootCmd.AddCommand(cmdVhost) 105 | } 106 | -------------------------------------------------------------------------------- /cli/cmd/vhost_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/OJ/gobuster/v3/cli" 11 | "github.com/OJ/gobuster/v3/gobustervhost" 12 | "github.com/OJ/gobuster/v3/libgobuster" 13 | ) 14 | 15 | func BenchmarkVhostMode(b *testing.B) { 16 | h := httpServer(b, "test") 17 | defer h.Close() 18 | 19 | pluginopts := gobustervhost.NewOptionsVhost() 20 | pluginopts.URL = h.URL 21 | pluginopts.Timeout = 10 * time.Second 22 | 23 | wordlist, err := os.CreateTemp("", "") 24 | if err != nil { 25 | b.Fatalf("could not create tempfile: %v", err) 26 | } 27 | defer os.Remove(wordlist.Name()) 28 | for w := 0; w < 1000; w++ { 29 | _, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w)) 30 | } 31 | wordlist.Close() 32 | 33 | globalopts := libgobuster.Options{ 34 | Threads: 10, 35 | Wordlist: wordlist.Name(), 36 | NoProgress: true, 37 | } 38 | 39 | ctx := context.Background() 40 | oldStdout := os.Stdout 41 | oldStderr := os.Stderr 42 | defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr) 43 | devnull, err := os.Open(os.DevNull) 44 | if err != nil { 45 | b.Fatalf("could not get devnull %v", err) 46 | } 47 | defer devnull.Close() 48 | log := libgobuster.NewLogger(false) 49 | 50 | // Run the real benchmark 51 | for x := 0; x < b.N; x++ { 52 | os.Stdout = devnull 53 | os.Stderr = devnull 54 | plugin, err := gobustervhost.NewGobusterVhost(&globalopts, pluginopts) 55 | if err != nil { 56 | b.Fatalf("error on creating gobusterdir: %v", err) 57 | } 58 | 59 | if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil { 60 | b.Fatalf("error on running gobuster: %v", err) 61 | } 62 | os.Stdout = oldStdout 63 | os.Stderr = oldStderr 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /cli/const.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | 3 | package cli 4 | 5 | const ( 6 | TERMINAL_CLEAR_LINE = "\r\x1b[2K" 7 | ) 8 | -------------------------------------------------------------------------------- /cli/const_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | 3 | package cli 4 | 5 | const ( 6 | TERMINAL_CLEAR_LINE = "\r\r" 7 | ) 8 | -------------------------------------------------------------------------------- /cli/gobuster.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "sync" 9 | "time" 10 | 11 | "github.com/OJ/gobuster/v3/libgobuster" 12 | ) 13 | 14 | const ruler = "===============================================================" 15 | const cliProgressUpdate = 500 * time.Millisecond 16 | 17 | // resultWorker outputs the results as they come in. This needs to be a range and should not handle 18 | // the context so the channel always has a receiver and libgobuster will not block. 19 | func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) { 20 | defer wg.Done() 21 | 22 | var f *os.File 23 | var err error 24 | if filename != "" { 25 | f, err = os.Create(filename) 26 | if err != nil { 27 | g.Logger.Fatalf("error on creating output file: %v", err) 28 | } 29 | defer f.Close() 30 | } 31 | 32 | for r := range g.Progress.ResultChan { 33 | s, err := r.ResultToString() 34 | if err != nil { 35 | g.Logger.Fatal(err) 36 | } 37 | if s != "" { 38 | s = strings.TrimSpace(s) 39 | _, _ = fmt.Printf("%s%s\n", TERMINAL_CLEAR_LINE, s) 40 | if f != nil { 41 | err = writeToFile(f, s) 42 | if err != nil { 43 | g.Logger.Fatalf("error on writing output file: %v", err) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | // errorWorker outputs the errors as they come in. This needs to be a range and should not handle 51 | // the context so the channel always has a receiver and libgobuster will not block. 52 | func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) { 53 | defer wg.Done() 54 | 55 | for e := range g.Progress.ErrorChan { 56 | if !g.Opts.Quiet && !g.Opts.NoError { 57 | g.Logger.Error(e.Error()) 58 | g.Logger.Debugf("%#v", e) 59 | } 60 | } 61 | } 62 | 63 | // messageWorker outputs messages as they come in. This needs to be a range and should not handle 64 | // the context so the channel always has a receiver and libgobuster will not block. 65 | func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) { 66 | defer wg.Done() 67 | 68 | for msg := range g.Progress.MessageChan { 69 | if !g.Opts.Quiet { 70 | switch msg.Level { 71 | case libgobuster.LevelDebug: 72 | g.Logger.Debug(msg.Message) 73 | case libgobuster.LevelError: 74 | g.Logger.Error(msg.Message) 75 | case libgobuster.LevelInfo: 76 | g.Logger.Info(msg.Message) 77 | default: 78 | panic(fmt.Sprintf("invalid level %d", msg.Level)) 79 | } 80 | } 81 | } 82 | } 83 | 84 | func printProgress(g *libgobuster.Gobuster) { 85 | if !g.Opts.Quiet && !g.Opts.NoProgress { 86 | requestsIssued := g.Progress.RequestsIssued() 87 | requestsExpected := g.Progress.RequestsExpected() 88 | if g.Opts.Wordlist == "-" { 89 | s := fmt.Sprintf("%sProgress: %d", TERMINAL_CLEAR_LINE, requestsIssued) 90 | _, _ = fmt.Fprint(os.Stderr, s) 91 | // only print status if we already read in the wordlist 92 | } else if requestsExpected > 0 { 93 | s := fmt.Sprintf("%sProgress: %d / %d (%3.2f%%)", TERMINAL_CLEAR_LINE, requestsIssued, requestsExpected, float32(requestsIssued)*100.0/float32(requestsExpected)) 94 | _, _ = fmt.Fprint(os.Stderr, s) 95 | } 96 | } 97 | } 98 | 99 | // progressWorker outputs the progress every tick. It will stop once cancel() is called 100 | // on the context 101 | func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup) { 102 | defer wg.Done() 103 | 104 | tick := time.NewTicker(cliProgressUpdate) 105 | 106 | for { 107 | select { 108 | case <-tick.C: 109 | printProgress(g) 110 | case <-ctx.Done(): 111 | // print the final progress so we end at 100% 112 | printProgress(g) 113 | fmt.Println() 114 | return 115 | } 116 | } 117 | } 118 | 119 | func writeToFile(f *os.File, output string) error { 120 | _, err := f.WriteString(fmt.Sprintf("%s\n", output)) 121 | if err != nil { 122 | return fmt.Errorf("[!] Unable to write to file %w", err) 123 | } 124 | return nil 125 | } 126 | 127 | // Gobuster is the main entry point for the CLI 128 | func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster.GobusterPlugin, log libgobuster.Logger) error { 129 | // Sanity checks 130 | if opts == nil { 131 | return fmt.Errorf("please provide valid options") 132 | } 133 | 134 | if plugin == nil { 135 | return fmt.Errorf("please provide a valid plugin") 136 | } 137 | 138 | ctxCancel, cancel := context.WithCancel(ctx) 139 | defer cancel() 140 | 141 | gobuster, err := libgobuster.NewGobuster(opts, plugin, log) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | if !opts.Quiet { 147 | log.Println(ruler) 148 | log.Printf("Gobuster v%s\n", libgobuster.VERSION) 149 | log.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)") 150 | log.Println(ruler) 151 | c, err := gobuster.GetConfigString() 152 | if err != nil { 153 | return fmt.Errorf("error on creating config string: %w", err) 154 | } 155 | log.Println(c) 156 | log.Println(ruler) 157 | gobuster.Logger.Printf("Starting gobuster in %s mode", plugin.Name()) 158 | if opts.WordlistOffset > 0 { 159 | gobuster.Logger.Printf("Skipping the first %d elements...", opts.WordlistOffset) 160 | } 161 | log.Println(ruler) 162 | } 163 | 164 | // our waitgroup for all goroutines 165 | // this ensures all goroutines are finished 166 | // when we call wg.Wait() 167 | var wg sync.WaitGroup 168 | 169 | wg.Add(1) 170 | go resultWorker(gobuster, opts.OutputFilename, &wg) 171 | 172 | wg.Add(1) 173 | go errorWorker(gobuster, &wg) 174 | 175 | wg.Add(1) 176 | go messageWorker(gobuster, &wg) 177 | 178 | if !opts.Quiet && !opts.NoProgress { 179 | // if not quiet add a new workgroup entry and start the goroutine 180 | wg.Add(1) 181 | go progressWorker(ctxCancel, gobuster, &wg) 182 | } 183 | 184 | err = gobuster.Run(ctxCancel) 185 | 186 | // call cancel func so progressWorker will exit (the only goroutine in this 187 | // file using the context) and to free resources 188 | cancel() 189 | // wait for all spun up goroutines to finish (all have to call wg.Done()) 190 | wg.Wait() 191 | 192 | // Late error checking to finish all threads 193 | if err != nil { 194 | return err 195 | } 196 | 197 | if !opts.Quiet { 198 | log.Println(ruler) 199 | gobuster.Logger.Println("Finished") 200 | log.Println(ruler) 201 | } 202 | return nil 203 | } 204 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | // cSpell Settings 2 | { 3 | // Version of the setting file. Always 0.2 4 | "version": "0.2", 5 | // language - current active spelling language 6 | "language": "en", 7 | // words - list of words to be always considered correct 8 | "words": [ 9 | "libgobuster", 10 | "gobuster", 11 | "gobusters", 12 | "gobusterdir", 13 | "gobusterdns", 14 | "gobusterfuzz", 15 | "gobustervhost", 16 | "gobustergcs", 17 | "vhost", 18 | "vhosts", 19 | "cname", 20 | "uuid", 21 | "dirb", 22 | "wordlist", 23 | "wordlists", 24 | "hashcat", 25 | "Mehlmauer", 26 | "firefart", 27 | "GOPATH", 28 | "nolint", 29 | "unconvert", 30 | "unparam", 31 | "prealloc", 32 | "gochecknoglobals", 33 | "gochecknoinits", 34 | "fatih", 35 | "netip" 36 | ], 37 | // flagWords - list of words to be always considered incorrect 38 | // This is useful for offensive words and common spelling errors. 39 | // For example "hte" should be "the" 40 | "flagWords": [ 41 | "hte" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/OJ/gobuster/v3 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/google/uuid v1.3.0 8 | github.com/pin/tftp/v3 v3.0.0 9 | github.com/spf13/cobra v1.7.0 10 | golang.org/x/crypto v0.14.0 11 | golang.org/x/term v0.13.0 12 | ) 13 | 14 | require ( 15 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.19 // indirect 18 | github.com/spf13/pflag v1.0.5 // indirect 19 | golang.org/x/net v0.17.0 // indirect 20 | golang.org/x/sys v0.13.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 3 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 4 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 5 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 6 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 7 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 8 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 9 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 10 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 11 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 12 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 13 | github.com/pin/tftp/v3 v3.0.0 h1:o9cQpmWBSbgiaYXuN+qJAB12XBIv4dT7OuOONucn2l0= 14 | github.com/pin/tftp/v3 v3.0.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w= 15 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 16 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 17 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 18 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 19 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 20 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 21 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 22 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 23 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 24 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 25 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 26 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 27 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 30 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= 32 | golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /gobusterdir/gobusterdir.go: -------------------------------------------------------------------------------- 1 | package gobusterdir 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "text/tabwriter" 12 | "unicode/utf8" 13 | 14 | "github.com/OJ/gobuster/v3/libgobuster" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | // nolint:gochecknoglobals 19 | var ( 20 | backupExtensions = []string{"~", ".bak", ".bak2", ".old", ".1"} 21 | backupDotExtensions = []string{".swp"} 22 | ) 23 | 24 | // ErrWildcard is returned if a wildcard response is found 25 | type ErrWildcard struct { 26 | url string 27 | statusCode int 28 | length int64 29 | } 30 | 31 | // Error is the implementation of the error interface 32 | func (e *ErrWildcard) Error() string { 33 | return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d (Length: %d)", e.url, e.statusCode, e.length) 34 | } 35 | 36 | // GobusterDir is the main type to implement the interface 37 | type GobusterDir struct { 38 | options *OptionsDir 39 | globalopts *libgobuster.Options 40 | http *libgobuster.HTTPClient 41 | } 42 | 43 | // NewGobusterDir creates a new initialized GobusterDir 44 | func NewGobusterDir(globalopts *libgobuster.Options, opts *OptionsDir) (*GobusterDir, error) { 45 | if globalopts == nil { 46 | return nil, fmt.Errorf("please provide valid global options") 47 | } 48 | 49 | if opts == nil { 50 | return nil, fmt.Errorf("please provide valid plugin options") 51 | } 52 | 53 | g := GobusterDir{ 54 | options: opts, 55 | globalopts: globalopts, 56 | } 57 | 58 | basicOptions := libgobuster.BasicHTTPOptions{ 59 | Proxy: opts.Proxy, 60 | Timeout: opts.Timeout, 61 | UserAgent: opts.UserAgent, 62 | NoTLSValidation: opts.NoTLSValidation, 63 | RetryOnTimeout: opts.RetryOnTimeout, 64 | RetryAttempts: opts.RetryAttempts, 65 | TLSCertificate: opts.TLSCertificate, 66 | } 67 | 68 | httpOpts := libgobuster.HTTPOptions{ 69 | BasicHTTPOptions: basicOptions, 70 | FollowRedirect: opts.FollowRedirect, 71 | Username: opts.Username, 72 | Password: opts.Password, 73 | Headers: opts.Headers, 74 | NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, 75 | Cookies: opts.Cookies, 76 | Method: opts.Method, 77 | } 78 | 79 | h, err := libgobuster.NewHTTPClient(&httpOpts) 80 | if err != nil { 81 | return nil, err 82 | } 83 | g.http = h 84 | 85 | return &g, nil 86 | } 87 | 88 | // Name should return the name of the plugin 89 | func (d *GobusterDir) Name() string { 90 | return "directory enumeration" 91 | } 92 | 93 | // PreRun is the pre run implementation of gobusterdir 94 | func (d *GobusterDir) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 95 | // add trailing slash 96 | if !strings.HasSuffix(d.options.URL, "/") { 97 | d.options.URL = fmt.Sprintf("%s/", d.options.URL) 98 | } 99 | 100 | _, _, _, _, err := d.http.Request(ctx, d.options.URL, libgobuster.RequestOptions{}) 101 | if err != nil { 102 | return fmt.Errorf("unable to connect to %s: %w", d.options.URL, err) 103 | } 104 | 105 | guid := uuid.New() 106 | url := fmt.Sprintf("%s%s", d.options.URL, guid) 107 | if d.options.UseSlash { 108 | url = fmt.Sprintf("%s/", url) 109 | } 110 | 111 | wildcardResp, wildcardLength, _, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{}) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if d.options.ExcludeLengthParsed.Contains(int(wildcardLength)) { 117 | // we are done and ignore the request as the length is excluded 118 | return nil 119 | } 120 | 121 | if d.options.StatusCodesBlacklistParsed.Length() > 0 { 122 | if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) { 123 | return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength} 124 | } 125 | } else if d.options.StatusCodesParsed.Length() > 0 { 126 | if d.options.StatusCodesParsed.Contains(wildcardResp) { 127 | return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength} 128 | } 129 | } else { 130 | return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen") 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func getBackupFilenames(word string) []string { 137 | ret := make([]string, len(backupExtensions)+len(backupDotExtensions)) 138 | i := 0 139 | for _, b := range backupExtensions { 140 | ret[i] = fmt.Sprintf("%s%s", word, b) 141 | i++ 142 | } 143 | for _, b := range backupDotExtensions { 144 | ret[i] = fmt.Sprintf(".%s%s", word, b) 145 | i++ 146 | } 147 | 148 | return ret 149 | } 150 | 151 | func (d *GobusterDir) AdditionalWords(word string) []string { 152 | var words []string 153 | // build list of urls to check 154 | // 1: No extension 155 | // 2: With extension 156 | // 3: backupextension 157 | if d.options.DiscoverBackup { 158 | words = append(words, getBackupFilenames(word)...) 159 | } 160 | for ext := range d.options.ExtensionsParsed.Set { 161 | filename := fmt.Sprintf("%s.%s", word, ext) 162 | words = append(words, filename) 163 | if d.options.DiscoverBackup { 164 | words = append(words, getBackupFilenames(filename)...) 165 | } 166 | } 167 | return words 168 | } 169 | 170 | // ProcessWord is the process implementation of gobusterdir 171 | func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 172 | suffix := "" 173 | if d.options.UseSlash { 174 | suffix = "/" 175 | } 176 | entity := fmt.Sprintf("%s%s", word, suffix) 177 | 178 | // make sure the url ends with a slash 179 | if !strings.HasSuffix(d.options.URL, "/") { 180 | d.options.URL = fmt.Sprintf("%s/", d.options.URL) 181 | } 182 | // prevent double slashes by removing leading / 183 | if strings.HasPrefix(entity, "/") { 184 | // get size of first rune and trim it 185 | _, i := utf8.DecodeRuneInString(entity) 186 | entity = entity[i:] 187 | } 188 | url := fmt.Sprintf("%s%s", d.options.URL, entity) 189 | 190 | tries := 1 191 | if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 { 192 | // add it so it will be the overall max requests 193 | tries += d.options.RetryAttempts 194 | } 195 | 196 | var statusCode int 197 | var size int64 198 | var header http.Header 199 | for i := 1; i <= tries; i++ { 200 | var err error 201 | statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{}) 202 | if err != nil { 203 | // check if it's a timeout and if we should try again and try again 204 | // otherwise the timeout error is raised 205 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { 206 | continue 207 | } else if strings.Contains(err.Error(), "invalid control character in URL") { 208 | // put error in error chan so it's printed out and ignore it 209 | // so gobuster will not quit 210 | progress.ErrorChan <- err 211 | continue 212 | } else { 213 | return err 214 | } 215 | } 216 | break 217 | } 218 | 219 | if statusCode != 0 { 220 | resultStatus := false 221 | 222 | if d.options.StatusCodesBlacklistParsed.Length() > 0 { 223 | if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) { 224 | resultStatus = true 225 | } 226 | } else if d.options.StatusCodesParsed.Length() > 0 { 227 | if d.options.StatusCodesParsed.Contains(statusCode) { 228 | resultStatus = true 229 | } 230 | } else { 231 | return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen") 232 | } 233 | 234 | if (resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size))) || d.globalopts.Verbose { 235 | progress.ResultChan <- Result{ 236 | URL: d.options.URL, 237 | Path: entity, 238 | Verbose: d.globalopts.Verbose, 239 | Expanded: d.options.Expanded, 240 | NoStatus: d.options.NoStatus, 241 | HideLength: d.options.HideLength, 242 | Found: resultStatus, 243 | Header: header, 244 | StatusCode: statusCode, 245 | Size: size, 246 | } 247 | } 248 | } 249 | 250 | return nil 251 | } 252 | 253 | // GetConfigString returns the string representation of the current config 254 | func (d *GobusterDir) GetConfigString() (string, error) { 255 | var buffer bytes.Buffer 256 | bw := bufio.NewWriter(&buffer) 257 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 258 | o := d.options 259 | if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil { 260 | return "", err 261 | } 262 | 263 | if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil { 264 | return "", err 265 | } 266 | 267 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil { 268 | return "", err 269 | } 270 | 271 | if d.globalopts.Delay > 0 { 272 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil { 273 | return "", err 274 | } 275 | } 276 | 277 | wordlist := "stdin (pipe)" 278 | if d.globalopts.Wordlist != "-" { 279 | wordlist = d.globalopts.Wordlist 280 | } 281 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 282 | return "", err 283 | } 284 | 285 | if d.globalopts.PatternFile != "" { 286 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil { 287 | return "", err 288 | } 289 | } 290 | 291 | if o.StatusCodesBlacklistParsed.Length() > 0 { 292 | if _, err := fmt.Fprintf(tw, "[+] Negative Status codes:\t%s\n", o.StatusCodesBlacklistParsed.Stringify()); err != nil { 293 | return "", err 294 | } 295 | } else if o.StatusCodesParsed.Length() > 0 { 296 | if _, err := fmt.Fprintf(tw, "[+] Status codes:\t%s\n", o.StatusCodesParsed.Stringify()); err != nil { 297 | return "", err 298 | } 299 | } 300 | 301 | if len(o.ExcludeLength) > 0 { 302 | if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil { 303 | return "", err 304 | } 305 | } 306 | 307 | if o.Proxy != "" { 308 | if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { 309 | return "", err 310 | } 311 | } 312 | 313 | if o.Cookies != "" { 314 | if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil { 315 | return "", err 316 | } 317 | } 318 | 319 | if o.UserAgent != "" { 320 | if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { 321 | return "", err 322 | } 323 | } 324 | 325 | if o.HideLength { 326 | if _, err := fmt.Fprintf(tw, "[+] Show length:\tfalse\n"); err != nil { 327 | return "", err 328 | } 329 | } 330 | 331 | if o.Username != "" { 332 | if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil { 333 | return "", err 334 | } 335 | } 336 | 337 | if o.Extensions != "" || o.ExtensionsFile != "" { 338 | if _, err := fmt.Fprintf(tw, "[+] Extensions:\t%s\n", o.ExtensionsParsed.Stringify()); err != nil { 339 | return "", err 340 | } 341 | } 342 | 343 | if o.ExtensionsFile != "" { 344 | if _, err := fmt.Fprintf(tw, "[+] Extensions file:\t%s\n", o.ExtensionsFile); err != nil { 345 | return "", err 346 | } 347 | } 348 | 349 | if o.UseSlash { 350 | if _, err := fmt.Fprintf(tw, "[+] Add Slash:\ttrue\n"); err != nil { 351 | return "", err 352 | } 353 | } 354 | 355 | if o.FollowRedirect { 356 | if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil { 357 | return "", err 358 | } 359 | } 360 | 361 | if o.Expanded { 362 | if _, err := fmt.Fprintf(tw, "[+] Expanded:\ttrue\n"); err != nil { 363 | return "", err 364 | } 365 | } 366 | 367 | if o.NoStatus { 368 | if _, err := fmt.Fprintf(tw, "[+] No status:\ttrue\n"); err != nil { 369 | return "", err 370 | } 371 | } 372 | 373 | if d.globalopts.Verbose { 374 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 375 | return "", err 376 | } 377 | } 378 | 379 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 380 | return "", err 381 | } 382 | 383 | if err := tw.Flush(); err != nil { 384 | return "", fmt.Errorf("error on tostring: %w", err) 385 | } 386 | 387 | if err := bw.Flush(); err != nil { 388 | return "", fmt.Errorf("error on tostring: %w", err) 389 | } 390 | 391 | return strings.TrimSpace(buffer.String()), nil 392 | } 393 | -------------------------------------------------------------------------------- /gobusterdir/options.go: -------------------------------------------------------------------------------- 1 | package gobusterdir 2 | 3 | import ( 4 | "github.com/OJ/gobuster/v3/libgobuster" 5 | ) 6 | 7 | // OptionsDir is the struct to hold all options for this plugin 8 | type OptionsDir struct { 9 | libgobuster.HTTPOptions 10 | Extensions string 11 | ExtensionsParsed libgobuster.Set[string] 12 | ExtensionsFile string 13 | StatusCodes string 14 | StatusCodesParsed libgobuster.Set[int] 15 | StatusCodesBlacklist string 16 | StatusCodesBlacklistParsed libgobuster.Set[int] 17 | UseSlash bool 18 | HideLength bool 19 | Expanded bool 20 | NoStatus bool 21 | DiscoverBackup bool 22 | ExcludeLength string 23 | ExcludeLengthParsed libgobuster.Set[int] 24 | } 25 | 26 | // NewOptionsDir returns a new initialized OptionsDir 27 | func NewOptionsDir() *OptionsDir { 28 | return &OptionsDir{ 29 | StatusCodesParsed: libgobuster.NewSet[int](), 30 | StatusCodesBlacklistParsed: libgobuster.NewSet[int](), 31 | ExtensionsParsed: libgobuster.NewSet[string](), 32 | ExcludeLengthParsed: libgobuster.NewSet[int](), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gobusterdir/options_test.go: -------------------------------------------------------------------------------- 1 | package gobusterdir 2 | 3 | import "testing" 4 | 5 | func TestNewOptions(t *testing.T) { 6 | t.Parallel() 7 | 8 | o := NewOptionsDir() 9 | if o.StatusCodesParsed.Set == nil { 10 | t.Fatal("StatusCodesParsed not initialized") 11 | } 12 | 13 | if o.ExtensionsParsed.Set == nil { 14 | t.Fatal("ExtensionsParsed not initialized") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gobusterdir/result.go: -------------------------------------------------------------------------------- 1 | package gobusterdir 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | white = color.New(color.FgWhite).FprintfFunc() 13 | yellow = color.New(color.FgYellow).FprintfFunc() 14 | green = color.New(color.FgGreen).FprintfFunc() 15 | blue = color.New(color.FgBlue).FprintfFunc() 16 | red = color.New(color.FgRed).FprintfFunc() 17 | cyan = color.New(color.FgCyan).FprintfFunc() 18 | ) 19 | 20 | // Result represents a single result 21 | type Result struct { 22 | URL string 23 | Path string 24 | Verbose bool 25 | Expanded bool 26 | NoStatus bool 27 | HideLength bool 28 | Found bool 29 | Header http.Header 30 | StatusCode int 31 | Size int64 32 | } 33 | 34 | // ResultToString converts the Result to it's textual representation 35 | func (r Result) ResultToString() (string, error) { 36 | buf := &bytes.Buffer{} 37 | 38 | // Prefix if we're in verbose mode 39 | if r.Verbose { 40 | if r.Found { 41 | if _, err := fmt.Fprintf(buf, "Found: "); err != nil { 42 | return "", err 43 | } 44 | } else { 45 | if _, err := fmt.Fprintf(buf, "Missed: "); err != nil { 46 | return "", err 47 | } 48 | } 49 | } 50 | 51 | if r.Expanded { 52 | if _, err := fmt.Fprintf(buf, "%s", r.URL); err != nil { 53 | return "", err 54 | } 55 | } else { 56 | if _, err := fmt.Fprintf(buf, "/"); err != nil { 57 | return "", err 58 | } 59 | } 60 | if _, err := fmt.Fprintf(buf, "%-20s", r.Path); err != nil { 61 | return "", err 62 | } 63 | 64 | if !r.NoStatus { 65 | color := white 66 | if r.StatusCode == 200 { 67 | color = green 68 | } else if r.StatusCode >= 300 && r.StatusCode < 400 { 69 | color = cyan 70 | } else if r.StatusCode >= 400 && r.StatusCode < 500 { 71 | color = yellow 72 | } else if r.StatusCode >= 500 && r.StatusCode < 600 { 73 | color = red 74 | } 75 | 76 | color(buf, " (Status: %d)", r.StatusCode) 77 | } 78 | 79 | if !r.HideLength { 80 | if _, err := fmt.Fprintf(buf, " [Size: %d]", r.Size); err != nil { 81 | return "", err 82 | } 83 | } 84 | 85 | location := r.Header.Get("Location") 86 | if location != "" { 87 | blue(buf, " [--> %s]", location) 88 | } 89 | 90 | if _, err := fmt.Fprintf(buf, "\n"); err != nil { 91 | return "", err 92 | } 93 | 94 | s := buf.String() 95 | 96 | return s, nil 97 | } 98 | -------------------------------------------------------------------------------- /gobusterdns/gobusterdns.go: -------------------------------------------------------------------------------- 1 | package gobusterdns 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "net" 9 | "net/netip" 10 | "strings" 11 | "text/tabwriter" 12 | "time" 13 | 14 | "github.com/OJ/gobuster/v3/libgobuster" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | // ErrWildcard is returned if a wildcard response is found 19 | type ErrWildcard struct { 20 | wildcardIps libgobuster.Set[netip.Addr] 21 | } 22 | 23 | // Error is the implementation of the error interface 24 | func (e *ErrWildcard) Error() string { 25 | return fmt.Sprintf("the DNS Server returned the same IP for every domain. IP address(es) returned: %s", e.wildcardIps.Stringify()) 26 | } 27 | 28 | // GobusterDNS is the main type to implement the interface 29 | type GobusterDNS struct { 30 | resolver *net.Resolver 31 | globalopts *libgobuster.Options 32 | options *OptionsDNS 33 | isWildcard bool 34 | wildcardIps libgobuster.Set[netip.Addr] 35 | } 36 | 37 | func newCustomDialer(server string) func(ctx context.Context, network, address string) (net.Conn, error) { 38 | return func(ctx context.Context, network, address string) (net.Conn, error) { 39 | d := net.Dialer{} 40 | if !strings.Contains(server, ":") { 41 | server = fmt.Sprintf("%s:53", server) 42 | } 43 | return d.DialContext(ctx, "udp", server) 44 | } 45 | } 46 | 47 | // NewGobusterDNS creates a new initialized GobusterDNS 48 | func NewGobusterDNS(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, error) { 49 | if globalopts == nil { 50 | return nil, fmt.Errorf("please provide valid global options") 51 | } 52 | 53 | if opts == nil { 54 | return nil, fmt.Errorf("please provide valid plugin options") 55 | } 56 | 57 | resolver := net.DefaultResolver 58 | if opts.Resolver != "" { 59 | resolver = &net.Resolver{ 60 | PreferGo: true, 61 | Dial: newCustomDialer(opts.Resolver), 62 | } 63 | } 64 | 65 | g := GobusterDNS{ 66 | options: opts, 67 | globalopts: globalopts, 68 | wildcardIps: libgobuster.NewSet[netip.Addr](), 69 | resolver: resolver, 70 | } 71 | return &g, nil 72 | } 73 | 74 | // Name should return the name of the plugin 75 | func (d *GobusterDNS) Name() string { 76 | return "DNS enumeration" 77 | } 78 | 79 | // PreRun is the pre run implementation of gobusterdns 80 | func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 81 | // Resolve a subdomain that probably shouldn't exist 82 | guid := uuid.New() 83 | wildcardIps, err := d.dnsLookup(ctx, fmt.Sprintf("%s.%s", guid, d.options.Domain)) 84 | if err == nil { 85 | d.isWildcard = true 86 | d.wildcardIps.AddRange(wildcardIps) 87 | if !d.options.WildcardForced { 88 | return &ErrWildcard{wildcardIps: d.wildcardIps} 89 | } 90 | } 91 | 92 | if !d.globalopts.Quiet { 93 | // Provide a warning if the base domain doesn't resolve (in case of typo) 94 | _, err = d.dnsLookup(ctx, d.options.Domain) 95 | if err != nil { 96 | // Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.yp.to` does! 97 | progress.MessageChan <- libgobuster.Message{ 98 | Level: libgobuster.LevelInfo, 99 | Message: fmt.Sprintf("[-] Unable to validate base domain: %s (%v)", d.options.Domain, err), 100 | } 101 | progress.MessageChan <- libgobuster.Message{ 102 | Level: libgobuster.LevelDebug, 103 | Message: fmt.Sprintf("%#v", err), 104 | } 105 | } 106 | } 107 | 108 | return nil 109 | } 110 | 111 | // ProcessWord is the process implementation of gobusterdns 112 | func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 113 | subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain) 114 | if !d.options.NoFQDN && !strings.HasSuffix(subdomain, ".") { 115 | // add a . to indicate this is the full domain and we do not want to traverse the search domains on the system 116 | subdomain = fmt.Sprintf("%s.", subdomain) 117 | } 118 | ips, err := d.dnsLookup(ctx, subdomain) 119 | if err == nil { 120 | if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) { 121 | result := Result{ 122 | Subdomain: subdomain, 123 | Found: true, 124 | ShowIPs: d.options.ShowIPs, 125 | ShowCNAME: d.options.ShowCNAME, 126 | NoFQDN: d.options.NoFQDN, 127 | } 128 | if d.options.ShowIPs { 129 | result.IPs = ips 130 | } else if d.options.ShowCNAME { 131 | cname, err := d.dnsLookupCname(ctx, subdomain) 132 | if err == nil { 133 | result.CNAME = cname 134 | } 135 | } 136 | progress.ResultChan <- result 137 | } 138 | } else if d.globalopts.Verbose { 139 | progress.ResultChan <- Result{ 140 | Subdomain: subdomain, 141 | Found: false, 142 | ShowIPs: d.options.ShowIPs, 143 | ShowCNAME: d.options.ShowCNAME, 144 | } 145 | } 146 | return nil 147 | } 148 | 149 | func (d *GobusterDNS) AdditionalWords(word string) []string { 150 | return []string{} 151 | } 152 | 153 | // GetConfigString returns the string representation of the current config 154 | func (d *GobusterDNS) GetConfigString() (string, error) { 155 | var buffer bytes.Buffer 156 | bw := bufio.NewWriter(&buffer) 157 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 158 | o := d.options 159 | 160 | if _, err := fmt.Fprintf(tw, "[+] Domain:\t%s\n", o.Domain); err != nil { 161 | return "", err 162 | } 163 | 164 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil { 165 | return "", err 166 | } 167 | 168 | if d.globalopts.Delay > 0 { 169 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil { 170 | return "", err 171 | } 172 | } 173 | 174 | if o.Resolver != "" { 175 | if _, err := fmt.Fprintf(tw, "[+] Resolver:\t%s\n", o.Resolver); err != nil { 176 | return "", err 177 | } 178 | } 179 | 180 | if o.ShowCNAME { 181 | if _, err := fmt.Fprintf(tw, "[+] Show CNAME:\ttrue\n"); err != nil { 182 | return "", err 183 | } 184 | } 185 | 186 | if o.ShowIPs { 187 | if _, err := fmt.Fprintf(tw, "[+] Show IPs:\ttrue\n"); err != nil { 188 | return "", err 189 | } 190 | } 191 | 192 | if o.WildcardForced { 193 | if _, err := fmt.Fprintf(tw, "[+] Wildcard forced:\ttrue\n"); err != nil { 194 | return "", err 195 | } 196 | } 197 | 198 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 199 | return "", err 200 | } 201 | 202 | wordlist := "stdin (pipe)" 203 | if d.globalopts.Wordlist != "-" { 204 | wordlist = d.globalopts.Wordlist 205 | } 206 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 207 | return "", err 208 | } 209 | 210 | if d.globalopts.PatternFile != "" { 211 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil { 212 | return "", err 213 | } 214 | } 215 | 216 | if d.globalopts.Verbose { 217 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 218 | return "", err 219 | } 220 | } 221 | 222 | if err := tw.Flush(); err != nil { 223 | return "", fmt.Errorf("error on tostring: %w", err) 224 | } 225 | 226 | if err := bw.Flush(); err != nil { 227 | return "", fmt.Errorf("error on tostring: %w", err) 228 | } 229 | 230 | return strings.TrimSpace(buffer.String()), nil 231 | } 232 | 233 | func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) { 234 | ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout) 235 | defer cancel() 236 | return d.resolver.LookupNetIP(ctx2, "ip", domain) 237 | } 238 | 239 | func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) { 240 | ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout) 241 | defer cancel() 242 | time.Sleep(time.Second) 243 | return d.resolver.LookupCNAME(ctx2, domain) 244 | } 245 | -------------------------------------------------------------------------------- /gobusterdns/options.go: -------------------------------------------------------------------------------- 1 | package gobusterdns 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // OptionsDNS holds all options for the dns plugin 8 | type OptionsDNS struct { 9 | Domain string 10 | ShowIPs bool 11 | ShowCNAME bool 12 | WildcardForced bool 13 | Resolver string 14 | NoFQDN bool 15 | Timeout time.Duration 16 | } 17 | 18 | // NewOptionsDNS returns a new initialized OptionsDNS 19 | func NewOptionsDNS() *OptionsDNS { 20 | return &OptionsDNS{} 21 | } 22 | -------------------------------------------------------------------------------- /gobusterdns/result.go: -------------------------------------------------------------------------------- 1 | package gobusterdns 2 | 3 | import ( 4 | "bytes" 5 | "net/netip" 6 | "strings" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | yellow = color.New(color.FgYellow).FprintfFunc() 13 | green = color.New(color.FgGreen).FprintfFunc() 14 | ) 15 | 16 | // Result represents a single result 17 | type Result struct { 18 | ShowIPs bool 19 | ShowCNAME bool 20 | Found bool 21 | Subdomain string 22 | NoFQDN bool 23 | IPs []netip.Addr 24 | CNAME string 25 | } 26 | 27 | // ResultToString converts the Result to it's textual representation 28 | func (r Result) ResultToString() (string, error) { 29 | buf := &bytes.Buffer{} 30 | 31 | c := green 32 | 33 | if !r.NoFQDN { 34 | r.Subdomain = strings.TrimSuffix(r.Subdomain, ".") 35 | } 36 | if r.Found { 37 | c(buf, "Found: ") 38 | } else { 39 | c = yellow 40 | c(buf, "Missed: ") 41 | } 42 | 43 | if r.ShowIPs && r.Found { 44 | ips := make([]string, len(r.IPs)) 45 | for i := range r.IPs { 46 | ips[i] = r.IPs[i].String() 47 | } 48 | c(buf, "%s [%s]\n", r.Subdomain, strings.Join(ips, ",")) 49 | } else if r.ShowCNAME && r.Found && r.CNAME != "" { 50 | c(buf, "%s [%s]\n", r.Subdomain, r.CNAME) 51 | } else { 52 | c(buf, "%s\n", r.Subdomain) 53 | } 54 | 55 | s := buf.String() 56 | return s, nil 57 | } 58 | -------------------------------------------------------------------------------- /gobusterfuzz/gobusterfuzz.go: -------------------------------------------------------------------------------- 1 | package gobusterfuzz 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "net" 9 | "strings" 10 | "text/tabwriter" 11 | 12 | "github.com/OJ/gobuster/v3/libgobuster" 13 | ) 14 | 15 | const FuzzKeyword = "FUZZ" 16 | 17 | // ErrWildcard is returned if a wildcard response is found 18 | type ErrWildcard struct { 19 | url string 20 | statusCode int 21 | } 22 | 23 | // Error is the implementation of the error interface 24 | func (e *ErrWildcard) Error() string { 25 | return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d", e.url, e.statusCode) 26 | } 27 | 28 | // GobusterFuzz is the main type to implement the interface 29 | type GobusterFuzz struct { 30 | options *OptionsFuzz 31 | globalopts *libgobuster.Options 32 | http *libgobuster.HTTPClient 33 | } 34 | 35 | // NewGobusterFuzz creates a new initialized GobusterFuzz 36 | func NewGobusterFuzz(globalopts *libgobuster.Options, opts *OptionsFuzz) (*GobusterFuzz, error) { 37 | if globalopts == nil { 38 | return nil, fmt.Errorf("please provide valid global options") 39 | } 40 | 41 | if opts == nil { 42 | return nil, fmt.Errorf("please provide valid plugin options") 43 | } 44 | 45 | g := GobusterFuzz{ 46 | options: opts, 47 | globalopts: globalopts, 48 | } 49 | 50 | basicOptions := libgobuster.BasicHTTPOptions{ 51 | Proxy: opts.Proxy, 52 | Timeout: opts.Timeout, 53 | UserAgent: opts.UserAgent, 54 | NoTLSValidation: opts.NoTLSValidation, 55 | RetryOnTimeout: opts.RetryOnTimeout, 56 | RetryAttempts: opts.RetryAttempts, 57 | TLSCertificate: opts.TLSCertificate, 58 | } 59 | 60 | httpOpts := libgobuster.HTTPOptions{ 61 | BasicHTTPOptions: basicOptions, 62 | FollowRedirect: opts.FollowRedirect, 63 | Username: opts.Username, 64 | Password: opts.Password, 65 | Headers: opts.Headers, 66 | NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, 67 | Cookies: opts.Cookies, 68 | Method: opts.Method, 69 | } 70 | 71 | h, err := libgobuster.NewHTTPClient(&httpOpts) 72 | if err != nil { 73 | return nil, err 74 | } 75 | g.http = h 76 | return &g, nil 77 | } 78 | 79 | // Name should return the name of the plugin 80 | func (d *GobusterFuzz) Name() string { 81 | return "fuzzing" 82 | } 83 | 84 | // PreRun is the pre run implementation of gobusterfuzz 85 | func (d *GobusterFuzz) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 86 | return nil 87 | } 88 | 89 | // ProcessWord is the process implementation of gobusterfuzz 90 | func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 91 | url := strings.ReplaceAll(d.options.URL, FuzzKeyword, word) 92 | 93 | requestOptions := libgobuster.RequestOptions{} 94 | 95 | if len(d.options.Headers) > 0 { 96 | requestOptions.ModifiedHeaders = make([]libgobuster.HTTPHeader, len(d.options.Headers)) 97 | for i := range d.options.Headers { 98 | requestOptions.ModifiedHeaders[i] = libgobuster.HTTPHeader{ 99 | Name: strings.ReplaceAll(d.options.Headers[i].Name, FuzzKeyword, word), 100 | Value: strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word), 101 | } 102 | } 103 | } 104 | 105 | if d.options.RequestBody != "" { 106 | data := strings.ReplaceAll(d.options.RequestBody, FuzzKeyword, word) 107 | buffer := strings.NewReader(data) 108 | requestOptions.Body = buffer 109 | } 110 | 111 | // fuzzing of basic auth 112 | if strings.Contains(d.options.Username, FuzzKeyword) || strings.Contains(d.options.Password, FuzzKeyword) { 113 | requestOptions.UpdatedBasicAuthUsername = strings.ReplaceAll(d.options.Username, FuzzKeyword, word) 114 | requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word) 115 | } 116 | 117 | tries := 1 118 | if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 { 119 | // add it so it will be the overall max requests 120 | tries += d.options.RetryAttempts 121 | } 122 | 123 | var statusCode int 124 | var size int64 125 | for i := 1; i <= tries; i++ { 126 | var err error 127 | statusCode, size, _, _, err = d.http.Request(ctx, url, requestOptions) 128 | if err != nil { 129 | // check if it's a timeout and if we should try again and try again 130 | // otherwise the timeout error is raised 131 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { 132 | continue 133 | } else if strings.Contains(err.Error(), "invalid control character in URL") { 134 | // put error in error chan so it's printed out and ignore it 135 | // so gobuster will not quit 136 | progress.ErrorChan <- err 137 | continue 138 | } else { 139 | return err 140 | } 141 | } 142 | break 143 | } 144 | 145 | if statusCode != 0 { 146 | resultStatus := true 147 | 148 | if d.options.ExcludeLengthParsed.Contains(int(size)) { 149 | resultStatus = false 150 | } 151 | 152 | if d.options.ExcludedStatusCodesParsed.Length() > 0 { 153 | if d.options.ExcludedStatusCodesParsed.Contains(statusCode) { 154 | resultStatus = false 155 | } 156 | } 157 | 158 | if resultStatus || d.globalopts.Verbose { 159 | progress.ResultChan <- Result{ 160 | Verbose: d.globalopts.Verbose, 161 | Found: resultStatus, 162 | Path: url, 163 | StatusCode: statusCode, 164 | Size: size, 165 | Word: word, 166 | } 167 | } 168 | } 169 | return nil 170 | } 171 | 172 | func (d *GobusterFuzz) AdditionalWords(word string) []string { 173 | return []string{} 174 | } 175 | 176 | // GetConfigString returns the string representation of the current config 177 | func (d *GobusterFuzz) GetConfigString() (string, error) { 178 | var buffer bytes.Buffer 179 | bw := bufio.NewWriter(&buffer) 180 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 181 | o := d.options 182 | if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil { 183 | return "", err 184 | } 185 | 186 | if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil { 187 | return "", err 188 | } 189 | 190 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil { 191 | return "", err 192 | } 193 | 194 | if d.globalopts.Delay > 0 { 195 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil { 196 | return "", err 197 | } 198 | } 199 | 200 | wordlist := "stdin (pipe)" 201 | if d.globalopts.Wordlist != "-" { 202 | wordlist = d.globalopts.Wordlist 203 | } 204 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 205 | return "", err 206 | } 207 | 208 | if d.globalopts.PatternFile != "" { 209 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil { 210 | return "", err 211 | } 212 | } 213 | 214 | if o.ExcludedStatusCodesParsed.Length() > 0 { 215 | if _, err := fmt.Fprintf(tw, "[+] Excluded Status codes:\t%s\n", o.ExcludedStatusCodesParsed.Stringify()); err != nil { 216 | return "", err 217 | } 218 | } 219 | 220 | if len(o.ExcludeLength) > 0 { 221 | if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil { 222 | return "", err 223 | } 224 | } 225 | 226 | if o.Proxy != "" { 227 | if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { 228 | return "", err 229 | } 230 | } 231 | 232 | if o.Cookies != "" { 233 | if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil { 234 | return "", err 235 | } 236 | } 237 | 238 | if o.UserAgent != "" { 239 | if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { 240 | return "", err 241 | } 242 | } 243 | 244 | if o.Username != "" { 245 | if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil { 246 | return "", err 247 | } 248 | } 249 | 250 | if o.FollowRedirect { 251 | if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil { 252 | return "", err 253 | } 254 | } 255 | 256 | if d.globalopts.Verbose { 257 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 258 | return "", err 259 | } 260 | } 261 | 262 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 263 | return "", err 264 | } 265 | 266 | if err := tw.Flush(); err != nil { 267 | return "", fmt.Errorf("error on tostring: %w", err) 268 | } 269 | 270 | if err := bw.Flush(); err != nil { 271 | return "", fmt.Errorf("error on tostring: %w", err) 272 | } 273 | 274 | return strings.TrimSpace(buffer.String()), nil 275 | } 276 | -------------------------------------------------------------------------------- /gobusterfuzz/options.go: -------------------------------------------------------------------------------- 1 | package gobusterfuzz 2 | 3 | import ( 4 | "github.com/OJ/gobuster/v3/libgobuster" 5 | ) 6 | 7 | // OptionsFuzz is the struct to hold all options for this plugin 8 | type OptionsFuzz struct { 9 | libgobuster.HTTPOptions 10 | ExcludedStatusCodes string 11 | ExcludedStatusCodesParsed libgobuster.Set[int] 12 | ExcludeLength string 13 | ExcludeLengthParsed libgobuster.Set[int] 14 | RequestBody string 15 | } 16 | 17 | // NewOptionsFuzz returns a new initialized OptionsFuzz 18 | func NewOptionsFuzz() *OptionsFuzz { 19 | return &OptionsFuzz{ 20 | ExcludedStatusCodesParsed: libgobuster.NewSet[int](), 21 | ExcludeLengthParsed: libgobuster.NewSet[int](), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /gobusterfuzz/options_test.go: -------------------------------------------------------------------------------- 1 | package gobusterfuzz 2 | 3 | import "testing" 4 | 5 | func TestNewOptions(t *testing.T) { 6 | t.Parallel() 7 | 8 | o := NewOptionsFuzz() 9 | if o.ExcludedStatusCodesParsed.Set == nil { 10 | t.Fatal("StatusCodesParsed not initialized") 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /gobusterfuzz/result.go: -------------------------------------------------------------------------------- 1 | package gobusterfuzz 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | var ( 10 | yellow = color.New(color.FgYellow).FprintfFunc() 11 | green = color.New(color.FgGreen).FprintfFunc() 12 | ) 13 | 14 | // Result represents a single result 15 | type Result struct { 16 | Word string 17 | Verbose bool 18 | Found bool 19 | Path string 20 | StatusCode int 21 | Size int64 22 | } 23 | 24 | // ResultToString converts the Result to it's textual representation 25 | func (r Result) ResultToString() (string, error) { 26 | buf := &bytes.Buffer{} 27 | 28 | c := green 29 | 30 | // Prefix if we're in verbose mode 31 | if r.Verbose { 32 | if r.Found { 33 | c(buf, "Found: ") 34 | } else { 35 | c = yellow 36 | c(buf, "Missed: ") 37 | } 38 | } else if r.Found { 39 | c(buf, "Found: ") 40 | } 41 | 42 | c(buf, "[Status=%d] [Length=%d] [Word=%s] %s", r.StatusCode, r.Size, r.Word, r.Path) 43 | c(buf, "\n") 44 | 45 | s := buf.String() 46 | return s, nil 47 | } 48 | -------------------------------------------------------------------------------- /gobustergcs/gobustersgcs.go: -------------------------------------------------------------------------------- 1 | package gobustergcs 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "regexp" 12 | "strings" 13 | "text/tabwriter" 14 | 15 | "github.com/OJ/gobuster/v3/libgobuster" 16 | ) 17 | 18 | // GobusterGCS is the main type to implement the interface 19 | type GobusterGCS struct { 20 | options *OptionsGCS 21 | globalopts *libgobuster.Options 22 | http *libgobuster.HTTPClient 23 | bucketRegex *regexp.Regexp 24 | } 25 | 26 | // NewGobusterGCS creates a new initialized GobusterGCS 27 | func NewGobusterGCS(globalopts *libgobuster.Options, opts *OptionsGCS) (*GobusterGCS, error) { 28 | if globalopts == nil { 29 | return nil, fmt.Errorf("please provide valid global options") 30 | } 31 | 32 | if opts == nil { 33 | return nil, fmt.Errorf("please provide valid plugin options") 34 | } 35 | 36 | g := GobusterGCS{ 37 | options: opts, 38 | globalopts: globalopts, 39 | } 40 | 41 | basicOptions := libgobuster.BasicHTTPOptions{ 42 | Proxy: opts.Proxy, 43 | Timeout: opts.Timeout, 44 | UserAgent: opts.UserAgent, 45 | NoTLSValidation: opts.NoTLSValidation, 46 | RetryOnTimeout: opts.RetryOnTimeout, 47 | RetryAttempts: opts.RetryAttempts, 48 | TLSCertificate: opts.TLSCertificate, 49 | } 50 | 51 | httpOpts := libgobuster.HTTPOptions{ 52 | BasicHTTPOptions: basicOptions, 53 | // needed so we can list bucket contents 54 | FollowRedirect: true, 55 | } 56 | 57 | h, err := libgobuster.NewHTTPClient(&httpOpts) 58 | if err != nil { 59 | return nil, err 60 | } 61 | g.http = h 62 | // https://cloud.google.com/storage/docs/naming-buckets 63 | 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])*$`) 64 | 65 | return &g, nil 66 | } 67 | 68 | // Name should return the name of the plugin 69 | func (s *GobusterGCS) Name() string { 70 | return "GCS bucket enumeration" 71 | } 72 | 73 | // PreRun is the pre run implementation of GobusterS3 74 | func (s *GobusterGCS) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 75 | return nil 76 | } 77 | 78 | // ProcessWord is the process implementation of GobusterS3 79 | func (s *GobusterGCS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 80 | // only check for valid bucket names 81 | if !s.isValidBucketName(word) { 82 | return nil 83 | } 84 | 85 | bucketURL := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o?maxResults=%d", word, s.options.MaxFilesToList) 86 | 87 | tries := 1 88 | if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 { 89 | // add it so it will be the overall max requests 90 | tries += s.options.RetryAttempts 91 | } 92 | 93 | var statusCode int 94 | var body []byte 95 | for i := 1; i <= tries; i++ { 96 | var err error 97 | statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true}) 98 | if err != nil { 99 | // check if it's a timeout and if we should try again and try again 100 | // otherwise the timeout error is raised 101 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { 102 | continue 103 | } else if strings.Contains(err.Error(), "invalid control character in URL") { 104 | // put error in error chan so it's printed out and ignore it 105 | // so gobuster will not quit 106 | progress.ErrorChan <- err 107 | continue 108 | } else { 109 | return err 110 | } 111 | } 112 | break 113 | } 114 | 115 | if statusCode == 0 || body == nil { 116 | return nil 117 | } 118 | 119 | // looks like 401, 403, and 404 are the only negative status codes 120 | found := false 121 | switch statusCode { 122 | case http.StatusUnauthorized, 123 | http.StatusForbidden, 124 | http.StatusNotFound: 125 | found = false 126 | case http.StatusOK: 127 | // listing enabled 128 | found = true 129 | default: 130 | // default to found as we use negative status codes 131 | found = true 132 | } 133 | 134 | // nothing found, bail out 135 | // may add the result later if we want to enable verbose output 136 | if !found { 137 | return nil 138 | } 139 | 140 | extraStr := "" 141 | if s.globalopts.Verbose { 142 | // get status 143 | var result map[string]interface{} 144 | err := json.Unmarshal(body, &result) 145 | 146 | if err != nil { 147 | return fmt.Errorf("could not parse response json: %w", err) 148 | } 149 | 150 | if _, exist := result["error"]; exist { 151 | // https://cloud.google.com/storage/docs/json_api/v1/status-codes 152 | gcsError := GCSError{} 153 | err := json.Unmarshal(body, &gcsError) 154 | if err != nil { 155 | return fmt.Errorf("could not parse error json: %w", err) 156 | } 157 | extraStr = fmt.Sprintf("Error: %s (%d)", gcsError.Error.Message, gcsError.Error.Code) 158 | } else if v, exist := result["kind"]; exist && v == "storage#objects" { 159 | // https://cloud.google.com/storage/docs/json_api/v1/status-codes 160 | // bucket listing enabled 161 | gcsListing := GCSListing{} 162 | err := json.Unmarshal(body, &gcsListing) 163 | if err != nil { 164 | return fmt.Errorf("could not parse result json: %w", err) 165 | } 166 | extraStr = "Bucket Listing enabled: " 167 | for _, x := range gcsListing.Items { 168 | extraStr += fmt.Sprintf("%s (%sb), ", x.Name, x.Size) 169 | } 170 | extraStr = strings.TrimRight(extraStr, ", ") 171 | } 172 | } 173 | 174 | progress.ResultChan <- Result{ 175 | Found: found, 176 | BucketName: word, 177 | Status: extraStr, 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (s *GobusterGCS) AdditionalWords(word string) []string { 184 | return []string{} 185 | } 186 | 187 | // GetConfigString returns the string representation of the current config 188 | func (s *GobusterGCS) GetConfigString() (string, error) { 189 | var buffer bytes.Buffer 190 | bw := bufio.NewWriter(&buffer) 191 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 192 | o := s.options 193 | 194 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", s.globalopts.Threads); err != nil { 195 | return "", err 196 | } 197 | 198 | if s.globalopts.Delay > 0 { 199 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil { 200 | return "", err 201 | } 202 | } 203 | 204 | wordlist := "stdin (pipe)" 205 | if s.globalopts.Wordlist != "-" { 206 | wordlist = s.globalopts.Wordlist 207 | } 208 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 209 | return "", err 210 | } 211 | 212 | if s.globalopts.PatternFile != "" { 213 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil { 214 | return "", err 215 | } 216 | } 217 | 218 | if o.Proxy != "" { 219 | if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { 220 | return "", err 221 | } 222 | } 223 | 224 | if o.UserAgent != "" { 225 | if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { 226 | return "", err 227 | } 228 | } 229 | 230 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 231 | return "", err 232 | } 233 | 234 | if s.globalopts.Verbose { 235 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 236 | return "", err 237 | } 238 | } 239 | 240 | if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil { 241 | return "", err 242 | } 243 | 244 | if err := tw.Flush(); err != nil { 245 | return "", fmt.Errorf("error on tostring: %w", err) 246 | } 247 | 248 | if err := bw.Flush(); err != nil { 249 | return "", fmt.Errorf("error on tostring: %w", err) 250 | } 251 | 252 | return strings.TrimSpace(buffer.String()), nil 253 | } 254 | 255 | // https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html 256 | func (s *GobusterGCS) isValidBucketName(bucketName string) bool { 257 | if len(bucketName) > 222 || !s.bucketRegex.MatchString(bucketName) { 258 | return false 259 | } 260 | if strings.HasPrefix(bucketName, "-") || strings.HasSuffix(bucketName, "-") || 261 | strings.HasPrefix(bucketName, "_") || strings.HasSuffix(bucketName, "_") || 262 | strings.HasPrefix(bucketName, ".") || strings.HasSuffix(bucketName, ".") { 263 | return false 264 | } 265 | return true 266 | } 267 | -------------------------------------------------------------------------------- /gobustergcs/options.go: -------------------------------------------------------------------------------- 1 | package gobustergcs 2 | 3 | import ( 4 | "github.com/OJ/gobuster/v3/libgobuster" 5 | ) 6 | 7 | // OptionsGCS is the struct to hold all options for this plugin 8 | type OptionsGCS struct { 9 | libgobuster.BasicHTTPOptions 10 | MaxFilesToList int 11 | } 12 | 13 | // NewOptionsGCS returns a new initialized OptionsS3 14 | func NewOptionsGCS() *OptionsGCS { 15 | return &OptionsGCS{} 16 | } 17 | -------------------------------------------------------------------------------- /gobustergcs/result.go: -------------------------------------------------------------------------------- 1 | package gobustergcs 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | var ( 10 | green = color.New(color.FgGreen).FprintfFunc() 11 | ) 12 | 13 | // Result represents a single result 14 | type Result struct { 15 | Found bool 16 | BucketName string 17 | Status string 18 | } 19 | 20 | // ResultToString converts the Result to it's textual representation 21 | func (r Result) ResultToString() (string, error) { 22 | buf := &bytes.Buffer{} 23 | 24 | c := green 25 | 26 | c(buf, "https://storage.googleapis.com/storage/v1/b/%s/o", r.BucketName) 27 | 28 | if r.Status != "" { 29 | c(buf, " [%s]", r.Status) 30 | } 31 | c(buf, "\n") 32 | 33 | str := buf.String() 34 | return str, nil 35 | } 36 | -------------------------------------------------------------------------------- /gobustergcs/types.go: -------------------------------------------------------------------------------- 1 | package gobustergcs 2 | 3 | // GCSError represents a returned error from GCS 4 | type GCSError struct { 5 | Error struct { 6 | Code int `json:"code"` 7 | Message string `json:"message"` 8 | Errors []struct { 9 | Message string `json:"message"` 10 | Reason string `json:"reason"` 11 | LocationType string `json:"locationType"` 12 | Location string `json:"location"` 13 | } `json:"errors"` 14 | } `json:"error"` 15 | } 16 | 17 | // GCSListing contains only a subset of returned properties 18 | type GCSListing struct { 19 | IsTruncated string `json:"nextPageToken"` 20 | Items []struct { 21 | Name string `json:"name"` 22 | LastModified string `json:"updated"` 23 | Size string `json:"size"` 24 | } `json:"items"` 25 | } 26 | -------------------------------------------------------------------------------- /gobusters3/gobusters3.go: -------------------------------------------------------------------------------- 1 | package gobusters3 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "encoding/xml" 8 | "fmt" 9 | "net" 10 | "net/http" 11 | "regexp" 12 | "strings" 13 | "text/tabwriter" 14 | 15 | "github.com/OJ/gobuster/v3/libgobuster" 16 | ) 17 | 18 | // GobusterS3 is the main type to implement the interface 19 | type GobusterS3 struct { 20 | options *OptionsS3 21 | globalopts *libgobuster.Options 22 | http *libgobuster.HTTPClient 23 | bucketRegex *regexp.Regexp 24 | } 25 | 26 | // NewGobusterS3 creates a new initialized GobusterS3 27 | func NewGobusterS3(globalopts *libgobuster.Options, opts *OptionsS3) (*GobusterS3, error) { 28 | if globalopts == nil { 29 | return nil, fmt.Errorf("please provide valid global options") 30 | } 31 | 32 | if opts == nil { 33 | return nil, fmt.Errorf("please provide valid plugin options") 34 | } 35 | 36 | g := GobusterS3{ 37 | options: opts, 38 | globalopts: globalopts, 39 | } 40 | 41 | basicOptions := libgobuster.BasicHTTPOptions{ 42 | Proxy: opts.Proxy, 43 | Timeout: opts.Timeout, 44 | UserAgent: opts.UserAgent, 45 | NoTLSValidation: opts.NoTLSValidation, 46 | RetryOnTimeout: opts.RetryOnTimeout, 47 | RetryAttempts: opts.RetryAttempts, 48 | TLSCertificate: opts.TLSCertificate, 49 | } 50 | 51 | httpOpts := libgobuster.HTTPOptions{ 52 | BasicHTTPOptions: basicOptions, 53 | // needed so we can list bucket contents 54 | FollowRedirect: true, 55 | } 56 | 57 | h, err := libgobuster.NewHTTPClient(&httpOpts) 58 | if err != nil { 59 | return nil, err 60 | } 61 | g.http = h 62 | g.bucketRegex = regexp.MustCompile(`^[a-z0-9\-.]{3,63}$`) 63 | 64 | return &g, nil 65 | } 66 | 67 | // Name should return the name of the plugin 68 | func (s *GobusterS3) Name() string { 69 | return "S3 bucket enumeration" 70 | } 71 | 72 | // PreRun is the pre run implementation of GobusterS3 73 | func (s *GobusterS3) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 74 | return nil 75 | } 76 | 77 | // ProcessWord is the process implementation of GobusterS3 78 | func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 79 | // only check for valid bucket names 80 | if !s.isValidBucketName(word) { 81 | return nil 82 | } 83 | 84 | bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", word, s.options.MaxFilesToList) 85 | 86 | tries := 1 87 | if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 { 88 | // add it so it will be the overall max requests 89 | tries += s.options.RetryAttempts 90 | } 91 | 92 | var statusCode int 93 | var body []byte 94 | for i := 1; i <= tries; i++ { 95 | var err error 96 | statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true}) 97 | if err != nil { 98 | // check if it's a timeout and if we should try again and try again 99 | // otherwise the timeout error is raised 100 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { 101 | continue 102 | } else if strings.Contains(err.Error(), "invalid control character in URL") { 103 | // put error in error chan so it's printed out and ignore it 104 | // so gobuster will not quit 105 | progress.ErrorChan <- err 106 | continue 107 | } else { 108 | return err 109 | } 110 | } 111 | break 112 | } 113 | 114 | if statusCode == 0 || body == nil { 115 | return nil 116 | } 117 | 118 | // looks like 404 and 400 are the only negative status codes 119 | found := false 120 | switch statusCode { 121 | case http.StatusBadRequest: 122 | case http.StatusNotFound: 123 | found = false 124 | case http.StatusOK: 125 | // listing enabled 126 | found = true 127 | // parse xml 128 | default: 129 | // default to found as we use negative status codes 130 | found = true 131 | } 132 | 133 | // nothing found, bail out 134 | // may add the result later if we want to enable verbose output 135 | if !found { 136 | return nil 137 | } 138 | 139 | extraStr := "" 140 | if s.globalopts.Verbose { 141 | // get status 142 | if bytes.Contains(body, []byte("")) { 143 | awsError := AWSError{} 144 | err := xml.Unmarshal(body, &awsError) 145 | if err != nil { 146 | return fmt.Errorf("could not parse error xml: %w", err) 147 | } 148 | // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList 149 | extraStr = fmt.Sprintf("Error: %s (%s)", awsError.Message, awsError.Code) 150 | } else if bytes.Contains(body, []byte(" 0 { 190 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil { 191 | return "", err 192 | } 193 | } 194 | 195 | wordlist := "stdin (pipe)" 196 | if s.globalopts.Wordlist != "-" { 197 | wordlist = s.globalopts.Wordlist 198 | } 199 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 200 | return "", err 201 | } 202 | 203 | if s.globalopts.PatternFile != "" { 204 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil { 205 | return "", err 206 | } 207 | } 208 | 209 | if o.Proxy != "" { 210 | if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { 211 | return "", err 212 | } 213 | } 214 | 215 | if o.UserAgent != "" { 216 | if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { 217 | return "", err 218 | } 219 | } 220 | 221 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 222 | return "", err 223 | } 224 | 225 | if s.globalopts.Verbose { 226 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 227 | return "", err 228 | } 229 | } 230 | 231 | if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil { 232 | return "", err 233 | } 234 | 235 | if err := tw.Flush(); err != nil { 236 | return "", fmt.Errorf("error on tostring: %w", err) 237 | } 238 | 239 | if err := bw.Flush(); err != nil { 240 | return "", fmt.Errorf("error on tostring: %w", err) 241 | } 242 | 243 | return strings.TrimSpace(buffer.String()), nil 244 | } 245 | 246 | // https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html 247 | func (s *GobusterS3) isValidBucketName(bucketName string) bool { 248 | if !s.bucketRegex.MatchString(bucketName) { 249 | return false 250 | } 251 | if strings.HasSuffix(bucketName, "-") || 252 | strings.HasPrefix(bucketName, ".") || 253 | strings.HasPrefix(bucketName, "-") || 254 | strings.Contains(bucketName, "..") || 255 | strings.Contains(bucketName, ".-") || 256 | strings.Contains(bucketName, "-.") { 257 | return false 258 | } 259 | return true 260 | } 261 | -------------------------------------------------------------------------------- /gobusters3/options.go: -------------------------------------------------------------------------------- 1 | package gobusters3 2 | 3 | import ( 4 | "github.com/OJ/gobuster/v3/libgobuster" 5 | ) 6 | 7 | // OptionsS3 is the struct to hold all options for this plugin 8 | type OptionsS3 struct { 9 | libgobuster.BasicHTTPOptions 10 | MaxFilesToList int 11 | } 12 | 13 | // NewOptionsS3 returns a new initialized OptionsS3 14 | func NewOptionsS3() *OptionsS3 { 15 | return &OptionsS3{} 16 | } 17 | -------------------------------------------------------------------------------- /gobusters3/result.go: -------------------------------------------------------------------------------- 1 | package gobusters3 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/fatih/color" 7 | ) 8 | 9 | var ( 10 | green = color.New(color.FgGreen).FprintfFunc() 11 | ) 12 | 13 | // Result represents a single result 14 | type Result struct { 15 | Found bool 16 | BucketName string 17 | Status string 18 | } 19 | 20 | // ResultToString converts the Result to it's textual representation 21 | func (r Result) ResultToString() (string, error) { 22 | buf := &bytes.Buffer{} 23 | 24 | c := green 25 | 26 | c(buf, "http://%s.s3.amazonaws.com/", r.BucketName) 27 | 28 | if r.Status != "" { 29 | c(buf, " [%s]", r.Status) 30 | } 31 | c(buf, "\n") 32 | 33 | str := buf.String() 34 | return str, nil 35 | } 36 | -------------------------------------------------------------------------------- /gobusters3/types.go: -------------------------------------------------------------------------------- 1 | package gobusters3 2 | 3 | import "encoding/xml" 4 | 5 | // AWSError represents a returned error from AWS 6 | type AWSError struct { 7 | XMLName xml.Name `xml:"Error"` 8 | Code string `xml:"Code"` 9 | Message string `xml:"Message"` 10 | RequestID string `xml:"RequestId"` 11 | HostID string `xml:"HostId"` 12 | } 13 | 14 | // AWSListing contains only a subset of returned properties 15 | type AWSListing struct { 16 | XMLName xml.Name `xml:"ListBucketResult"` 17 | Name string `xml:"Name"` 18 | IsTruncated string `xml:"IsTruncated"` 19 | Contents []struct { 20 | Key string `xml:"Key"` 21 | LastModified string `xml:"LastModified"` 22 | Size int `xml:"Size"` 23 | } `xml:"Contents"` 24 | } 25 | -------------------------------------------------------------------------------- /gobustertftp/gobustertftp.go: -------------------------------------------------------------------------------- 1 | package gobustertftp 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "strings" 9 | "text/tabwriter" 10 | 11 | "github.com/OJ/gobuster/v3/libgobuster" 12 | 13 | "github.com/pin/tftp/v3" 14 | ) 15 | 16 | // GobusterTFTP is the main type to implement the interface 17 | type GobusterTFTP struct { 18 | globalopts *libgobuster.Options 19 | options *OptionsTFTP 20 | } 21 | 22 | // NewGobusterTFTP creates a new initialized NewGobusterTFTP 23 | func NewGobusterTFTP(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterTFTP, error) { 24 | if globalopts == nil { 25 | return nil, fmt.Errorf("please provide valid global options") 26 | } 27 | 28 | if opts == nil { 29 | return nil, fmt.Errorf("please provide valid plugin options") 30 | } 31 | 32 | g := GobusterTFTP{ 33 | options: opts, 34 | globalopts: globalopts, 35 | } 36 | return &g, nil 37 | } 38 | 39 | // Name should return the name of the plugin 40 | func (d *GobusterTFTP) Name() string { 41 | return "TFTP enumeration" 42 | } 43 | 44 | // PreRun is the pre run implementation of gobustertftp 45 | func (d *GobusterTFTP) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 46 | _, err := tftp.NewClient(d.options.Server) 47 | if err != nil { 48 | return err 49 | } 50 | return nil 51 | } 52 | 53 | // ProcessWord is the process implementation of gobustertftp 54 | func (d *GobusterTFTP) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 55 | c, err := tftp.NewClient(d.options.Server) 56 | if err != nil { 57 | return err 58 | } 59 | c.SetTimeout(d.options.Timeout) 60 | wt, err := c.Receive(word, "octet") 61 | if err != nil { 62 | // file not found 63 | if d.globalopts.Verbose { 64 | progress.ResultChan <- Result{ 65 | Filename: word, 66 | Found: false, 67 | ErrorMessage: err.Error(), 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | result := Result{ 74 | Filename: word, 75 | Found: true, 76 | } 77 | if n, ok := wt.(tftp.IncomingTransfer).Size(); ok { 78 | result.Size = n 79 | } 80 | progress.ResultChan <- result 81 | return nil 82 | } 83 | 84 | func (d *GobusterTFTP) AdditionalWords(word string) []string { 85 | return []string{} 86 | } 87 | 88 | // GetConfigString returns the string representation of the current config 89 | func (d *GobusterTFTP) GetConfigString() (string, error) { 90 | var buffer bytes.Buffer 91 | bw := bufio.NewWriter(&buffer) 92 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 93 | o := d.options 94 | 95 | if _, err := fmt.Fprintf(tw, "[+] Server:\t%s\n", o.Server); err != nil { 96 | return "", err 97 | } 98 | 99 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil { 100 | return "", err 101 | } 102 | 103 | if d.globalopts.Delay > 0 { 104 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil { 105 | return "", err 106 | } 107 | } 108 | 109 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 110 | return "", err 111 | } 112 | 113 | wordlist := "stdin (pipe)" 114 | if d.globalopts.Wordlist != "-" { 115 | wordlist = d.globalopts.Wordlist 116 | } 117 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 118 | return "", err 119 | } 120 | 121 | if d.globalopts.PatternFile != "" { 122 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil { 123 | return "", err 124 | } 125 | } 126 | 127 | if d.globalopts.Verbose { 128 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 129 | return "", err 130 | } 131 | } 132 | 133 | if err := tw.Flush(); err != nil { 134 | return "", fmt.Errorf("error on tostring: %w", err) 135 | } 136 | 137 | if err := bw.Flush(); err != nil { 138 | return "", fmt.Errorf("error on tostring: %w", err) 139 | } 140 | 141 | return strings.TrimSpace(buffer.String()), nil 142 | } 143 | -------------------------------------------------------------------------------- /gobustertftp/options.go: -------------------------------------------------------------------------------- 1 | package gobustertftp 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // OptionsTFTP holds all options for the tftp plugin 8 | type OptionsTFTP struct { 9 | Server string 10 | Timeout time.Duration 11 | } 12 | 13 | // NewOptionsTFTP returns a new initialized OptionsTFTP 14 | func NewOptionsTFTP() *OptionsTFTP { 15 | return &OptionsTFTP{} 16 | } 17 | -------------------------------------------------------------------------------- /gobustertftp/result.go: -------------------------------------------------------------------------------- 1 | package gobustertftp 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | var ( 11 | red = color.New(color.FgRed).FprintfFunc() 12 | green = color.New(color.FgGreen).FprintfFunc() 13 | ) 14 | 15 | // Result represents a single result 16 | type Result struct { 17 | Filename string 18 | Found bool 19 | Size int64 20 | ErrorMessage string 21 | } 22 | 23 | // ResultToString converts the Result to it's textual representation 24 | func (r Result) ResultToString() (string, error) { 25 | buf := &bytes.Buffer{} 26 | 27 | if r.Found { 28 | green(buf, "Found: ") 29 | if _, err := fmt.Fprintf(buf, "%s", r.Filename); err != nil { 30 | return "", err 31 | } 32 | if r.Size > 0 { 33 | if _, err := fmt.Fprintf(buf, " [%d]", r.Size); err != nil { 34 | return "", err 35 | } 36 | } 37 | } else { 38 | red(buf, "Missed: ") 39 | if _, err := fmt.Fprintf(buf, "%s - %s", r.Filename, r.ErrorMessage); err != nil { 40 | return "", err 41 | } 42 | } 43 | 44 | s := buf.String() 45 | return s, nil 46 | } 47 | -------------------------------------------------------------------------------- /gobustervhost/gobustervhost.go: -------------------------------------------------------------------------------- 1 | package gobustervhost 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "fmt" 8 | "net" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "text/tabwriter" 13 | 14 | "github.com/OJ/gobuster/v3/libgobuster" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | // GobusterVhost is the main type to implement the interface 19 | type GobusterVhost struct { 20 | options *OptionsVhost 21 | globalopts *libgobuster.Options 22 | http *libgobuster.HTTPClient 23 | domain string 24 | normalBody []byte 25 | abnormalBody []byte 26 | } 27 | 28 | // NewGobusterVhost creates a new initialized GobusterDir 29 | func NewGobusterVhost(globalopts *libgobuster.Options, opts *OptionsVhost) (*GobusterVhost, error) { 30 | if globalopts == nil { 31 | return nil, fmt.Errorf("please provide valid global options") 32 | } 33 | 34 | if opts == nil { 35 | return nil, fmt.Errorf("please provide valid plugin options") 36 | } 37 | 38 | g := GobusterVhost{ 39 | options: opts, 40 | globalopts: globalopts, 41 | } 42 | 43 | basicOptions := libgobuster.BasicHTTPOptions{ 44 | Proxy: opts.Proxy, 45 | Timeout: opts.Timeout, 46 | UserAgent: opts.UserAgent, 47 | NoTLSValidation: opts.NoTLSValidation, 48 | RetryOnTimeout: opts.RetryOnTimeout, 49 | RetryAttempts: opts.RetryAttempts, 50 | TLSCertificate: opts.TLSCertificate, 51 | } 52 | 53 | httpOpts := libgobuster.HTTPOptions{ 54 | BasicHTTPOptions: basicOptions, 55 | FollowRedirect: opts.FollowRedirect, 56 | Username: opts.Username, 57 | Password: opts.Password, 58 | Headers: opts.Headers, 59 | NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders, 60 | Cookies: opts.Cookies, 61 | Method: opts.Method, 62 | } 63 | 64 | h, err := libgobuster.NewHTTPClient(&httpOpts) 65 | if err != nil { 66 | return nil, err 67 | } 68 | g.http = h 69 | return &g, nil 70 | } 71 | 72 | // Name should return the name of the plugin 73 | func (v *GobusterVhost) Name() string { 74 | return "VHOST enumeration" 75 | } 76 | 77 | // PreRun is the pre run implementation of gobusterdir 78 | func (v *GobusterVhost) PreRun(ctx context.Context, progress *libgobuster.Progress) error { 79 | // add trailing slash 80 | if !strings.HasSuffix(v.options.URL, "/") { 81 | v.options.URL = fmt.Sprintf("%s/", v.options.URL) 82 | } 83 | 84 | urlParsed, err := url.Parse(v.options.URL) 85 | if err != nil { 86 | return fmt.Errorf("invalid url %s: %w", v.options.URL, err) 87 | } 88 | if v.options.Domain != "" { 89 | v.domain = v.options.Domain 90 | } else { 91 | v.domain = urlParsed.Host 92 | } 93 | 94 | // request default vhost for normalBody 95 | _, _, _, body, err := v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{ReturnBody: true}) 96 | if err != nil { 97 | return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err) 98 | } 99 | v.normalBody = body 100 | 101 | // request non existent vhost for abnormalBody 102 | subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain) 103 | _, _, _, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true}) 104 | if err != nil { 105 | return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err) 106 | } 107 | v.abnormalBody = body 108 | return nil 109 | } 110 | 111 | // ProcessWord is the process implementation of gobusterdir 112 | func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error { 113 | var subdomain string 114 | if v.options.AppendDomain { 115 | subdomain = fmt.Sprintf("%s.%s", word, v.domain) 116 | } else { 117 | // wordlist needs to include full domains 118 | subdomain = word 119 | } 120 | 121 | tries := 1 122 | if v.options.RetryOnTimeout && v.options.RetryAttempts > 0 { 123 | // add it so it will be the overall max requests 124 | tries += v.options.RetryAttempts 125 | } 126 | 127 | var statusCode int 128 | var size int64 129 | var header http.Header 130 | var body []byte 131 | for i := 1; i <= tries; i++ { 132 | var err error 133 | statusCode, size, header, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true}) 134 | if err != nil { 135 | // check if it's a timeout and if we should try again and try again 136 | // otherwise the timeout error is raised 137 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries { 138 | continue 139 | } else if strings.Contains(err.Error(), "invalid control character in URL") { 140 | // put error in error chan so it's printed out and ignore it 141 | // so gobuster will not quit 142 | progress.ErrorChan <- err 143 | continue 144 | } else { 145 | return err 146 | } 147 | } 148 | break 149 | } 150 | 151 | // subdomain must not match default vhost and non existent vhost 152 | // or verbose mode is enabled 153 | found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody) 154 | if (found && !v.options.ExcludeLengthParsed.Contains(int(size))) || v.globalopts.Verbose { 155 | resultStatus := false 156 | if found { 157 | resultStatus = true 158 | } 159 | progress.ResultChan <- Result{ 160 | Found: resultStatus, 161 | Vhost: subdomain, 162 | StatusCode: statusCode, 163 | Size: size, 164 | Header: header, 165 | } 166 | } 167 | return nil 168 | } 169 | 170 | func (v *GobusterVhost) AdditionalWords(word string) []string { 171 | return []string{} 172 | } 173 | 174 | // GetConfigString returns the string representation of the current config 175 | func (v *GobusterVhost) GetConfigString() (string, error) { 176 | var buffer bytes.Buffer 177 | bw := bufio.NewWriter(&buffer) 178 | tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0) 179 | o := v.options 180 | if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil { 181 | return "", err 182 | } 183 | 184 | if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil { 185 | return "", err 186 | } 187 | 188 | if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", v.globalopts.Threads); err != nil { 189 | return "", err 190 | } 191 | 192 | if v.globalopts.Delay > 0 { 193 | if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", v.globalopts.Delay); err != nil { 194 | return "", err 195 | } 196 | } 197 | 198 | wordlist := "stdin (pipe)" 199 | if v.globalopts.Wordlist != "-" { 200 | wordlist = v.globalopts.Wordlist 201 | } 202 | if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil { 203 | return "", err 204 | } 205 | 206 | if v.globalopts.PatternFile != "" { 207 | if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", v.globalopts.PatternFile, len(v.globalopts.Patterns)); err != nil { 208 | return "", err 209 | } 210 | } 211 | 212 | if o.Proxy != "" { 213 | if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil { 214 | return "", err 215 | } 216 | } 217 | 218 | if o.Cookies != "" { 219 | if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil { 220 | return "", err 221 | } 222 | } 223 | 224 | if o.UserAgent != "" { 225 | if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil { 226 | return "", err 227 | } 228 | } 229 | 230 | if o.Username != "" { 231 | if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil { 232 | return "", err 233 | } 234 | } 235 | 236 | if v.globalopts.Verbose { 237 | if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil { 238 | return "", err 239 | } 240 | } 241 | 242 | if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil { 243 | return "", err 244 | } 245 | 246 | if _, err := fmt.Fprintf(tw, "[+] Append Domain:\t%t\n", v.options.AppendDomain); err != nil { 247 | return "", err 248 | } 249 | 250 | if len(o.ExcludeLength) > 0 { 251 | if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", v.options.ExcludeLengthParsed.Stringify()); err != nil { 252 | return "", err 253 | } 254 | } 255 | 256 | if err := tw.Flush(); err != nil { 257 | return "", fmt.Errorf("error on tostring: %w", err) 258 | } 259 | 260 | if err := bw.Flush(); err != nil { 261 | return "", fmt.Errorf("error on tostring: %w", err) 262 | } 263 | 264 | return strings.TrimSpace(buffer.String()), nil 265 | } 266 | -------------------------------------------------------------------------------- /gobustervhost/options.go: -------------------------------------------------------------------------------- 1 | package gobustervhost 2 | 3 | import ( 4 | "github.com/OJ/gobuster/v3/libgobuster" 5 | ) 6 | 7 | // OptionsVhost is the struct to hold all options for this plugin 8 | type OptionsVhost struct { 9 | libgobuster.HTTPOptions 10 | AppendDomain bool 11 | ExcludeLength string 12 | ExcludeLengthParsed libgobuster.Set[int] 13 | Domain string 14 | } 15 | 16 | // NewOptionsVhost returns a new initialized OptionsVhost 17 | func NewOptionsVhost() *OptionsVhost { 18 | return &OptionsVhost{ 19 | ExcludeLengthParsed: libgobuster.NewSet[int](), 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gobustervhost/result.go: -------------------------------------------------------------------------------- 1 | package gobustervhost 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | var ( 11 | white = color.New(color.FgWhite).SprintFunc() 12 | yellow = color.New(color.FgYellow).SprintFunc() 13 | green = color.New(color.FgGreen).SprintFunc() 14 | blue = color.New(color.FgBlue).SprintFunc() 15 | red = color.New(color.FgRed).SprintFunc() 16 | cyan = color.New(color.FgCyan).SprintFunc() 17 | ) 18 | 19 | // Result represents a single result 20 | type Result struct { 21 | Found bool 22 | Vhost string 23 | StatusCode int 24 | Size int64 25 | Header http.Header 26 | } 27 | 28 | // ResultToString converts the Result to it's textual representation 29 | func (r Result) ResultToString() (string, error) { 30 | statusText := yellow("Missed") 31 | if r.Found { 32 | statusText = green("Found") 33 | } 34 | 35 | statusCodeColor := white 36 | if r.StatusCode == 200 { 37 | statusCodeColor = green 38 | } else if r.StatusCode >= 300 && r.StatusCode < 400 { 39 | statusCodeColor = cyan 40 | } else if r.StatusCode >= 400 && r.StatusCode < 500 { 41 | statusCodeColor = yellow 42 | } else if r.StatusCode >= 500 && r.StatusCode < 600 { 43 | statusCodeColor = red 44 | } 45 | 46 | statusCode := statusCodeColor(fmt.Sprintf("Status: %d", r.StatusCode)) 47 | 48 | location := r.Header.Get("Location") 49 | locationString := "" 50 | if location != "" { 51 | locationString = blue(fmt.Sprintf(" [--> %s]", location)) 52 | } 53 | 54 | return fmt.Sprintf("%s: %s %s [Size: %d]%s\n", statusText, r.Vhost, statusCode, r.Size, locationString), nil 55 | } 56 | -------------------------------------------------------------------------------- /libgobuster/helpers.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | // Set is a set of Ts 16 | type Set[T comparable] struct { 17 | Set map[T]bool 18 | } 19 | 20 | // NewSSet creates a new initialized Set 21 | func NewSet[T comparable]() Set[T] { 22 | return Set[T]{Set: map[T]bool{}} 23 | } 24 | 25 | // Add an element to a set 26 | func (set *Set[T]) Add(s T) bool { 27 | _, found := set.Set[s] 28 | set.Set[s] = true 29 | return !found 30 | } 31 | 32 | // AddRange adds a list of elements to a set 33 | func (set *Set[T]) AddRange(ss []T) { 34 | for _, s := range ss { 35 | set.Set[s] = true 36 | } 37 | } 38 | 39 | // Contains tests if an element is in a set 40 | func (set *Set[T]) Contains(s T) bool { 41 | _, found := set.Set[s] 42 | return found 43 | } 44 | 45 | // ContainsAny checks if any of the elements exist 46 | func (set *Set[T]) ContainsAny(ss []T) bool { 47 | for _, s := range ss { 48 | if set.Set[s] { 49 | return true 50 | } 51 | } 52 | return false 53 | } 54 | 55 | // Length returns the length of the Set 56 | func (set *Set[T]) Length() int { 57 | return len(set.Set) 58 | } 59 | 60 | // Stringify the set 61 | func (set *Set[T]) Stringify() string { 62 | values := make([]string, len(set.Set)) 63 | i := 0 64 | for s := range set.Set { 65 | values[i] = fmt.Sprint(s) 66 | i++ 67 | } 68 | return strings.Join(values, ",") 69 | } 70 | 71 | func lineCounter(r io.Reader) (int, error) { 72 | buf := make([]byte, 32*1024) 73 | count := 1 74 | lineSep := []byte{'\n'} 75 | 76 | for { 77 | c, err := r.Read(buf) 78 | count += bytes.Count(buf[:c], lineSep) 79 | 80 | switch { 81 | case errors.Is(err, io.EOF): 82 | return count, nil 83 | 84 | case err != nil: 85 | return count, err 86 | } 87 | } 88 | } 89 | 90 | // DefaultUserAgent returns the default user agent to use in HTTP requests 91 | func DefaultUserAgent() string { 92 | return fmt.Sprintf("gobuster/%s", VERSION) 93 | } 94 | 95 | // ParseExtensions parses the extensions provided as a comma separated list 96 | func ParseExtensions(extensions string) (Set[string], error) { 97 | ret := NewSet[string]() 98 | 99 | if extensions == "" { 100 | return ret, nil 101 | } 102 | 103 | for _, e := range strings.Split(extensions, ",") { 104 | e = strings.TrimSpace(e) 105 | // remove leading . from extensions 106 | ret.Add(strings.TrimPrefix(e, ".")) 107 | } 108 | return ret, nil 109 | } 110 | 111 | func ParseExtensionsFile(file string) ([]string, error) { 112 | var ret []string 113 | 114 | stream, err := os.Open(file) 115 | if err != nil { 116 | return ret, err 117 | } 118 | defer stream.Close() 119 | 120 | scanner := bufio.NewScanner(stream) 121 | for scanner.Scan() { 122 | e := scanner.Text() 123 | e = strings.TrimSpace(e) 124 | // remove leading . from extensions 125 | ret = append(ret, (strings.TrimPrefix(e, "."))) 126 | } 127 | 128 | if err := scanner.Err(); err != nil { 129 | return nil, err 130 | } 131 | 132 | return ret, nil 133 | } 134 | 135 | // ParseCommaSeparatedInt parses the status codes provided as a comma separated list 136 | func ParseCommaSeparatedInt(inputString string) (Set[int], error) { 137 | ret := NewSet[int]() 138 | 139 | if inputString == "" { 140 | return ret, nil 141 | } 142 | 143 | for _, part := range strings.Split(inputString, ",") { 144 | part = strings.TrimSpace(part) 145 | // check for range 146 | if strings.Contains(part, "-") { 147 | re := regexp.MustCompile(`^\s*(\d+)\s*-\s*(\d+)\s*$`) 148 | match := re.FindStringSubmatch(part) 149 | if match == nil || len(match) != 3 { 150 | return NewSet[int](), fmt.Errorf("invalid range given: %s", part) 151 | } 152 | from := strings.TrimSpace(match[1]) 153 | to := strings.TrimSpace(match[2]) 154 | fromI, err := strconv.Atoi(from) 155 | if err != nil { 156 | return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, from) 157 | } 158 | toI, err := strconv.Atoi(to) 159 | if err != nil { 160 | return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, to) 161 | } 162 | if toI < fromI { 163 | return NewSet[int](), fmt.Errorf("invalid range given: %s", part) 164 | } 165 | for i := fromI; i <= toI; i++ { 166 | ret.Add(i) 167 | } 168 | } else { 169 | i, err := strconv.Atoi(part) 170 | if err != nil { 171 | return NewSet[int](), fmt.Errorf("invalid string given: %s", part) 172 | } 173 | ret.Add(i) 174 | } 175 | } 176 | return ret, nil 177 | } 178 | 179 | // SliceContains checks if an integer slice contains a specific value 180 | func SliceContains(s []int, e int) bool { 181 | for _, a := range s { 182 | if a == e { 183 | return true 184 | } 185 | } 186 | return false 187 | } 188 | 189 | // JoinIntSlice joins an int slice by , 190 | func JoinIntSlice(s []int) string { 191 | valuesText := make([]string, len(s)) 192 | for i, number := range s { 193 | text := strconv.Itoa(number) 194 | valuesText[i] = text 195 | } 196 | result := strings.Join(valuesText, ",") 197 | return result 198 | } 199 | -------------------------------------------------------------------------------- /libgobuster/helpers_test.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | "testing/iotest" 10 | ) 11 | 12 | func TestNewSet(t *testing.T) { 13 | t.Parallel() 14 | if NewSet[string]().Set == nil { 15 | t.Fatal("NewSet[string] returned nil Set") 16 | } 17 | 18 | if NewSet[int]().Set == nil { 19 | t.Fatal("NewSet[int] returned nil Set") 20 | } 21 | } 22 | 23 | func TestSetAdd(t *testing.T) { 24 | t.Parallel() 25 | x := NewSet[string]() 26 | x.Add("test") 27 | if len(x.Set) != 1 { 28 | t.Fatalf("Unexpected string size. Should have 1 Got %v", len(x.Set)) 29 | } 30 | 31 | y := NewSet[int]() 32 | y.Add(1) 33 | if len(y.Set) != 1 { 34 | t.Fatalf("Unexpected int size. Should have 1 Got %v", len(y.Set)) 35 | } 36 | } 37 | 38 | func TestSetAddDouble(t *testing.T) { 39 | t.Parallel() 40 | x := NewSet[string]() 41 | x.Add("test") 42 | x.Add("test") 43 | if len(x.Set) != 1 { 44 | t.Fatalf("Unexpected string size. Should be 1 (unique) Got %v", len(x.Set)) 45 | } 46 | 47 | y := NewSet[int]() 48 | y.Add(1) 49 | y.Add(1) 50 | if len(y.Set) != 1 { 51 | t.Fatalf("Unexpected int size. Should be 1 (unique) Got %v", len(y.Set)) 52 | } 53 | } 54 | 55 | func TestSetAddRange(t *testing.T) { 56 | t.Parallel() 57 | x := NewSet[string]() 58 | x.AddRange([]string{"string1", "string2"}) 59 | if len(x.Set) != 2 { 60 | t.Fatalf("Unexpected string size. Should have 2 Got %v", len(x.Set)) 61 | } 62 | 63 | y := NewSet[int]() 64 | y.AddRange([]int{1, 2}) 65 | if len(y.Set) != 2 { 66 | t.Fatalf("Unexpected int size. Should have 2 Got %v", len(y.Set)) 67 | } 68 | } 69 | 70 | func TestSetAddRangeDouble(t *testing.T) { 71 | t.Parallel() 72 | x := NewSet[string]() 73 | x.AddRange([]string{"string1", "string2", "string1", "string2"}) 74 | if len(x.Set) != 2 { 75 | t.Fatalf("Unexpected string size. Should be 2 (unique) Got %v", len(x.Set)) 76 | } 77 | 78 | y := NewSet[int]() 79 | y.AddRange([]int{1, 2, 1, 2}) 80 | if len(y.Set) != 2 { 81 | t.Fatalf("Unexpected int size. Should be 2 (unique) Got %v", len(y.Set)) 82 | } 83 | } 84 | 85 | func TestSetContains(t *testing.T) { 86 | t.Parallel() 87 | x := NewSet[string]() 88 | v := []string{"string1", "string2", "1234", "5678"} 89 | x.AddRange(v) 90 | for _, i := range v { 91 | if !x.Contains(i) { 92 | t.Fatalf("Did not find value %s in array. %v", i, x.Set) 93 | } 94 | } 95 | 96 | y := NewSet[int]() 97 | v2 := []int{1, 2312, 123121, 999, -99} 98 | y.AddRange(v2) 99 | for _, i := range v2 { 100 | if !y.Contains(i) { 101 | t.Fatalf("Did not find value %d in array. %v", i, y.Set) 102 | } 103 | } 104 | } 105 | 106 | func TestSetContainsAny(t *testing.T) { 107 | t.Parallel() 108 | x := NewSet[string]() 109 | v := []string{"string1", "string2", "1234", "5678"} 110 | x.AddRange(v) 111 | if !x.ContainsAny(v) { 112 | t.Fatalf("Did not find any") 113 | } 114 | 115 | // test not found 116 | if x.ContainsAny([]string{"mmmm", "nnnnn"}) { 117 | t.Fatal("Found unexpected values") 118 | } 119 | 120 | y := NewSet[int]() 121 | v2 := []int{1, 2312, 123121, 999, -99} 122 | y.AddRange(v2) 123 | if !y.ContainsAny(v2) { 124 | t.Fatalf("Did not find any") 125 | } 126 | 127 | // test not found 128 | if y.ContainsAny([]int{9235, 2398532}) { 129 | t.Fatal("Found unexpected values") 130 | } 131 | } 132 | 133 | func TestSetStringify(t *testing.T) { 134 | t.Parallel() 135 | x := NewSet[string]() 136 | v := []string{"string1", "string2", "1234", "5678"} 137 | x.AddRange(v) 138 | z := x.Stringify() 139 | // order is random 140 | for _, i := range v { 141 | if !strings.Contains(z, i) { 142 | t.Fatalf("Did not find value %q in %q", i, z) 143 | } 144 | } 145 | 146 | y := NewSet[int]() 147 | v2 := []int{1, 2312, 123121, 999, -99} 148 | y.AddRange(v2) 149 | z = y.Stringify() 150 | // order is random 151 | for _, i := range v2 { 152 | if !strings.Contains(z, fmt.Sprint(i)) { 153 | t.Fatalf("Did not find value %q in %q", i, z) 154 | } 155 | } 156 | } 157 | 158 | func TestLineCounter(t *testing.T) { 159 | t.Parallel() 160 | var tt = []struct { 161 | testName string 162 | s string 163 | expected int 164 | }{ 165 | {"One Line", "test", 1}, 166 | {"3 Lines", "TestString\nTest\n1234", 3}, 167 | {"Trailing newline", "TestString\nTest\n1234\n", 4}, 168 | {"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3}, 169 | {"Empty", "", 1}, 170 | } 171 | for _, x := range tt { 172 | x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 173 | t.Run(x.testName, func(t *testing.T) { 174 | t.Parallel() 175 | r := strings.NewReader(x.s) 176 | l, err := lineCounter(r) 177 | if err != nil { 178 | t.Fatalf("Got error: %v", err) 179 | } 180 | if l != x.expected { 181 | t.Fatalf("wrong line count! Got %d expected %d", l, x.expected) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | func TestLineCounterError(t *testing.T) { 188 | t.Parallel() 189 | r := iotest.TimeoutReader(strings.NewReader("test")) 190 | _, err := lineCounter(r) 191 | if !errors.Is(err, iotest.ErrTimeout) { 192 | t.Fatalf("Got wrong error! %v", err) 193 | } 194 | } 195 | 196 | func TestParseExtensions(t *testing.T) { 197 | t.Parallel() 198 | var tt = []struct { 199 | testName string 200 | extensions string 201 | expectedExtensions Set[string] 202 | expectedError string 203 | }{ 204 | {"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 205 | {"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 206 | {"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 207 | {"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 208 | {"Empty string", "", NewSet[string](), "invalid extension string provided"}, 209 | } 210 | 211 | for _, x := range tt { 212 | x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 213 | t.Run(x.testName, func(t *testing.T) { 214 | t.Parallel() 215 | ret, err := ParseExtensions(x.extensions) 216 | if x.expectedError != "" { 217 | if err != nil && err.Error() != x.expectedError { 218 | t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error()) 219 | } 220 | } else if !reflect.DeepEqual(x.expectedExtensions, ret) { 221 | t.Fatalf("Expected %v but got %v", x.expectedExtensions, ret) 222 | } 223 | }) 224 | } 225 | } 226 | 227 | func TestParseCommaSeparatedInt(t *testing.T) { 228 | t.Parallel() 229 | var tt = []struct { 230 | stringCodes string 231 | expectedCodes []int 232 | expectedError string 233 | }{ 234 | {"200,100,202", []int{200, 100, 202}, ""}, 235 | {"200, 100 , 202", []int{200, 100, 202}, ""}, 236 | {"200, 100, 202, 100", []int{200, 100, 202}, ""}, 237 | {"200,AAA", []int{}, "invalid string given: AAA"}, 238 | {"2000000000000000000000000000000", []int{}, "invalid string given: 2000000000000000000000000000000"}, 239 | {"", []int{}, "invalid string provided"}, 240 | {"200-205", []int{200, 201, 202, 203, 204, 205}, ""}, 241 | {"200-202,203-205", []int{200, 201, 202, 203, 204, 205}, ""}, 242 | {"200-202,204-205", []int{200, 201, 202, 204, 205}, ""}, 243 | {"200-202,205", []int{200, 201, 202, 205}, ""}, 244 | {"205,200,100-101,103-105", []int{100, 101, 103, 104, 105, 200, 205}, ""}, 245 | {"200-200", []int{200}, ""}, 246 | {"200 - 202", []int{200, 201, 202}, ""}, 247 | {"200 -202", []int{200, 201, 202}, ""}, 248 | {"200- 202", []int{200, 201, 202}, ""}, 249 | {"200 - 202", []int{200, 201, 202}, ""}, 250 | {"230-200", []int{}, "invalid range given: 230-200"}, 251 | {"A-200", []int{}, "invalid range given: A-200"}, 252 | {"230-A", []int{}, "invalid range given: 230-A"}, 253 | {"200,202-205,A,206-210", []int{}, "invalid string given: A"}, 254 | {"200,202-205,A-1,206-210", []int{}, "invalid range given: A-1"}, 255 | {"200,202-205,1-A,206-210", []int{}, "invalid range given: 1-A"}, 256 | } 257 | 258 | for _, x := range tt { 259 | x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 260 | t.Run(x.stringCodes, func(t *testing.T) { 261 | t.Parallel() 262 | want := NewSet[int]() 263 | want.AddRange(x.expectedCodes) 264 | ret, err := ParseCommaSeparatedInt(x.stringCodes) 265 | if x.expectedError != "" { 266 | if err != nil && err.Error() != x.expectedError { 267 | t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error()) 268 | } 269 | } else if !reflect.DeepEqual(want, ret) { 270 | t.Fatalf("Expected %v but got %v", want, ret) 271 | } 272 | }) 273 | } 274 | } 275 | 276 | func BenchmarkParseExtensions(b *testing.B) { 277 | var tt = []struct { 278 | testName string 279 | extensions string 280 | expectedExtensions Set[string] 281 | expectedError string 282 | }{ 283 | {"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 284 | {"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 285 | {"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 286 | {"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, 287 | {"Empty string", "", NewSet[string](), "invalid extension string provided"}, 288 | } 289 | 290 | for _, x := range tt { 291 | x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 292 | b.Run(x.testName, func(b2 *testing.B) { 293 | for y := 0; y < b2.N; y++ { 294 | _, _ = ParseExtensions(x.extensions) 295 | } 296 | }) 297 | } 298 | } 299 | 300 | func BenchmarkParseCommaSeparatedInt(b *testing.B) { 301 | var tt = []struct { 302 | testName string 303 | stringCodes string 304 | expectedCodes Set[int] 305 | expectedError string 306 | }{ 307 | {"Valid codes", "200,100,202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, 308 | {"Spaces", "200, 100 , 202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, 309 | {"Double codes", "200, 100, 202, 100", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, 310 | {"Invalid code", "200,AAA", NewSet[int](), "invalid string given: AAA"}, 311 | {"Invalid integer", "2000000000000000000000000000000", NewSet[int](), "invalid string given: 2000000000000000000000000000000"}, 312 | {"Empty string", "", NewSet[int](), "invalid string string provided"}, 313 | } 314 | 315 | for _, x := range tt { 316 | x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables 317 | b.Run(x.testName, func(b2 *testing.B) { 318 | for y := 0; y < b2.N; y++ { 319 | _, _ = ParseCommaSeparatedInt(x.stringCodes) 320 | } 321 | }) 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /libgobuster/http.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | // HTTPHeader holds a single key value pair of a HTTP header 15 | type HTTPHeader struct { 16 | Name string 17 | Value string 18 | } 19 | 20 | // HTTPClient represents a http object 21 | type HTTPClient struct { 22 | client *http.Client 23 | userAgent string 24 | defaultUserAgent string 25 | username string 26 | password string 27 | headers []HTTPHeader 28 | noCanonicalizeHeaders bool 29 | cookies string 30 | method string 31 | host string 32 | } 33 | 34 | // RequestOptions is used to pass options to a single individual request 35 | type RequestOptions struct { 36 | Host string 37 | Body io.Reader 38 | ReturnBody bool 39 | ModifiedHeaders []HTTPHeader 40 | UpdatedBasicAuthUsername string 41 | UpdatedBasicAuthPassword string 42 | } 43 | 44 | // NewHTTPClient returns a new HTTPClient 45 | func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) { 46 | var proxyURLFunc func(*http.Request) (*url.URL, error) 47 | var client HTTPClient 48 | proxyURLFunc = http.ProxyFromEnvironment 49 | 50 | if opt == nil { 51 | return nil, fmt.Errorf("options is nil") 52 | } 53 | 54 | if opt.Proxy != "" { 55 | proxyURL, err := url.Parse(opt.Proxy) 56 | if err != nil { 57 | return nil, fmt.Errorf("proxy URL is invalid (%w)", err) 58 | } 59 | proxyURLFunc = http.ProxyURL(proxyURL) 60 | } 61 | 62 | var redirectFunc func(req *http.Request, via []*http.Request) error 63 | if !opt.FollowRedirect { 64 | redirectFunc = func(req *http.Request, via []*http.Request) error { 65 | return http.ErrUseLastResponse 66 | } 67 | } else { 68 | redirectFunc = nil 69 | } 70 | 71 | tlsConfig := tls.Config{ 72 | InsecureSkipVerify: opt.NoTLSValidation, 73 | // enable TLS1.0 and TLS1.1 support 74 | MinVersion: tls.VersionTLS10, 75 | } 76 | if opt.TLSCertificate != nil { 77 | tlsConfig.Certificates = []tls.Certificate{*opt.TLSCertificate} 78 | } 79 | 80 | client.client = &http.Client{ 81 | Timeout: opt.Timeout, 82 | CheckRedirect: redirectFunc, 83 | Transport: &http.Transport{ 84 | Proxy: proxyURLFunc, 85 | MaxIdleConns: 100, 86 | MaxIdleConnsPerHost: 100, 87 | TLSClientConfig: &tlsConfig, 88 | }} 89 | client.username = opt.Username 90 | client.password = opt.Password 91 | client.userAgent = opt.UserAgent 92 | client.defaultUserAgent = DefaultUserAgent() 93 | client.headers = opt.Headers 94 | client.noCanonicalizeHeaders = opt.NoCanonicalizeHeaders 95 | client.cookies = opt.Cookies 96 | client.method = opt.Method 97 | if client.method == "" { 98 | client.method = http.MethodGet 99 | } 100 | // Host header needs to be set separately 101 | for _, h := range opt.Headers { 102 | if h.Name == "Host" { 103 | client.host = h.Value 104 | break 105 | } 106 | } 107 | return &client, nil 108 | } 109 | 110 | // Request makes an http request and returns the status, the content length, the headers, the body and an error 111 | // if you want the body returned set the corresponding property inside RequestOptions 112 | func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts RequestOptions) (int, int64, http.Header, []byte, error) { 113 | resp, err := client.makeRequest(ctx, fullURL, opts) 114 | if err != nil { 115 | // ignore context canceled errors 116 | if errors.Is(ctx.Err(), context.Canceled) { 117 | return 0, 0, nil, nil, nil 118 | } 119 | return 0, 0, nil, nil, err 120 | } 121 | defer resp.Body.Close() 122 | 123 | var body []byte 124 | var length int64 125 | if opts.ReturnBody { 126 | body, err = io.ReadAll(resp.Body) 127 | if err != nil { 128 | return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err) 129 | } 130 | length = int64(len(body)) 131 | } else { 132 | // DO NOT REMOVE! 133 | // absolutely needed so golang will reuse connections! 134 | length, err = io.Copy(io.Discard, resp.Body) 135 | if err != nil { 136 | return 0, 0, nil, nil, err 137 | } 138 | } 139 | 140 | return resp.StatusCode, length, resp.Header, body, nil 141 | } 142 | 143 | func (client *HTTPClient) makeRequest(ctx context.Context, fullURL string, opts RequestOptions) (*http.Response, error) { 144 | req, err := http.NewRequest(client.method, fullURL, opts.Body) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | // add the context so we can easily cancel out 150 | req = req.WithContext(ctx) 151 | 152 | if client.cookies != "" { 153 | req.Header.Set("Cookie", client.cookies) 154 | } 155 | 156 | // Use host for VHOST mode on a per request basis, otherwise the one provided from headers 157 | if opts.Host != "" { 158 | req.Host = opts.Host 159 | } else if client.host != "" { 160 | req.Host = client.host 161 | } 162 | 163 | if client.userAgent != "" { 164 | req.Header.Set("User-Agent", client.userAgent) 165 | } else { 166 | req.Header.Set("User-Agent", client.defaultUserAgent) 167 | } 168 | 169 | // add custom headers 170 | // if ModifiedHeaders are supplied use those, otherwise use the original ones 171 | // currently only relevant on fuzzing 172 | if len(opts.ModifiedHeaders) > 0 { 173 | for _, h := range opts.ModifiedHeaders { 174 | if client.noCanonicalizeHeaders { 175 | // https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang 176 | req.Header[h.Name] = []string{h.Value} 177 | } else { 178 | req.Header.Set(h.Name, h.Value) 179 | } 180 | } 181 | } else { 182 | for _, h := range client.headers { 183 | if client.noCanonicalizeHeaders { 184 | // https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang 185 | req.Header[h.Name] = []string{h.Value} 186 | } else { 187 | req.Header.Set(h.Name, h.Value) 188 | } 189 | } 190 | } 191 | 192 | if opts.UpdatedBasicAuthUsername != "" { 193 | req.SetBasicAuth(opts.UpdatedBasicAuthUsername, opts.UpdatedBasicAuthPassword) 194 | } else if client.username != "" { 195 | req.SetBasicAuth(client.username, client.password) 196 | } 197 | 198 | resp, err := client.client.Do(req) 199 | if err != nil { 200 | var ue *url.Error 201 | if errors.As(err, &ue) { 202 | if strings.HasPrefix(ue.Err.Error(), "x509") { 203 | return nil, fmt.Errorf("invalid certificate: %w", ue.Err) 204 | } 205 | } 206 | return nil, err 207 | } 208 | 209 | return resp, nil 210 | } 211 | -------------------------------------------------------------------------------- /libgobuster/http_test.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "fmt" 8 | "math/big" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | ) 13 | 14 | func httpServerB(b *testing.B, content string) *httptest.Server { 15 | b.Helper() 16 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | fmt.Fprint(w, content) 18 | })) 19 | return ts 20 | } 21 | 22 | func httpServerT(t *testing.T, content string) *httptest.Server { 23 | t.Helper() 24 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | fmt.Fprint(w, content) 26 | })) 27 | return ts 28 | } 29 | 30 | func randomString(length int) (string, error) { 31 | var letter = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") 32 | letterLen := len(letter) 33 | 34 | b := make([]byte, length) 35 | for i := range b { 36 | n, err := rand.Int(rand.Reader, big.NewInt(int64(letterLen))) 37 | if err != nil { 38 | return "", err 39 | } 40 | b[i] = letter[n.Int64()] 41 | } 42 | return string(b), nil 43 | } 44 | 45 | func TestRequest(t *testing.T) { 46 | t.Parallel() 47 | ret, err := randomString(100) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | h := httpServerT(t, ret) 52 | defer h.Close() 53 | var o HTTPOptions 54 | c, err := NewHTTPClient(&o) 55 | if err != nil { 56 | t.Fatalf("Got Error: %v", err) 57 | } 58 | status, length, _, body, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: true}) 59 | if err != nil { 60 | t.Fatalf("Got Error: %v", err) 61 | } 62 | if status != 200 { 63 | t.Fatalf("Invalid status returned: %d", status) 64 | } 65 | if length != int64(len(ret)) { 66 | t.Fatalf("Invalid length returned: %d", length) 67 | } 68 | if body == nil || !bytes.Equal(body, []byte(ret)) { 69 | t.Fatalf("Invalid body returned: %d", body) 70 | } 71 | } 72 | 73 | func BenchmarkRequestWithoutBody(b *testing.B) { 74 | r, err := randomString(10000) 75 | if err != nil { 76 | b.Fatal(err) 77 | } 78 | h := httpServerB(b, r) 79 | defer h.Close() 80 | var o HTTPOptions 81 | c, err := NewHTTPClient(&o) 82 | if err != nil { 83 | b.Fatalf("Got Error: %v", err) 84 | } 85 | for x := 0; x < b.N; x++ { 86 | _, _, _, _, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: false}) 87 | if err != nil { 88 | b.Fatalf("Got Error: %v", err) 89 | } 90 | } 91 | } 92 | 93 | func BenchmarkRequestWitBody(b *testing.B) { 94 | r, err := randomString(10000) 95 | if err != nil { 96 | b.Fatal(err) 97 | } 98 | h := httpServerB(b, r) 99 | defer h.Close() 100 | var o HTTPOptions 101 | c, err := NewHTTPClient(&o) 102 | if err != nil { 103 | b.Fatalf("Got Error: %v", err) 104 | } 105 | for x := 0; x < b.N; x++ { 106 | _, _, _, _, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: true}) 107 | if err != nil { 108 | b.Fatalf("Got Error: %v", err) 109 | } 110 | } 111 | } 112 | 113 | func BenchmarkNewHTTPClient(b *testing.B) { 114 | r, err := randomString(500) 115 | if err != nil { 116 | b.Fatal(err) 117 | } 118 | h := httpServerB(b, r) 119 | defer h.Close() 120 | var o HTTPOptions 121 | for x := 0; x < b.N; x++ { 122 | _, err := NewHTTPClient(&o) 123 | if err != nil { 124 | b.Fatalf("Got Error: %v", err) 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /libgobuster/interfaces.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import "context" 4 | 5 | // GobusterPlugin is an interface which plugins must implement 6 | type GobusterPlugin interface { 7 | Name() string 8 | PreRun(context.Context, *Progress) error 9 | ProcessWord(context.Context, string, *Progress) error 10 | AdditionalWords(string) []string 11 | GetConfigString() (string, error) 12 | } 13 | 14 | // Result is an interface for the Result object 15 | type Result interface { 16 | ResultToString() (string, error) 17 | } 18 | -------------------------------------------------------------------------------- /libgobuster/libgobuster.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // PATTERN is the pattern for wordlist replacements in pattern file 14 | const PATTERN = "{GOBUSTER}" 15 | 16 | // SetupFunc is the "setup" function prototype for implementations 17 | type SetupFunc func(*Gobuster) error 18 | 19 | // ProcessFunc is the "process" function prototype for implementations 20 | type ProcessFunc func(*Gobuster, string) ([]Result, error) 21 | 22 | // ResultToStringFunc is the "to string" function prototype for implementations 23 | type ResultToStringFunc func(*Gobuster, *Result) (*string, error) 24 | 25 | // Gobuster is the main object when creating a new run 26 | type Gobuster struct { 27 | Opts *Options 28 | Logger Logger 29 | plugin GobusterPlugin 30 | Progress *Progress 31 | } 32 | 33 | // NewGobuster returns a new Gobuster object 34 | func NewGobuster(opts *Options, plugin GobusterPlugin, logger Logger) (*Gobuster, error) { 35 | var g Gobuster 36 | g.Opts = opts 37 | g.plugin = plugin 38 | g.Logger = logger 39 | g.Progress = NewProgress() 40 | 41 | return &g, nil 42 | } 43 | 44 | func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.WaitGroup) { 45 | defer wg.Done() 46 | for { 47 | select { 48 | case <-ctx.Done(): 49 | return 50 | case word, ok := <-wordChan: 51 | // worker finished 52 | if !ok { 53 | return 54 | } 55 | g.Progress.incrementRequests() 56 | 57 | wordCleaned := strings.TrimSpace(word) 58 | // Skip "comment" (starts with #), as well as empty lines 59 | if strings.HasPrefix(wordCleaned, "#") || len(wordCleaned) == 0 { 60 | break 61 | } 62 | 63 | // Mode-specific processing 64 | err := g.plugin.ProcessWord(ctx, wordCleaned, g.Progress) 65 | if err != nil { 66 | // do not exit and continue 67 | g.Progress.ErrorChan <- err 68 | continue 69 | } 70 | 71 | select { 72 | case <-ctx.Done(): 73 | case <-time.After(g.Opts.Delay): 74 | } 75 | } 76 | } 77 | } 78 | 79 | func (g *Gobuster) getWordlist() (*bufio.Scanner, error) { 80 | if g.Opts.Wordlist == "-" { 81 | // Read directly from stdin 82 | return bufio.NewScanner(os.Stdin), nil 83 | } 84 | // Pull content from the wordlist 85 | wordlist, err := os.Open(g.Opts.Wordlist) 86 | if err != nil { 87 | return nil, fmt.Errorf("failed to open wordlist: %w", err) 88 | } 89 | 90 | lines, err := lineCounter(wordlist) 91 | if err != nil { 92 | return nil, fmt.Errorf("failed to get number of lines: %w", err) 93 | } 94 | 95 | if lines-g.Opts.WordlistOffset <= 0 { 96 | return nil, fmt.Errorf("offset is greater than the number of lines in the wordlist") 97 | } 98 | 99 | // calcutate expected requests 100 | g.Progress.IncrementTotalRequests(lines) 101 | 102 | // add offset if needed (offset defaults to 0) 103 | g.Progress.incrementRequestsIssues(g.Opts.WordlistOffset) 104 | 105 | // call the function once with a dummy entry to receive the number 106 | // of custom words per wordlist word 107 | customWordsLen := len(g.plugin.AdditionalWords("dummy")) 108 | if customWordsLen > 0 { 109 | origExpected := g.Progress.RequestsExpected() 110 | inc := origExpected * customWordsLen 111 | g.Progress.IncrementTotalRequests(inc) 112 | } 113 | 114 | // rewind wordlist 115 | _, err = wordlist.Seek(0, 0) 116 | if err != nil { 117 | return nil, fmt.Errorf("failed to rewind wordlist: %w", err) 118 | } 119 | 120 | wordlistScanner := bufio.NewScanner(wordlist) 121 | 122 | // skip lines 123 | for i := 0; i < g.Opts.WordlistOffset; i++ { 124 | if !wordlistScanner.Scan() { 125 | if err := wordlistScanner.Err(); err != nil { 126 | return nil, fmt.Errorf("failed to skip lines in wordlist: %w", err) 127 | } 128 | return nil, fmt.Errorf("failed to skip lines in wordlist") 129 | } 130 | } 131 | 132 | return wordlistScanner, nil 133 | } 134 | 135 | // Run the busting of the website with the given 136 | // set of settings from the command line. 137 | func (g *Gobuster) Run(ctx context.Context) error { 138 | defer close(g.Progress.ResultChan) 139 | defer close(g.Progress.ErrorChan) 140 | defer close(g.Progress.MessageChan) 141 | 142 | if err := g.plugin.PreRun(ctx, g.Progress); err != nil { 143 | return err 144 | } 145 | 146 | var workerGroup sync.WaitGroup 147 | workerGroup.Add(g.Opts.Threads) 148 | 149 | wordChan := make(chan string, g.Opts.Threads) 150 | 151 | // Create goroutines for each of the number of threads 152 | // specified. 153 | for i := 0; i < g.Opts.Threads; i++ { 154 | go g.worker(ctx, wordChan, &workerGroup) 155 | } 156 | 157 | scanner, err := g.getWordlist() 158 | if err != nil { 159 | return err 160 | } 161 | 162 | Scan: 163 | for scanner.Scan() { 164 | select { 165 | case <-ctx.Done(): 166 | break Scan 167 | default: 168 | word := scanner.Text() 169 | perms := g.processPatterns(word) 170 | // add the original word 171 | wordChan <- word 172 | // now create perms 173 | for _, w := range perms { 174 | select { 175 | // need to check here too otherwise wordChan will block 176 | case <-ctx.Done(): 177 | break Scan 178 | case wordChan <- w: 179 | } 180 | } 181 | 182 | for _, w := range g.plugin.AdditionalWords(word) { 183 | select { 184 | // need to check here too otherwise wordChan will block 185 | case <-ctx.Done(): 186 | break Scan 187 | case wordChan <- w: 188 | } 189 | } 190 | } 191 | } 192 | close(wordChan) 193 | workerGroup.Wait() 194 | 195 | if err := scanner.Err(); err != nil { 196 | return err 197 | } 198 | 199 | return nil 200 | } 201 | 202 | // GetConfigString returns the current config as a printable string 203 | func (g *Gobuster) GetConfigString() (string, error) { 204 | return g.plugin.GetConfigString() 205 | } 206 | 207 | func (g *Gobuster) processPatterns(word string) []string { 208 | if g.Opts.PatternFile == "" { 209 | return nil 210 | } 211 | 212 | //nolint:prealloc 213 | var pat []string 214 | for _, x := range g.Opts.Patterns { 215 | repl := strings.ReplaceAll(x, PATTERN, word) 216 | pat = append(pat, repl) 217 | } 218 | return pat 219 | } 220 | -------------------------------------------------------------------------------- /libgobuster/logger.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | type Logger struct { 11 | log *log.Logger 12 | errorLog *log.Logger 13 | debugLog *log.Logger 14 | infoLog *log.Logger 15 | debug bool 16 | } 17 | 18 | func NewLogger(debug bool) Logger { 19 | return Logger{ 20 | log: log.New(os.Stdout, "", 0), 21 | errorLog: log.New(os.Stderr, color.New(color.FgRed).Sprint("[ERROR] "), 0), 22 | debugLog: log.New(os.Stderr, color.New(color.FgBlue).Sprint("[DEBUG] "), 0), 23 | infoLog: log.New(os.Stderr, color.New(color.FgCyan).Sprint("[INFO] "), 0), 24 | debug: debug, 25 | } 26 | } 27 | 28 | func (l Logger) Debug(v ...any) { 29 | if !l.debug { 30 | return 31 | } 32 | l.debugLog.Print(v...) 33 | } 34 | 35 | func (l Logger) Debugf(format string, v ...any) { 36 | if !l.debug { 37 | return 38 | } 39 | l.debugLog.Printf(format, v...) 40 | } 41 | 42 | func (l Logger) Info(v ...any) { 43 | l.infoLog.Print(v...) 44 | } 45 | 46 | func (l Logger) Infof(format string, v ...any) { 47 | l.infoLog.Printf(format, v...) 48 | } 49 | 50 | func (l Logger) Print(v ...any) { 51 | l.log.Print(v...) 52 | } 53 | 54 | func (l Logger) Printf(format string, v ...any) { 55 | l.log.Printf(format, v...) 56 | } 57 | 58 | func (l Logger) Println(v ...any) { 59 | l.log.Println(v...) 60 | } 61 | 62 | func (l Logger) Error(v ...any) { 63 | l.errorLog.Print(v...) 64 | } 65 | 66 | func (l Logger) Errorf(format string, v ...any) { 67 | l.errorLog.Printf(format, v...) 68 | } 69 | 70 | func (l Logger) Fatal(v ...any) { 71 | l.errorLog.Fatal(v...) 72 | } 73 | 74 | func (l Logger) Fatalf(format string, v ...any) { 75 | l.errorLog.Fatalf(format, v...) 76 | } 77 | 78 | func (l Logger) Fatalln(v ...any) { 79 | l.errorLog.Fatalln(v...) 80 | } 81 | -------------------------------------------------------------------------------- /libgobuster/options.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import "time" 4 | 5 | // Options holds all options that can be passed to libgobuster 6 | type Options struct { 7 | Threads int 8 | Debug bool 9 | Wordlist string 10 | WordlistOffset int 11 | PatternFile string 12 | Patterns []string 13 | OutputFilename string 14 | NoStatus bool 15 | NoProgress bool 16 | NoError bool 17 | Quiet bool 18 | Verbose bool 19 | Delay time.Duration 20 | } 21 | 22 | // NewOptions returns a new initialized Options object 23 | func NewOptions() *Options { 24 | return &Options{} 25 | } 26 | -------------------------------------------------------------------------------- /libgobuster/options_http.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import ( 4 | "crypto/tls" 5 | "time" 6 | ) 7 | 8 | // BasicHTTPOptions defines only core http options 9 | type BasicHTTPOptions struct { 10 | UserAgent string 11 | Proxy string 12 | NoTLSValidation bool 13 | Timeout time.Duration 14 | RetryOnTimeout bool 15 | RetryAttempts int 16 | TLSCertificate *tls.Certificate 17 | } 18 | 19 | // HTTPOptions is the struct to pass in all http options to Gobuster 20 | type HTTPOptions struct { 21 | BasicHTTPOptions 22 | Password string 23 | URL string 24 | Username string 25 | Cookies string 26 | Headers []HTTPHeader 27 | NoCanonicalizeHeaders bool 28 | FollowRedirect bool 29 | Method string 30 | } 31 | -------------------------------------------------------------------------------- /libgobuster/progress.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | import "sync" 4 | 5 | type MessageLevel int 6 | 7 | const ( 8 | LevelDebug MessageLevel = iota 9 | LevelInfo 10 | LevelError 11 | ) 12 | 13 | type Message struct { 14 | Level MessageLevel 15 | Message string 16 | } 17 | 18 | type Progress struct { 19 | requestsExpectedMutex *sync.RWMutex 20 | requestsExpected int 21 | requestsCountMutex *sync.RWMutex 22 | requestsIssued int 23 | ResultChan chan Result 24 | ErrorChan chan error 25 | MessageChan chan Message 26 | } 27 | 28 | func NewProgress() *Progress { 29 | var p Progress 30 | p.requestsIssued = 0 31 | p.requestsExpectedMutex = new(sync.RWMutex) 32 | p.requestsCountMutex = new(sync.RWMutex) 33 | p.ResultChan = make(chan Result) 34 | p.ErrorChan = make(chan error) 35 | p.MessageChan = make(chan Message) 36 | return &p 37 | } 38 | 39 | func (p *Progress) RequestsExpected() int { 40 | p.requestsExpectedMutex.RLock() 41 | defer p.requestsExpectedMutex.RUnlock() 42 | return p.requestsExpected 43 | } 44 | 45 | func (p *Progress) RequestsIssued() int { 46 | p.requestsCountMutex.RLock() 47 | defer p.requestsCountMutex.RUnlock() 48 | return p.requestsIssued 49 | } 50 | 51 | func (p *Progress) incrementRequestsIssues(by int) { 52 | p.requestsCountMutex.Lock() 53 | defer p.requestsCountMutex.Unlock() 54 | p.requestsIssued += by 55 | } 56 | 57 | func (p *Progress) incrementRequests() { 58 | p.requestsCountMutex.Lock() 59 | defer p.requestsCountMutex.Unlock() 60 | p.requestsIssued++ 61 | } 62 | 63 | func (p *Progress) IncrementTotalRequests(by int) { 64 | p.requestsCountMutex.Lock() 65 | defer p.requestsCountMutex.Unlock() 66 | p.requestsExpected += by 67 | } 68 | -------------------------------------------------------------------------------- /libgobuster/version.go: -------------------------------------------------------------------------------- 1 | package libgobuster 2 | 3 | const ( 4 | // VERSION contains the current gobuster version 5 | VERSION = "3.6" 6 | ) 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/OJ/gobuster/v3/cli/cmd" 4 | 5 | //---------------------------------------------------- 6 | // Gobuster -- by OJ Reeves 7 | // 8 | // A crap attempt at building something that resembles 9 | // dirbuster or dirb using Go. The goal was to build 10 | // a tool that would help learn Go and to actually do 11 | // something useful. The idea of having this compile 12 | // to native code is also appealing. 13 | // 14 | // Run: gobuster -h 15 | // 16 | // Please see THANKS file for contributors. 17 | // Please see LICENSE file for license details. 18 | // 19 | //---------------------------------------------------- 20 | 21 | func main() { 22 | cmd.Execute() 23 | } 24 | --------------------------------------------------------------------------------