stupid go modules

This commit is contained in:
casual 2024-09-04 23:15:35 +03:00
commit c2c0dcde76
62 changed files with 11484 additions and 0 deletions

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM golang:latest AS build-env
WORKDIR /src
ENV CGO_ENABLED=0
COPY go.mod /src/
RUN go mod download
COPY . .
RUN go build -a -o gobuster -trimpath
FROM alpine:latest
RUN apk add --no-cache ca-certificates \
&& rm -rf /var/cache/*
RUN mkdir -p /app \
&& adduser -D gobuster \
&& chown -R gobuster:gobuster /app
USER gobuster
WORKDIR /app
COPY --from=build-env /src/gobuster .
ENTRYPOINT [ "./gobuster" ]

202
LICENSE Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

41
Makefile Normal file
View File

@ -0,0 +1,41 @@
.DEFAULT_GOAL := linux
.PHONY: linux
linux:
go build -o ./gobuster
.PHONY: windows
windows:
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o ./gobuster.exe
.PHONY: fmt
fmt:
go fmt ./...
.PHONY: update
update:
go get -u
go mod tidy -v
.PHONY: all
all: fmt update linux windows test lint
.PHONY: test
test:
go test -v -race ./...
.PHONY: lint
lint:
"$$(go env GOPATH)/bin/golangci-lint" run ./...
go mod tidy
.PHONY: lint-update
lint-update:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin
$$(go env GOPATH)/bin/golangci-lint --version
.PHONY: tag
tag:
@[ "${TAG}" ] && echo "Tagging a new version ${TAG}" || ( echo "TAG is not set"; exit 1 )
git tag -a "${TAG}" -m "${TAG}"
git push origin "${TAG}"

977
README.md Normal file
View File

@ -0,0 +1,977 @@
# Gobuster
> It's a fork https://github.com/OJ/gobuster to make it a bit more usable as a library + here is example:
```
package main
import (
"fmt"
gobustercli "git.sual.in/casual/v3/gobuster/cli"
"git.sual.in/casual/gobuster-lib/v3/gobusterdir"
"git.sual.in/casual/gobuster-lib/v3/libgobuster"
"context"
"time"
)
func DirbUrl(url string, rateLimit int, header string) ([]string, error) {
ctx := context.Background()
if rateLimit < 7 {
rateLimit = 7
}
globalopts := libgobuster.Options{
Threads: rateLimit / 7,
Wordlist: "./wordlists/dirb/big.txt",
WordlistOffset: 0,
NoStatus: true,
NoProgress: true,
Quiet: true,
NoStdout: true, // My custom option to disable other output
}
pluginopts := gobusterdir.NewOptionsDir()
pluginopts.URL = url
pluginopts.Timeout = 10 * time.Second
pluginopts.HideLength = true
pluginopts.NoTLSValidation = true
pluginopts.NoStatus = true
pluginopts.Expanded = true
pluginopts.FollowRedirect = true
pluginopts.NoTLSValidation = true
pluginopts.UserAgent = "ruina"
pluginopts.StatusCodes = "200-299,301,302,307,401,403,405,500,303"
tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes)
if err != nil {
return nil, err
}
pluginopts.StatusCodesParsed = tmpStat
log := libgobuster.NewLogger(false)
plugin, err := gobusterdir.NewGobusterDir(&globalopts, pluginopts)
if err != nil {
return nil, err
}
result, err := gobustercli.Gobuster(ctx, &globalopts, plugin, log)
if err != nil {
return result, err
}
return result, nil
}
func main() {
output, _ := DirbUrl("https://blog.ca.sual.in", 100, "")
fmt.Println(output)
}
```
TODO not complete instructions
NOTE: Go modules is complecated and i suffered a lot to make it work, so there is a notice how to use own fork for yourself
1. Delete .git
2. Create repo, clone it, move files to it
3. Make changes to repo
4. `git add . && git commit -am 'stupid go modules'`
5. `git tag v3.0.0`
6. `git push --tags`
---
Gobuster is a tool used to brute-force:
- URIs (directories and files) in web sites.
- DNS subdomains (with wildcard support).
- Virtual Host names on target web servers.
- Open Amazon S3 buckets
- Open Google Cloud buckets
- TFTP servers
## Tags, Statuses, etc
[![Build Status](https://travis-ci.com/OJ/gobuster.svg?branch=master)](https://travis-ci.com/OJ/gobuster) [![Backers on Open Collective](https://opencollective.com/gobuster/backers/badge.svg)](https://opencollective.com/gobuster) [![Sponsors on Open Collective](https://opencollective.com/gobuster/sponsors/badge.svg)](https://opencollective.com/gobuster)
## Love this tool? Back it!
If you're backing us already, you rock. If you're not, that's cool too! Want to back us? [Become a backer](https://opencollective.com/gobuster#backer)!
[![Backers](https://opencollective.com/gobuster/backers.svg?width=890)](https://opencollective.com/gobuster#backers)
All funds that are donated to this project will be donated to charity. A full log of charity donations will be available in this repository as they are processed.
# Changes
## 3.6
- Wordlist offset parameter to skip x lines from the wordlist
- prevent double slashes when building up an url in dir mode
- allow for multiple values and ranges on `--exclude-length`
- `no-fqdn` parameter on dns bruteforce to disable the use of the systems search domains. This should speed up the run if you have configured some search domains. [https://github.com/OJ/gobuster/pull/418](https://github.com/OJ/gobuster/pull/418)
## 3.5
- Allow Ranges in status code and status code blacklist. Example: 200,300-305,404
## 3.4
- Enable TLS1.0 and TLS1.1 support
- Add TFTP mode to search for files on tftp servers
## 3.3
- Support TLS client certificates / mtls
- support loading extensions from file
- support fuzzing POST body, HTTP headers and basic auth
- new option to not canonicalize header names
## 3.2
- Use go 1.19
- use contexts in the correct way
- get rid of the wildcard flag (except in DNS mode)
- color output
- retry on timeout
- google cloud bucket enumeration
- fix nil reference errors
## 3.1
- enumerate public AWS S3 buckets
- fuzzing mode
- specify HTTP method
- added support for patterns. You can now specify a file containing patterns that are applied to every word, one by line. Every occurrence of the term `{GOBUSTER}` in it will be replaced with the current wordlist item. Please use with caution as this can cause increase the number of requests issued a lot.
- The shorthand `p` flag which was assigned to proxy is now used by the pattern flag
## 3.0
- New CLI options so modes are strictly separated (`-m` is now gone!)
- Performance Optimizations and better connection handling
- Ability to enumerate vhost names
- Option to supply custom HTTP headers
# License
See the LICENSE file.
# Manual
## Available Modes
- dir - the classic directory brute-forcing mode
- dns - DNS subdomain brute-forcing mode
- s3 - Enumerate open S3 buckets and look for existence and bucket listings
- gcs - Enumerate open google cloud buckets
- vhost - virtual host brute-forcing mode (not the same as DNS!)
- fuzz - some basic fuzzing, replaces the `FUZZ` keyword
- tftp - bruteforce tftp files
## Easy Installation
### Binary Releases
We are now shipping binaries for each of the releases so that you don't even have to build them yourself! How wonderful is that!
If you're stupid enough to trust binaries that I've put together, you can download them from the [releases](https://github.com/OJ/gobuster/releases) page.
### Docker
You can also grab a prebuilt docker image from [https://github.com/OJ/gobuster/pkgs/container/gobuster](https://github.com/OJ/gobuster/pkgs/container/gobuster)
```bash
docker pull ghcr.io/oj/gobuster:latest
```
### Using `go install`
If you have a [Go](https://golang.org/) environment ready to go (at least go 1.19), it's as easy as:
```bash
go install github.com/OJ/gobuster/v3@latest
```
PS: You need at least go 1.19 to compile gobuster.
### Building From Source
Since this tool is written in [Go](https://golang.org/) you need to install the Go language/compiler/etc. Full details of installation and set up can be found [on the Go language website](https://golang.org/doc/install). Once installed you have two options. You need at least go 1.19 to compile gobuster.
### Compiling
`gobuster` has external dependencies, and so they need to be pulled in first:
```bash
go get && go build
```
This will create a `gobuster` binary for you. If you want to install it in the `$GOPATH/bin` folder you can run:
```bash
go install
```
## Modes
Help is built-in!
- `gobuster help` - outputs the top-level help.
- `gobuster help <mode>` - outputs the help specific to that mode.
## `dns` Mode
### Options
```text
Uses DNS subdomain enumeration mode
Usage:
gobuster dns [flags]
Flags:
-d, --domain string The target domain
-h, --help help for dns
-r, --resolver string Use custom DNS server (format server.com or server.com:port)
-c, --show-cname Show CNAME records (cannot be used with '-i' option)
-i, --show-ips Show IP addresses
--timeout duration DNS resolver timeout (default 1s)
--wildcard Force continued operation when wildcard found
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster dns -d mysite.com -t 50 -w common-names.txt
```
Normal sample run goes like this:
```text
gobuster dns -d google.com -w ~/wordlists/subdomains.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dns
[+] Url/Domain : google.com
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/subdomains.txt
===============================================================
2019/06/21 11:54:20 Starting gobuster
===============================================================
Found: chrome.google.com
Found: ns1.google.com
Found: admin.google.com
Found: www.google.com
Found: m.google.com
Found: support.google.com
Found: translate.google.com
Found: cse.google.com
Found: news.google.com
Found: music.google.com
Found: mail.google.com
Found: store.google.com
Found: mobile.google.com
Found: search.google.com
Found: wap.google.com
Found: directory.google.com
Found: local.google.com
Found: blog.google.com
===============================================================
2019/06/21 11:54:20 Finished
===============================================================
```
Show IP sample run goes like this:
```text
gobuster dns -d google.com -w ~/wordlists/subdomains.txt -i
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dns
[+] Url/Domain : google.com
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/subdomains.txt
===============================================================
2019/06/21 11:54:54 Starting gobuster
===============================================================
Found: www.google.com [172.217.25.36, 2404:6800:4006:802::2004]
Found: admin.google.com [172.217.25.46, 2404:6800:4006:806::200e]
Found: store.google.com [172.217.167.78, 2404:6800:4006:802::200e]
Found: mobile.google.com [172.217.25.43, 2404:6800:4006:802::200b]
Found: ns1.google.com [216.239.32.10, 2001:4860:4802:32::a]
Found: m.google.com [172.217.25.43, 2404:6800:4006:802::200b]
Found: cse.google.com [172.217.25.46, 2404:6800:4006:80a::200e]
Found: chrome.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: search.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: local.google.com [172.217.25.46, 2404:6800:4006:80a::200e]
Found: news.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: blog.google.com [216.58.199.73, 2404:6800:4006:806::2009]
Found: support.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: wap.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: directory.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: translate.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: music.google.com [172.217.25.46, 2404:6800:4006:802::200e]
Found: mail.google.com [172.217.25.37, 2404:6800:4006:802::2005]
===============================================================
2019/06/21 11:54:55 Finished
===============================================================
```
Base domain validation warning when the base domain fails to resolve. This is a warning rather than a failure in case the user fat-fingers while typing the domain.
```text
gobuster dns -d yp.to -w ~/wordlists/subdomains.txt -i
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dns
[+] Url/Domain : yp.to
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/subdomains.txt
===============================================================
2019/06/21 11:56:43 Starting gobuster
===============================================================
2019/06/21 11:56:53 [-] Unable to validate base domain: yp.to
Found: cr.yp.to [131.193.32.108, 131.193.32.109]
===============================================================
2019/06/21 11:56:53 Finished
===============================================================
```
Wildcard DNS is also detected properly:
```text
gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dns
[+] Url/Domain : 0.0.1.xip.io
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/subdomains.txt
===============================================================
2019/06/21 12:13:48 Starting gobuster
===============================================================
2019/06/21 12:13:48 [-] Wildcard DNS found. IP address(es): 1.0.0.0
2019/06/21 12:13:48 [!] To force processing of Wildcard DNS, specify the '--wildcard' switch.
===============================================================
2019/06/21 12:13:48 Finished
===============================================================
```
If the user wants to force processing of a domain that has wildcard entries, use `--wildcard`:
```text
gobuster dns -d 0.0.1.xip.io -w ~/wordlists/subdomains.txt --wildcard
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dns
[+] Url/Domain : 0.0.1.xip.io
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/subdomains.txt
===============================================================
2019/06/21 12:13:51 Starting gobuster
===============================================================
2019/06/21 12:13:51 [-] Wildcard DNS found. IP address(es): 1.0.0.0
Found: 127.0.0.1.xip.io
Found: test.127.0.0.1.xip.io
===============================================================
2019/06/21 12:13:53 Finished
===============================================================
```
## `dir` Mode
### Options
```text
Uses directory/file enumeration mode
Usage:
gobuster dir [flags]
Flags:
-f, --add-slash Append / to each request
-c, --cookies string Cookies to use for the requests
-d, --discover-backup Also search for backup files by appending multiple backup extensions
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-e, --expanded Expanded mode, print full URLs
-x, --extensions string File extension(s) to search for
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for dir
--hide-length Hide the length of the body in the output
-m, --method string Use the following HTTP method (default "GET")
-n, --no-status Don't print status codes
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
-s, --status-codes string Positive status codes (will be overwritten with status-codes-blacklist if set)
-b, --status-codes-blacklist string Negative status codes (will override status-codes if set) (default "404")
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster dir -u https://mysite.com/path/to/folder -c 'session=123456' -t 50 -w common-files.txt -x .php,.html
```
Default options looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Timeout : 10s
===============================================================
2019/06/21 11:49:43 Starting gobuster
===============================================================
/categories (Status: 301)
/contact (Status: 301)
/posts (Status: 301)
/index (Status: 200)
===============================================================
2019/06/21 11:49:44 Finished
===============================================================
```
Default options with status codes disabled looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -n
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] No status : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:18 Starting gobuster
===============================================================
/categories
/contact
/index
/posts
===============================================================
2019/06/21 11:50:18 Finished
===============================================================
```
Verbose output looks like this:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -v
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Verbose : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:50:51 Starting gobuster
===============================================================
Missed: /alsodoesnotexist (Status: 404)
Found: /index (Status: 200)
Missed: /doesnotexist (Status: 404)
Found: /categories (Status: 301)
Found: /posts (Status: 301)
Found: /contact (Status: 301)
===============================================================
2019/06/21 11:50:51 Finished
===============================================================
```
Example showing content length:
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -l
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Mode : dir
[+] Url/Domain : https://buffered.io/
[+] Threads : 10
[+] Wordlist : /home/oj/wordlists/shortlist.txt
[+] Status codes : 200,204,301,302,307,401,403
[+] User Agent : gobuster/3.2.0
[+] Show length : true
[+] Timeout : 10s
===============================================================
2019/06/21 11:51:16 Starting gobuster
===============================================================
/categories (Status: 301) [Size: 178]
/posts (Status: 301) [Size: 178]
/contact (Status: 301) [Size: 178]
/index (Status: 200) [Size: 51759]
===============================================================
2019/06/21 11:51:17 Finished
===============================================================
```
Quiet output, with status disabled and expanded mode looks like this ("grep mode"):
```text
gobuster dir -u https://buffered.io -w ~/wordlists/shortlist.txt -q -n -e
https://buffered.io/index
https://buffered.io/contact
https://buffered.io/posts
https://buffered.io/categories
```
## `vhost` Mode
### Options
```text
Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)
Usage:
gobuster vhost [flags]
Flags:
--append-domain Append main domain from URL to words from wordlist. Otherwise the fully qualified domains need to be specified in the wordlist.
-c, --cookies string Cookies to use for the requests
--domain string the domain to append when using an IP address as URL. If left empty and you specify a domain based URL the hostname from the URL is extracted
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for vhost
-m, --method string Use the following HTTP method (default "GET")
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster vhost -u https://mysite.com -w common-vhosts.txt
```
Normal sample run goes like this:
```text
gobuster vhost -u https://mysite.com -w common-vhosts.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://mysite.com
[+] Threads: 10
[+] Wordlist: common-vhosts.txt
[+] User Agent: gobuster/3.2.0
[+] Timeout: 10s
===============================================================
2019/06/21 08:36:00 Starting gobuster
===============================================================
Found: www.mysite.com
Found: piwik.mysite.com
Found: mail.mysite.com
===============================================================
2019/06/21 08:36:05 Finished
===============================================================
```
## `fuzz` Mode
### Options
```text
Uses fuzzing mode
Usage:
gobuster fuzz [flags]
Flags:
-c, --cookies string Cookies to use for the requests
--exclude-length ints exclude the following content length (completely ignores the status). Supply multiple times to exclude multiple sizes.
-b, --excludestatuscodes string Negative status codes (will override statuscodes if set)
-r, --follow-redirect Follow redirects
-H, --headers stringArray Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'
-h, --help help for fuzz
-m, --method string Use the following HTTP method (default "GET")
-k, --no-tls-validation Skip TLS certificate verification
-P, --password string Password for Basic Auth
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-u, --url string The target URL
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
-U, --username string Username for Basic Auth
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster fuzz -u https://example.com?FUZZ=test -w parameter-names.txt
```
## `s3` Mode
### Options
```text
Uses aws bucket enumeration mode
Usage:
gobuster s3 [flags]
Flags:
-h, --help help for s3
-m, --maxfiles int max files to list when listing buckets (only shown in verbose mode) (default 5)
-k, --no-tls-validation Skip TLS certificate verification
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster s3 -w bucket-names.txt
```
## `gcs` Mode
### Options
```text
Uses gcs bucket enumeration mode
Usage:
gobuster gcs [flags]
Flags:
-h, --help help for gcs
-m, --maxfiles int max files to list when listing buckets (only shown in verbose mode) (default 5)
-k, --no-tls-validation Skip TLS certificate verification
--proxy string Proxy to use for requests [http(s)://host:port]
--random-agent Use a random User-Agent string
--retry Should retry on request timeout
--retry-attempts int Times to retry on request timeout (default 3)
--timeout duration HTTP Timeout (default 10s)
-a, --useragent string Set the User-Agent string (default "gobuster/3.2.0")
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster gcs -w bucket-names.txt
```
## `tftp` Mode
### Options
```text
Uses TFTP enumeration mode
Usage:
gobuster tftp [flags]
Flags:
-h, --help help for tftp
-s, --server string The target TFTP server
--timeout duration TFTP timeout (default 1s)
Global Flags:
--delay duration Time each thread waits between requests (e.g. 1500ms)
--no-color Disable color output
--no-error Don't display errors
-z, --no-progress Don't display progress
-o, --output string Output file to write results to (defaults to stdout)
-p, --pattern string File containing replacement patterns
-q, --quiet Don't print the banner and other noise
-t, --threads int Number of concurrent threads (default 10)
-v, --verbose Verbose output (errors)
-w, --wordlist string Path to the wordlist
```
### Examples
```text
gobuster tftp -s tftp.example.com -w common-filenames.txt
```
## Wordlists via STDIN
Wordlists can be piped into `gobuster` via stdin by providing a `-` to the `-w` option:
```bash
hashcat -a 3 --stdout ?l | gobuster dir -u https://mysite.com -w -
```
Note: If the `-w` option is specified at the same time as piping from STDIN, an error will be shown and the program will terminate.
## Patterns
You can supply pattern files that will be applied to every word from the wordlist.
Just place the string `{GOBUSTER}` in it and this will be replaced with the word.
This feature is also handy in s3 mode to pre- or postfix certain patterns.
**Caution:** Using a big pattern file can cause a lot of request as every pattern is applied to every word in the wordlist.
### Example file
```text
{GOBUSTER}Partial
{GOBUSTER}Service
PRE{GOBUSTER}POST
{GOBUSTER}-prod
{GOBUSTER}-dev
```
#### Use case in combination with patterns
- Create a custom wordlist for the target containing company names and so on
- Create a pattern file to use for common bucket names.
```bash
curl -s --output - https://raw.githubusercontent.com/eth0izzle/bucket-stream/master/permutations/extended.txt | sed -s 's/%s/{GOBUSTER}/' > patterns.txt
```
- Run gobuster with the custom input. Be sure to turn verbose mode on to see the bucket details
```text
gobuster s3 --wordlist my.custom.wordlist -p patterns.txt -v
```
Normal sample run goes like this:
```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Threads: 10
[+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.2.0
[+] Timeout: 10s
[+] Maximum files to list: 5
===============================================================
2019/08/12 21:48:16 Starting gobuster in S3 bucket enumeration mode
===============================================================
webmail
hacking
css
img
www
dav
web
localhost
===============================================================
2019/08/12 21:48:17 Finished
===============================================================
```
Verbose and sample run
```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -v
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Threads: 10
[+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.2.0
[+] Verbose: true
[+] Timeout: 10s
[+] Maximum files to list: 5
===============================================================
2019/08/12 21:49:00 Starting gobuster in S3 bucket enumeration mode
===============================================================
www [Error: All access to this object has been disabled (AllAccessDisabled)]
hacking [Error: Access Denied (AccessDenied)]
css [Error: All access to this object has been disabled (AllAccessDisabled)]
webmail [Error: All access to this object has been disabled (AllAccessDisabled)]
img [Bucket Listing enabled: GodBlessPotomac1.jpg (1236807b), HOMEWORKOUTAUDIO.zip (203908818b), ProductionInfo.xml (11946b), Start of Perpetual Motion Logo-1.mp3 (621821b), addressbook.gif (3115b)]
web [Error: Access Denied (AccessDenied)]
dav [Error: All access to this object has been disabled (AllAccessDisabled)]
localhost [Error: Access Denied (AccessDenied)]
===============================================================
2019/08/12 21:49:01 Finished
===============================================================
```
Extended sample run
```text
PS C:\Users\firefart\Documents\code\gobuster> .\gobuster.exe s3 --wordlist .\wordlist.txt -e
===============================================================
Gobuster v3.2.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Threads: 10
[+] Wordlist: .\wordlist.txt
[+] User Agent: gobuster/3.2.0
[+] Timeout: 10s
[+] Expanded: true
[+] Maximum files to list: 5
===============================================================
2019/08/12 21:48:38 Starting gobuster in S3 bucket enumeration mode
===============================================================
http://css.s3.amazonaws.com/
http://www.s3.amazonaws.com/
http://webmail.s3.amazonaws.com/
http://hacking.s3.amazonaws.com/
http://img.s3.amazonaws.com/
http://web.s3.amazonaws.com/
http://dav.s3.amazonaws.com/
http://localhost.s3.amazonaws.com/
===============================================================
2019/08/12 21:48:38 Finished
===============================================================
```

188
cli/cmd/dir.go Normal file
View File

@ -0,0 +1,188 @@
package cmd
import (
"errors"
"fmt"
"log"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobusterdir"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdDir *cobra.Command
func runDir(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseDirOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobusterdir.NewGobusterDir(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobusterdir: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
var wErr *gobusterdir.ErrWildcard
if errors.As(err, &wErr) {
return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr)
}
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseDirOptions() (*libgobuster.Options, *gobusterdir.OptionsDir, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobusterdir.NewOptionsDir()
httpOpts, err := parseCommonHTTPOptions(cmdDir)
if err != nil {
return nil, nil, err
}
pluginOpts.Password = httpOpts.Password
pluginOpts.URL = httpOpts.URL
pluginOpts.UserAgent = httpOpts.UserAgent
pluginOpts.Username = httpOpts.Username
pluginOpts.Proxy = httpOpts.Proxy
pluginOpts.Cookies = httpOpts.Cookies
pluginOpts.Timeout = httpOpts.Timeout
pluginOpts.FollowRedirect = httpOpts.FollowRedirect
pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation
pluginOpts.Headers = httpOpts.Headers
pluginOpts.Method = httpOpts.Method
pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout
pluginOpts.RetryAttempts = httpOpts.RetryAttempts
pluginOpts.TLSCertificate = httpOpts.TLSCertificate
pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders
pluginOpts.Extensions, err = cmdDir.Flags().GetString("extensions")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for extensions: %w", err)
}
ret, err := libgobuster.ParseExtensions(pluginOpts.Extensions)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for extensions: %w", err)
}
pluginOpts.ExtensionsParsed = ret
pluginOpts.ExtensionsFile, err = cmdDir.Flags().GetString("extensions-file")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err)
}
if pluginOpts.ExtensionsFile != "" {
extensions, err := libgobuster.ParseExtensionsFile(pluginOpts.ExtensionsFile)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for extensions file: %w", err)
}
pluginOpts.ExtensionsParsed.AddRange(extensions)
}
// parse normal status codes
pluginOpts.StatusCodes, err = cmdDir.Flags().GetString("status-codes")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err)
}
ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodes)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for status-codes: %w", err)
}
pluginOpts.StatusCodesParsed = ret2
// blacklist will override the normal status codes
pluginOpts.StatusCodesBlacklist, err = cmdDir.Flags().GetString("status-codes-blacklist")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err)
}
ret3, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.StatusCodesBlacklist)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for status-codes-blacklist: %w", err)
}
pluginOpts.StatusCodesBlacklistParsed = ret3
if pluginOpts.StatusCodes != "" && pluginOpts.StatusCodesBlacklist != "" {
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.",
pluginOpts.StatusCodes, pluginOpts.StatusCodesBlacklist)
}
if pluginOpts.StatusCodes == "" && pluginOpts.StatusCodesBlacklist == "" {
return nil, nil, fmt.Errorf("status-codes and status-codes-blacklist are both not set, please set one")
}
pluginOpts.UseSlash, err = cmdDir.Flags().GetBool("add-slash")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for add-slash: %w", err)
}
pluginOpts.Expanded, err = cmdDir.Flags().GetBool("expanded")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for expanded: %w", err)
}
pluginOpts.NoStatus, err = cmdDir.Flags().GetBool("no-status")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for no-status: %w", err)
}
pluginOpts.HideLength, err = cmdDir.Flags().GetBool("hide-length")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for hide-length: %w", err)
}
pluginOpts.DiscoverBackup, err = cmdDir.Flags().GetBool("discover-backup")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for discover-backup: %w", err)
}
pluginOpts.ExcludeLength, err = cmdDir.Flags().GetString("exclude-length")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
ret4, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret4
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdDir = &cobra.Command{
Use: "dir",
Short: "Uses directory/file enumeration mode",
RunE: runDir,
}
if err := addCommonHTTPOptions(cmdDir); err != nil {
log.Fatalf("%v", err)
}
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.")
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.")
cmdDir.Flags().StringP("extensions", "x", "", "File extension(s) to search for")
cmdDir.Flags().StringP("extensions-file", "X", "", "Read file extension(s) to search from the file")
cmdDir.Flags().BoolP("expanded", "e", false, "Expanded mode, print full URLs")
cmdDir.Flags().BoolP("no-status", "n", false, "Don't print status codes")
cmdDir.Flags().Bool("hide-length", false, "Hide the length of the body in the output")
cmdDir.Flags().BoolP("add-slash", "f", false, "Append / to each request")
cmdDir.Flags().BoolP("discover-backup", "d", false, "Also search for backup files by appending multiple backup extensions")
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")
cmdDir.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdDir)
}

88
cli/cmd/dir_test.go Normal file
View File

@ -0,0 +1,88 @@
package cmd
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobusterdir"
"git.sual.in/casual/gobuster-lib/libgobuster"
)
func httpServer(b *testing.B, content string) *httptest.Server {
b.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, content)
}))
return ts
}
func BenchmarkDirMode(b *testing.B) {
h := httpServer(b, "test")
defer h.Close()
pluginopts := gobusterdir.NewOptionsDir()
pluginopts.URL = h.URL
pluginopts.Timeout = 10 * time.Second
pluginopts.Extensions = ".php,.csv"
tmpExt, err := libgobuster.ParseExtensions(pluginopts.Extensions)
if err != nil {
b.Fatalf("could not parse extensions: %v", err)
}
pluginopts.ExtensionsParsed = tmpExt
pluginopts.StatusCodes = "200,204,301,302,307,401,403"
tmpStat, err := libgobuster.ParseCommaSeparatedInt(pluginopts.StatusCodes)
if err != nil {
b.Fatalf("could not parse status codes: %v", err)
}
pluginopts.StatusCodesParsed = tmpStat
wordlist, err := os.CreateTemp("", "")
if err != nil {
b.Fatalf("could not create tempfile: %v", err)
}
defer os.Remove(wordlist.Name())
for w := 0; w < 1000; w++ {
_, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w))
}
wordlist.Close()
globalopts := libgobuster.Options{
Threads: 10,
Wordlist: wordlist.Name(),
NoProgress: true,
}
ctx := context.Background()
oldStdout := os.Stdout
oldStderr := os.Stderr
defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
devnull, err := os.Open(os.DevNull)
if err != nil {
b.Fatalf("could not get devnull %v", err)
}
defer devnull.Close()
log := libgobuster.NewLogger(false)
// Run the real benchmark
for x := 0; x < b.N; x++ {
os.Stdout = devnull
os.Stderr = devnull
plugin, err := gobusterdir.NewGobusterDir(&globalopts, pluginopts)
if err != nil {
b.Fatalf("error on creating gobusterdir: %v", err)
}
if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
b.Fatalf("error on running gobuster: %v", err)
}
os.Stdout = oldStdout
os.Stderr = oldStderr
}
}

115
cli/cmd/dns.go Normal file
View File

@ -0,0 +1,115 @@
package cmd
import (
"errors"
"fmt"
"log"
"runtime"
"time"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobusterdns"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdDNS *cobra.Command
func runDNS(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseDNSOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobusterdns.NewGobusterDNS(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobusterdns: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
var wErr *gobusterdns.ErrWildcard
if errors.As(err, &wErr) {
return fmt.Errorf("%w. To force processing of Wildcard DNS, specify the '--wildcard' switch", wErr)
}
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseDNSOptions() (*libgobuster.Options, *gobusterdns.OptionsDNS, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobusterdns.NewOptionsDNS()
pluginOpts.Domain, err = cmdDNS.Flags().GetString("domain")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for domain: %w", err)
}
pluginOpts.ShowIPs, err = cmdDNS.Flags().GetBool("show-ips")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for show-ips: %w", err)
}
pluginOpts.ShowCNAME, err = cmdDNS.Flags().GetBool("show-cname")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for show-cname: %w", err)
}
pluginOpts.WildcardForced, err = cmdDNS.Flags().GetBool("wildcard")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for wildcard: %w", err)
}
pluginOpts.Timeout, err = cmdDNS.Flags().GetDuration("timeout")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for timeout: %w", err)
}
pluginOpts.Resolver, err = cmdDNS.Flags().GetString("resolver")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for resolver: %w", err)
}
pluginOpts.NoFQDN, err = cmdDNS.Flags().GetBool("no-fqdn")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for no-fqdn: %w", err)
}
if pluginOpts.Resolver != "" && runtime.GOOS == "windows" {
return nil, nil, fmt.Errorf("currently can not set custom dns resolver on windows. See https://golang.org/pkg/net/#hdr-Name_Resolution")
}
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdDNS = &cobra.Command{
Use: "dns",
Short: "Uses DNS subdomain enumeration mode",
RunE: runDNS,
}
cmdDNS.Flags().StringP("domain", "d", "", "The target domain")
cmdDNS.Flags().BoolP("show-ips", "i", false, "Show IP addresses")
cmdDNS.Flags().BoolP("show-cname", "c", false, "Show CNAME records (cannot be used with '-i' option)")
cmdDNS.Flags().DurationP("timeout", "", time.Second, "DNS resolver timeout")
cmdDNS.Flags().BoolP("wildcard", "", false, "Force continued operation when wildcard found")
cmdDNS.Flags().BoolP("no-fqdn", "", false, "Do not automatically add a trailing dot to the domain, so the resolver uses the DNS search domain")
cmdDNS.Flags().StringP("resolver", "r", "", "Use custom DNS server (format server.com or server.com:port)")
if err := cmdDNS.MarkFlagRequired("domain"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
cmdDNS.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdDNS)
}

148
cli/cmd/fuzz.go Normal file
View File

@ -0,0 +1,148 @@
package cmd
import (
"errors"
"fmt"
"log"
"strings"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobusterfuzz"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdFuzz *cobra.Command
func runFuzz(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseFuzzOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
if !containsFuzzKeyword(*pluginopts) {
return fmt.Errorf("please provide the %s keyword", gobusterfuzz.FuzzKeyword)
}
plugin, err := gobusterfuzz.NewGobusterFuzz(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobusterfuzz: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
var wErr *gobusterfuzz.ErrWildcard
if errors.As(err, &wErr) {
return fmt.Errorf("%w. To continue please exclude the status code or the length", wErr)
}
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseFuzzOptions() (*libgobuster.Options, *gobusterfuzz.OptionsFuzz, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobusterfuzz.NewOptionsFuzz()
httpOpts, err := parseCommonHTTPOptions(cmdFuzz)
if err != nil {
return nil, nil, err
}
pluginOpts.Password = httpOpts.Password
pluginOpts.URL = httpOpts.URL
pluginOpts.UserAgent = httpOpts.UserAgent
pluginOpts.Username = httpOpts.Username
pluginOpts.Proxy = httpOpts.Proxy
pluginOpts.Cookies = httpOpts.Cookies
pluginOpts.Timeout = httpOpts.Timeout
pluginOpts.FollowRedirect = httpOpts.FollowRedirect
pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation
pluginOpts.Headers = httpOpts.Headers
pluginOpts.Method = httpOpts.Method
pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout
pluginOpts.RetryAttempts = httpOpts.RetryAttempts
pluginOpts.TLSCertificate = httpOpts.TLSCertificate
pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders
// blacklist will override the normal status codes
pluginOpts.ExcludedStatusCodes, err = cmdFuzz.Flags().GetString("excludestatuscodes")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err)
}
ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludedStatusCodes)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for excludestatuscodes: %w", err)
}
pluginOpts.ExcludedStatusCodesParsed = ret
pluginOpts.ExcludeLength, err = cmdFuzz.Flags().GetString("exclude-length")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
ret2, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret2
pluginOpts.RequestBody, err = cmdFuzz.Flags().GetString("body")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for body: %w", err)
}
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdFuzz = &cobra.Command{
Use: "fuzz",
Short: fmt.Sprintf("Uses fuzzing mode. Replaces the keyword %s in the URL, Headers and the request body", gobusterfuzz.FuzzKeyword),
RunE: runFuzz,
}
if err := addCommonHTTPOptions(cmdFuzz); err != nil {
log.Fatalf("%v", err)
}
cmdFuzz.Flags().StringP("excludestatuscodes", "b", "", "Excluded status codes. Can also handle ranges like 200,300-400,404.")
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")
cmdFuzz.Flags().StringP("body", "B", "", "Request body")
cmdFuzz.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdFuzz)
}
func containsFuzzKeyword(pluginopts gobusterfuzz.OptionsFuzz) bool {
if strings.Contains(pluginopts.URL, gobusterfuzz.FuzzKeyword) {
return true
}
if strings.Contains(pluginopts.RequestBody, gobusterfuzz.FuzzKeyword) {
return true
}
for _, h := range pluginopts.Headers {
if strings.Contains(h.Name, gobusterfuzz.FuzzKeyword) || strings.Contains(h.Value, gobusterfuzz.FuzzKeyword) {
return true
}
}
if strings.Contains(pluginopts.Username, gobusterfuzz.FuzzKeyword) {
return true
}
if strings.Contains(pluginopts.Password, gobusterfuzz.FuzzKeyword) {
return true
}
return false
}

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

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

258
cli/cmd/http.go Normal file
View File

@ -0,0 +1,258 @@
package cmd
import (
"crypto/tls"
"encoding/pem"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
"golang.org/x/crypto/pkcs12"
"golang.org/x/term"
)
func addBasicHTTPOptions(cmd *cobra.Command) {
cmd.Flags().StringP("useragent", "a", libgobuster.DefaultUserAgent(), "Set the User-Agent string")
cmd.Flags().BoolP("random-agent", "", false, "Use a random User-Agent string")
cmd.Flags().StringP("proxy", "", "", "Proxy to use for requests [http(s)://host:port] or [socks5://host:port]")
cmd.Flags().DurationP("timeout", "", 10*time.Second, "HTTP Timeout")
cmd.Flags().BoolP("no-tls-validation", "k", false, "Skip TLS certificate verification")
cmd.Flags().BoolP("retry", "", false, "Should retry on request timeout")
cmd.Flags().IntP("retry-attempts", "", 3, "Times to retry on request timeout")
// client certificates, either pem or p12
cmd.Flags().StringP("client-cert-pem", "", "", "public key in PEM format for optional TLS client certificates")
cmd.Flags().StringP("client-cert-pem-key", "", "", "private key in PEM format for optional TLS client certificates (this key needs to have no password)")
cmd.Flags().StringP("client-cert-p12", "", "", "a p12 file to use for options TLS client certificates")
cmd.Flags().StringP("client-cert-p12-password", "", "", "the password to the p12 file")
}
func addCommonHTTPOptions(cmd *cobra.Command) error {
addBasicHTTPOptions(cmd)
cmd.Flags().StringP("url", "u", "", "The target URL")
cmd.Flags().StringP("cookies", "c", "", "Cookies to use for the requests")
cmd.Flags().StringP("username", "U", "", "Username for Basic Auth")
cmd.Flags().StringP("password", "P", "", "Password for Basic Auth")
cmd.Flags().BoolP("follow-redirect", "r", false, "Follow redirects")
cmd.Flags().StringArrayP("headers", "H", []string{""}, "Specify HTTP headers, -H 'Header1: val1' -H 'Header2: val2'")
cmd.Flags().BoolP("no-canonicalize-headers", "", false, "Do not canonicalize HTTP header names. If set header names are sent as is.")
cmd.Flags().StringP("method", "m", "GET", "Use the following HTTP method")
if err := cmd.MarkFlagRequired("url"); err != nil {
return fmt.Errorf("error on marking flag as required: %w", err)
}
return nil
}
func parseBasicHTTPOptions(cmd *cobra.Command) (libgobuster.BasicHTTPOptions, error) {
options := libgobuster.BasicHTTPOptions{}
var err error
options.UserAgent, err = cmd.Flags().GetString("useragent")
if err != nil {
return options, fmt.Errorf("invalid value for useragent: %w", err)
}
randomUA, err := cmd.Flags().GetBool("random-agent")
if err != nil {
return options, fmt.Errorf("invalid value for random-agent: %w", err)
}
if randomUA {
ua, err := libgobuster.GetRandomUserAgent()
if err != nil {
return options, err
}
options.UserAgent = ua
}
options.Proxy, err = cmd.Flags().GetString("proxy")
if err != nil {
return options, fmt.Errorf("invalid value for proxy: %w", err)
}
options.Timeout, err = cmd.Flags().GetDuration("timeout")
if err != nil {
return options, fmt.Errorf("invalid value for timeout: %w", err)
}
options.RetryOnTimeout, err = cmd.Flags().GetBool("retry")
if err != nil {
return options, fmt.Errorf("invalid value for retry: %w", err)
}
options.RetryAttempts, err = cmd.Flags().GetInt("retry-attempts")
if err != nil {
return options, fmt.Errorf("invalid value for retry-attempts: %w", err)
}
options.NoTLSValidation, err = cmd.Flags().GetBool("no-tls-validation")
if err != nil {
return options, fmt.Errorf("invalid value for no-tls-validation: %w", err)
}
pemFile, err := cmd.Flags().GetString("client-cert-pem")
if err != nil {
return options, fmt.Errorf("invalid value for client-cert-pem: %w", err)
}
pemKeyFile, err := cmd.Flags().GetString("client-cert-pem-key")
if err != nil {
return options, fmt.Errorf("invalid value for client-cert-pem-key: %w", err)
}
p12File, err := cmd.Flags().GetString("client-cert-p12")
if err != nil {
return options, fmt.Errorf("invalid value for client-cert-p12: %w", err)
}
p12Pass, err := cmd.Flags().GetString("client-cert-p12-password")
if err != nil {
return options, fmt.Errorf("invalid value for client-cert-p12-password: %w", err)
}
if pemFile != "" && p12File != "" {
return options, fmt.Errorf("please supply either a pem or a p12, not both")
}
if pemFile != "" {
cert, err := tls.LoadX509KeyPair(pemFile, pemKeyFile)
if err != nil {
return options, fmt.Errorf("could not load supplied pem key: %w", err)
}
options.TLSCertificate = &cert
} else if p12File != "" {
p12Content, err := os.ReadFile(p12File)
if err != nil {
return options, fmt.Errorf("could not read p12 %s: %w", p12File, err)
}
blocks, err := pkcs12.ToPEM(p12Content, p12Pass)
if err != nil {
return options, fmt.Errorf("could not load P12: %w", err)
}
var pemData []byte
for _, b := range blocks {
pemData = append(pemData, pem.EncodeToMemory(b)...)
}
cert, err := tls.X509KeyPair(pemData, pemData)
if err != nil {
return options, fmt.Errorf("could not load certificate from P12: %w", err)
}
options.TLSCertificate = &cert
}
return options, nil
}
func parseCommonHTTPOptions(cmd *cobra.Command) (libgobuster.HTTPOptions, error) {
options := libgobuster.HTTPOptions{}
var err error
basic, err := parseBasicHTTPOptions(cmd)
if err != nil {
return options, err
}
options.Proxy = basic.Proxy
options.Timeout = basic.Timeout
options.UserAgent = basic.UserAgent
options.NoTLSValidation = basic.NoTLSValidation
options.RetryOnTimeout = basic.RetryOnTimeout
options.RetryAttempts = basic.RetryAttempts
options.TLSCertificate = basic.TLSCertificate
options.URL, err = cmd.Flags().GetString("url")
if err != nil {
return options, fmt.Errorf("invalid value for url: %w", err)
}
if !strings.HasPrefix(options.URL, "http") {
// check to see if a port was specified
re := regexp.MustCompile(`^[^/]+:(\d+)`)
match := re.FindStringSubmatch(options.URL)
if len(match) < 2 {
// no port, default to http on 80
options.URL = fmt.Sprintf("http://%s", options.URL)
} else {
port, err2 := strconv.Atoi(match[1])
if err2 != nil || (port != 80 && port != 443) {
return options, fmt.Errorf("url scheme not specified")
} else if port == 80 {
options.URL = fmt.Sprintf("http://%s", options.URL)
} else {
options.URL = fmt.Sprintf("https://%s", options.URL)
}
}
}
options.Cookies, err = cmd.Flags().GetString("cookies")
if err != nil {
return options, fmt.Errorf("invalid value for cookies: %w", err)
}
options.Username, err = cmd.Flags().GetString("username")
if err != nil {
return options, fmt.Errorf("invalid value for username: %w", err)
}
options.Password, err = cmd.Flags().GetString("password")
if err != nil {
return options, fmt.Errorf("invalid value for password: %w", err)
}
options.FollowRedirect, err = cmd.Flags().GetBool("follow-redirect")
if err != nil {
return options, fmt.Errorf("invalid value for follow-redirect: %w", err)
}
options.Method, err = cmd.Flags().GetString("method")
if err != nil {
return options, fmt.Errorf("invalid value for method: %w", err)
}
headers, err := cmd.Flags().GetStringArray("headers")
if err != nil {
return options, fmt.Errorf("invalid value for headers: %w", err)
}
for _, h := range headers {
keyAndValue := strings.SplitN(h, ":", 2)
if len(keyAndValue) != 2 {
return options, fmt.Errorf("invalid header format for header %q", h)
}
key := strings.TrimSpace(keyAndValue[0])
value := strings.TrimSpace(keyAndValue[1])
if len(key) == 0 {
return options, fmt.Errorf("invalid header format for header %q - name is empty", h)
}
header := libgobuster.HTTPHeader{Name: key, Value: value}
options.Headers = append(options.Headers, header)
}
noCanonHeaders, err := cmd.Flags().GetBool("no-canonicalize-headers")
if err != nil {
return options, fmt.Errorf("invalid value for no-canonicalize-headers: %w", err)
}
options.NoCanonicalizeHeaders = noCanonHeaders
// Prompt for PW if not provided
if options.Username != "" && options.Password == "" {
fmt.Printf("[?] Auth Password: ")
// please don't remove the int cast here as it is sadly needed on windows :/
passBytes, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
// print a newline to simulate the newline that was entered
// this means that formatting/printing after doesn't look bad.
fmt.Println("")
if err != nil {
return options, fmt.Errorf("username given but reading of password failed")
}
options.Password = string(passBytes)
}
// if it's still empty bail out
if options.Username != "" && options.Password == "" {
return options, fmt.Errorf("username was provided but password is missing")
}
return options, nil
}

194
cli/cmd/root.go Normal file
View File

@ -0,0 +1,194 @@
package cmd
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/signal"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/fatih/color"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var rootCmd = &cobra.Command{
Use: "gobuster",
SilenceUsage: true,
}
// nolint:gochecknoglobals
var mainContext context.Context
// Execute is the main cobra method
func Execute() {
var cancel context.CancelFunc
mainContext, cancel = context.WithCancel(context.Background())
defer cancel()
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
defer func() {
signal.Stop(signalChan)
cancel()
}()
go func() {
select {
case <-signalChan:
// caught CTRL+C
fmt.Println("\n[!] Keyboard interrupt detected, terminating.")
cancel()
case <-mainContext.Done():
}
}()
if err := rootCmd.Execute(); err != nil {
// Leaving this in results in the same error appearing twice
// Once before and once after the help output. Not sure if
// this is going to be needed to output other errors that
// aren't automatically outputted.
// fmt.Println(err)
os.Exit(1)
}
}
func parseGlobalOptions() (*libgobuster.Options, error) {
globalopts := libgobuster.NewOptions()
threads, err := rootCmd.Flags().GetInt("threads")
if err != nil {
return nil, fmt.Errorf("invalid value for threads: %w", err)
}
if threads <= 0 {
return nil, fmt.Errorf("threads must be bigger than 0")
}
globalopts.Threads = threads
delay, err := rootCmd.Flags().GetDuration("delay")
if err != nil {
return nil, fmt.Errorf("invalid value for delay: %w", err)
}
if delay < 0 {
return nil, fmt.Errorf("delay must be positive")
}
globalopts.Delay = delay
globalopts.Wordlist, err = rootCmd.Flags().GetString("wordlist")
if err != nil {
return nil, fmt.Errorf("invalid value for wordlist: %w", err)
}
if globalopts.Wordlist == "-" {
// STDIN
} else if _, err2 := os.Stat(globalopts.Wordlist); os.IsNotExist(err2) {
return nil, fmt.Errorf("wordlist file %q does not exist: %w", globalopts.Wordlist, err2)
}
offset, err := rootCmd.Flags().GetInt("wordlist-offset")
if err != nil {
return nil, fmt.Errorf("invalid value for wordlist-offset: %w", err)
}
if offset < 0 {
return nil, fmt.Errorf("wordlist-offset must be bigger or equal to 0")
}
globalopts.WordlistOffset = offset
if globalopts.Wordlist == "-" && globalopts.WordlistOffset > 0 {
return nil, fmt.Errorf("wordlist-offset is not supported when reading from STDIN")
}
globalopts.PatternFile, err = rootCmd.Flags().GetString("pattern")
if err != nil {
return nil, fmt.Errorf("invalid value for pattern: %w", err)
}
if globalopts.PatternFile != "" {
if _, err = os.Stat(globalopts.PatternFile); os.IsNotExist(err) {
return nil, fmt.Errorf("pattern file %q does not exist: %w", globalopts.PatternFile, err)
}
patternFile, err := os.Open(globalopts.PatternFile)
if err != nil {
return nil, fmt.Errorf("could not open pattern file %q: %w", globalopts.PatternFile, err)
}
defer patternFile.Close()
scanner := bufio.NewScanner(patternFile)
for scanner.Scan() {
globalopts.Patterns = append(globalopts.Patterns, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("could not read pattern file %q: %w", globalopts.PatternFile, err)
}
}
globalopts.OutputFilename, err = rootCmd.Flags().GetString("output")
if err != nil {
return nil, fmt.Errorf("invalid value for output filename: %w", err)
}
globalopts.Verbose, err = rootCmd.Flags().GetBool("verbose")
if err != nil {
return nil, fmt.Errorf("invalid value for verbose: %w", err)
}
globalopts.Quiet, err = rootCmd.Flags().GetBool("quiet")
if err != nil {
return nil, fmt.Errorf("invalid value for quiet: %w", err)
}
globalopts.NoProgress, err = rootCmd.Flags().GetBool("no-progress")
if err != nil {
return nil, fmt.Errorf("invalid value for no-progress: %w", err)
}
globalopts.NoError, err = rootCmd.Flags().GetBool("no-error")
if err != nil {
return nil, fmt.Errorf("invalid value for no-error: %w", err)
}
noColor, err := rootCmd.Flags().GetBool("no-color")
if err != nil {
return nil, fmt.Errorf("invalid value for no-color: %w", err)
}
if noColor {
color.NoColor = true
}
globalopts.Debug, err = rootCmd.Flags().GetBool("debug")
if err != nil {
return nil, fmt.Errorf("invalid value for debug: %w", err)
}
return globalopts, nil
}
// This has to be called as part of the pre-run for sub commands. Including
// this in the init() function results in the built-in `help` command not
// working as intended. The required flags should only be marked as required
// on the global flags when one of the non-help commands is used.
func configureGlobalOptions() {
if err := rootCmd.MarkPersistentFlagRequired("wordlist"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
}
// nolint:gochecknoinits
func init() {
rootCmd.PersistentFlags().DurationP("delay", "", 0, "Time each thread waits between requests (e.g. 1500ms)")
rootCmd.PersistentFlags().IntP("threads", "t", 10, "Number of concurrent threads")
rootCmd.PersistentFlags().StringP("wordlist", "w", "", "Path to the wordlist. Set to - to use STDIN.")
rootCmd.PersistentFlags().IntP("wordlist-offset", "", 0, "Resume from a given position in the wordlist (defaults to 0)")
rootCmd.PersistentFlags().StringP("output", "o", "", "Output file to write results to (defaults to stdout)")
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output (errors)")
rootCmd.PersistentFlags().BoolP("quiet", "q", false, "Don't print the banner and other noise")
rootCmd.PersistentFlags().BoolP("no-progress", "z", false, "Don't display progress")
rootCmd.PersistentFlags().Bool("no-error", false, "Don't display errors")
rootCmd.PersistentFlags().StringP("pattern", "p", "", "File containing replacement patterns")
rootCmd.PersistentFlags().Bool("no-color", false, "Disable color output")
rootCmd.PersistentFlags().Bool("debug", false, "Enable debug output")
}

79
cli/cmd/s3.go Normal file
View File

@ -0,0 +1,79 @@
package cmd
import (
"fmt"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobusters3"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdS3 *cobra.Command
func runS3(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseS3Options()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobusters3.NewGobusterS3(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobusters3: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseS3Options() (*libgobuster.Options, *gobusters3.OptionsS3, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobusters3.NewOptionsS3()
httpOpts, err := parseBasicHTTPOptions(cmdS3)
if err != nil {
return nil, nil, err
}
pluginOpts.UserAgent = httpOpts.UserAgent
pluginOpts.Proxy = httpOpts.Proxy
pluginOpts.Timeout = httpOpts.Timeout
pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation
pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout
pluginOpts.RetryAttempts = httpOpts.RetryAttempts
pluginOpts.TLSCertificate = httpOpts.TLSCertificate
pluginOpts.MaxFilesToList, err = cmdS3.Flags().GetInt("maxfiles")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for maxfiles: %w", err)
}
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdS3 = &cobra.Command{
Use: "s3",
Short: "Uses aws bucket enumeration mode",
RunE: runS3,
}
addBasicHTTPOptions(cmdS3)
cmdS3.Flags().IntP("maxfiles", "m", 5, "max files to list when listing buckets (only shown in verbose mode)")
cmdS3.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdS3)
}

80
cli/cmd/tftp.go Normal file
View File

@ -0,0 +1,80 @@
package cmd
import (
"fmt"
"log"
"strings"
"time"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobustertftp"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdTFTP *cobra.Command
func runTFTP(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseTFTPOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobustertftp.NewGobusterTFTP(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobustertftp: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseTFTPOptions() (*libgobuster.Options, *gobustertftp.OptionsTFTP, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobustertftp.NewOptionsTFTP()
pluginOpts.Server, err = cmdTFTP.Flags().GetString("server")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for domain: %w", err)
}
if !strings.Contains(pluginOpts.Server, ":") {
pluginOpts.Server = fmt.Sprintf("%s:69", pluginOpts.Server)
}
pluginOpts.Timeout, err = cmdTFTP.Flags().GetDuration("timeout")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for timeout: %w", err)
}
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdTFTP = &cobra.Command{
Use: "tftp",
Short: "Uses TFTP enumeration mode",
RunE: runTFTP,
}
cmdTFTP.Flags().StringP("server", "s", "", "The target TFTP server")
cmdTFTP.Flags().DurationP("timeout", "", time.Second, "TFTP timeout")
if err := cmdTFTP.MarkFlagRequired("server"); err != nil {
log.Fatalf("error on marking flag as required: %v", err)
}
cmdTFTP.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdTFTP)
}

27
cli/cmd/version.go Normal file
View File

@ -0,0 +1,27 @@
package cmd
import (
"fmt"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdVersion *cobra.Command
func runVersion(cmd *cobra.Command, args []string) error {
fmt.Println(libgobuster.VERSION)
return nil
}
// nolint:gochecknoinits
func init() {
cmdVersion = &cobra.Command{
Use: "version",
Short: "shows the current version",
RunE: runVersion,
}
rootCmd.AddCommand(cmdVersion)
}

105
cli/cmd/vhost.go Normal file
View File

@ -0,0 +1,105 @@
package cmd
import (
"fmt"
"log"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobustervhost"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/spf13/cobra"
)
// nolint:gochecknoglobals
var cmdVhost *cobra.Command
func runVhost(cmd *cobra.Command, args []string) error {
globalopts, pluginopts, err := parseVhostOptions()
if err != nil {
return fmt.Errorf("error on parsing arguments: %w", err)
}
plugin, err := gobustervhost.NewGobusterVhost(globalopts, pluginopts)
if err != nil {
return fmt.Errorf("error on creating gobustervhost: %w", err)
}
log := libgobuster.NewLogger(globalopts.Debug)
if _,err := cli.Gobuster(mainContext, globalopts, plugin, log); err != nil {
log.Debugf("%#v", err)
return fmt.Errorf("error on running gobuster: %w", err)
}
return nil
}
func parseVhostOptions() (*libgobuster.Options, *gobustervhost.OptionsVhost, error) {
globalopts, err := parseGlobalOptions()
if err != nil {
return nil, nil, err
}
pluginOpts := gobustervhost.NewOptionsVhost()
httpOpts, err := parseCommonHTTPOptions(cmdVhost)
if err != nil {
return nil, nil, err
}
pluginOpts.Password = httpOpts.Password
pluginOpts.URL = httpOpts.URL
pluginOpts.UserAgent = httpOpts.UserAgent
pluginOpts.Username = httpOpts.Username
pluginOpts.Proxy = httpOpts.Proxy
pluginOpts.Cookies = httpOpts.Cookies
pluginOpts.Timeout = httpOpts.Timeout
pluginOpts.FollowRedirect = httpOpts.FollowRedirect
pluginOpts.NoTLSValidation = httpOpts.NoTLSValidation
pluginOpts.Headers = httpOpts.Headers
pluginOpts.Method = httpOpts.Method
pluginOpts.RetryOnTimeout = httpOpts.RetryOnTimeout
pluginOpts.RetryAttempts = httpOpts.RetryAttempts
pluginOpts.TLSCertificate = httpOpts.TLSCertificate
pluginOpts.NoCanonicalizeHeaders = httpOpts.NoCanonicalizeHeaders
pluginOpts.AppendDomain, err = cmdVhost.Flags().GetBool("append-domain")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for append-domain: %w", err)
}
pluginOpts.ExcludeLength, err = cmdVhost.Flags().GetString("exclude-length")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
ret, err := libgobuster.ParseCommaSeparatedInt(pluginOpts.ExcludeLength)
if err != nil {
return nil, nil, fmt.Errorf("invalid value for exclude-length: %w", err)
}
pluginOpts.ExcludeLengthParsed = ret
pluginOpts.Domain, err = cmdVhost.Flags().GetString("domain")
if err != nil {
return nil, nil, fmt.Errorf("invalid value for domain: %w", err)
}
return globalopts, pluginOpts, nil
}
// nolint:gochecknoinits
func init() {
cmdVhost = &cobra.Command{
Use: "vhost",
Short: "Uses VHOST enumeration mode (you most probably want to use the IP address as the URL parameter)",
RunE: runVhost,
}
if err := addCommonHTTPOptions(cmdVhost); err != nil {
log.Fatalf("%v", err)
}
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.")
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")
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")
cmdVhost.PersistentPreRun = func(cmd *cobra.Command, args []string) {
configureGlobalOptions()
}
rootCmd.AddCommand(cmdVhost)
}

65
cli/cmd/vhost_test.go Normal file
View File

@ -0,0 +1,65 @@
package cmd
import (
"context"
"fmt"
"os"
"testing"
"time"
"git.sual.in/casual/gobuster-lib/cli"
"git.sual.in/casual/gobuster-lib/gobustervhost"
"git.sual.in/casual/gobuster-lib/libgobuster"
)
func BenchmarkVhostMode(b *testing.B) {
h := httpServer(b, "test")
defer h.Close()
pluginopts := gobustervhost.NewOptionsVhost()
pluginopts.URL = h.URL
pluginopts.Timeout = 10 * time.Second
wordlist, err := os.CreateTemp("", "")
if err != nil {
b.Fatalf("could not create tempfile: %v", err)
}
defer os.Remove(wordlist.Name())
for w := 0; w < 1000; w++ {
_, _ = wordlist.WriteString(fmt.Sprintf("%d\n", w))
}
wordlist.Close()
globalopts := libgobuster.Options{
Threads: 10,
Wordlist: wordlist.Name(),
NoProgress: true,
}
ctx := context.Background()
oldStdout := os.Stdout
oldStderr := os.Stderr
defer func(out, err *os.File) { os.Stdout = out; os.Stderr = err }(oldStdout, oldStderr)
devnull, err := os.Open(os.DevNull)
if err != nil {
b.Fatalf("could not get devnull %v", err)
}
defer devnull.Close()
log := libgobuster.NewLogger(false)
// Run the real benchmark
for x := 0; x < b.N; x++ {
os.Stdout = devnull
os.Stderr = devnull
plugin, err := gobustervhost.NewGobusterVhost(&globalopts, pluginopts)
if err != nil {
b.Fatalf("error on creating gobusterdir: %v", err)
}
if err := cli.Gobuster(ctx, &globalopts, plugin, log); err != nil {
b.Fatalf("error on running gobuster: %v", err)
}
os.Stdout = oldStdout
os.Stderr = oldStderr
}
}

7
cli/const.go Normal file
View File

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

7
cli/const_windows.go Normal file
View File

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

232
cli/gobuster.go Normal file
View File

@ -0,0 +1,232 @@
package cli
import (
"context"
"fmt"
"os"
"strings"
"sync"
"time"
"git.sual.in/casual/gobuster-lib/libgobuster"
)
const ruler = "==============================================================="
const cliProgressUpdate = 500 * time.Millisecond
var Output []string
// var NoStdout bool
// resultWorker outputs the results as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) {
defer wg.Done()
var f *os.File
var err error
var s string
if filename != "" {
f, err = os.Create(filename)
if err != nil {
g.Logger.Fatalf("error on creating output file: %v", err)
}
defer f.Close()
}
for r := range g.Progress.ResultChan {
s, err = r.ResultToString()
if err != nil {
g.Logger.Fatal(err)
}
if s != "" {
s = strings.TrimSpace(s)
Output = append(Output,s)
if !g.Opts.NoStdout {
_, _ = fmt.Printf("%s%s\n", TERMINAL_CLEAR_LINE, s)
}
if f != nil {
err = writeToFile(f, s)
if err != nil {
g.Logger.Fatalf("error on writing output file: %v", err)
}
}
}
}
// output <- s
}
// errorWorker outputs the errors as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for e := range g.Progress.ErrorChan {
if !g.Opts.Quiet && !g.Opts.NoError {
g.Logger.Error(e.Error())
g.Logger.Debugf("%#v", e)
}
}
}
// messageWorker outputs messages as they come in. This needs to be a range and should not handle
// the context so the channel always has a receiver and libgobuster will not block.
func messageWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
for msg := range g.Progress.MessageChan {
if !g.Opts.Quiet {
switch msg.Level {
case libgobuster.LevelDebug:
g.Logger.Debug(msg.Message)
case libgobuster.LevelError:
g.Logger.Error(msg.Message)
case libgobuster.LevelInfo:
g.Logger.Info(msg.Message)
default:
panic(fmt.Sprintf("invalid level %d", msg.Level))
}
}
}
}
func printProgress(g *libgobuster.Gobuster) {
if !g.Opts.Quiet && !g.Opts.NoProgress {
requestsIssued := g.Progress.RequestsIssued()
requestsExpected := g.Progress.RequestsExpected()
if g.Opts.Wordlist == "-" {
s := fmt.Sprintf("%sProgress: %d", TERMINAL_CLEAR_LINE, requestsIssued)
_, _ = fmt.Fprint(os.Stderr, s)
// only print status if we already read in the wordlist
} else if requestsExpected > 0 {
s := fmt.Sprintf("%sProgress: %d / %d (%3.2f%%)", TERMINAL_CLEAR_LINE, requestsIssued, requestsExpected, float32(requestsIssued)*100.0/float32(requestsExpected))
_, _ = fmt.Fprint(os.Stderr, s)
}
}
}
// progressWorker outputs the progress every tick. It will stop once cancel() is called
// on the context
func progressWorker(ctx context.Context, g *libgobuster.Gobuster, wg *sync.WaitGroup) {
defer wg.Done()
tick := time.NewTicker(cliProgressUpdate)
for {
select {
case <-tick.C:
printProgress(g)
case <-ctx.Done():
// print the final progress so we end at 100%
printProgress(g)
fmt.Println()
return
}
}
}
func writeToFile(f *os.File, output string) error {
_, err := f.WriteString(fmt.Sprintf("%s\n", output))
if err != nil {
return fmt.Errorf("[!] Unable to write to file %w", err)
}
return nil
}
// Gobuster is the main entry point for the CLI
func Gobuster(ctx context.Context, opts *libgobuster.Options, plugin libgobuster.GobusterPlugin, log libgobuster.Logger) ([]string,error) {
// Sanity checks
if opts == nil {
return nil,fmt.Errorf("please provide valid options")
}
if plugin == nil {
return nil,fmt.Errorf("please provide a valid plugin")
}
ctxCancel, cancel := context.WithCancel(ctx)
defer cancel()
gobuster, err := libgobuster.NewGobuster(opts, plugin, log)
if err != nil {
return nil,err
}
if !opts.Quiet {
log.Println(ruler)
log.Printf("Gobuster v%s\n", libgobuster.VERSION)
log.Println("by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)")
log.Println(ruler)
c, err := gobuster.GetConfigString()
if err != nil {
return nil,fmt.Errorf("error on creating config string: %w", err)
}
log.Println(c)
log.Println(ruler)
gobuster.Logger.Printf("Starting gobuster in %s mode", plugin.Name())
if opts.WordlistOffset > 0 {
gobuster.Logger.Printf("Skipping the first %d elements...", opts.WordlistOffset)
}
log.Println(ruler)
}
// our waitgroup for all goroutines
// this ensures all goroutines are finished
// when we call wg.Wait()
var wg sync.WaitGroup
wg.Add(1)
// output := make(chan string,1)
go resultWorker(gobuster, opts.OutputFilename, &wg)
// go func() {
// fmt.Println(&gobuster.Progress.ResultChan)
// for r := range gobuster.Progress.ResultChan {
// s, _ := r.ResultToString()
// // if err != nil {
// // // return err
// // }
// fmt.Println(s)
// }
// }()
wg.Add(1)
go errorWorker(gobuster, &wg)
wg.Add(1)
go messageWorker(gobuster, &wg)
if !opts.Quiet && !opts.NoProgress {
// if not quiet add a new workgroup entry and start the goroutine
wg.Add(1)
go progressWorker(ctxCancel, gobuster, &wg)
}
err = gobuster.Run(ctxCancel)
// call cancel func so progressWorker will exit (the only goroutine in this
// file using the context) and to free resources
cancel()
// wait for all spun up goroutines to finish (all have to call wg.Done())
wg.Wait()
// Late error checking to finish all threads
if err != nil {
return nil,err
}
if !opts.Quiet {
log.Println(ruler)
gobuster.Logger.Println("Finished")
log.Println(ruler)
}
//
// log.Println(ruler)
// gobuster.Logger.Println("Finished")
// log.Println(ruler)
return Output,nil
}

43
cspell.json Normal file
View File

@ -0,0 +1,43 @@
// cSpell Settings
{
// Version of the setting file. Always 0.2
"version": "0.2",
// language - current active spelling language
"language": "en",
// words - list of words to be always considered correct
"words": [
"libgobuster",
"gobuster",
"gobusters",
"gobusterdir",
"gobusterdns",
"gobusterfuzz",
"gobustervhost",
"gobustergcs",
"vhost",
"vhosts",
"cname",
"uuid",
"dirb",
"wordlist",
"wordlists",
"hashcat",
"Mehlmauer",
"firefart",
"GOPATH",
"nolint",
"unconvert",
"unparam",
"prealloc",
"gochecknoglobals",
"gochecknoinits",
"fatih",
"netip"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
// For example "hte" should be "the"
"flagWords": [
"hte"
]
}

22
go.mod Normal file
View File

@ -0,0 +1,22 @@
module git.sual.in/casual/gobuster-lib
go 1.22.5
require (
git.sual.in/casual/gobuster-lib/libgobuster v0.0.0-20240904201007-8210f5ee7e12
github.com/fatih/color v1.17.0
github.com/google/uuid v1.6.0
github.com/pin/tftp/v3 v3.1.0
github.com/spf13/cobra v1.8.1
golang.org/x/crypto v0.26.0
golang.org/x/term v0.23.0
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.25.0 // indirect
)

37
go.sum Normal file
View File

@ -0,0 +1,37 @@
git.sual.in/casual/gobuster-lib/libgobuster v0.0.0-20240904201007-8210f5ee7e12 h1:+CnxZE3aMK45ZGtoSNv/5J2VSL2igm0/Iytvbxcatog=
git.sual.in/casual/gobuster-lib/libgobuster v0.0.0-20240904201007-8210f5ee7e12/go.mod h1:bkuQXxQgSQ+tO2Qs6PiRKRLXd8g5izddsxraLjHzrD8=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pin/tftp/v3 v3.1.0 h1:rQaxd4pGwcAJnpId8zC+O2NX3B2/NscjDZQaqEjuE7c=
github.com/pin/tftp/v3 v3.1.0/go.mod h1:xwQaN4viYL019tM4i8iecm++5cGxSqen6AJEOEyEI0w=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

392
gobusterdir/gobusterdir.go Normal file
View File

@ -0,0 +1,392 @@
package gobusterdir
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"net/http"
"strings"
"text/tabwriter"
"unicode/utf8"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/google/uuid"
)
// nolint:gochecknoglobals
var (
backupExtensions = []string{"~", ".bak", ".bak2", ".old", ".1"}
backupDotExtensions = []string{".swp"}
)
// ErrWildcard is returned if a wildcard response is found
type ErrWildcard struct {
url string
statusCode int
length int64
}
// Error is the implementation of the error interface
func (e *ErrWildcard) Error() string {
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)
}
// GobusterDir is the main type to implement the interface
type GobusterDir struct {
options *OptionsDir
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
}
// NewGobusterDir creates a new initialized GobusterDir
func NewGobusterDir(globalopts *libgobuster.Options, opts *OptionsDir) (*GobusterDir, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterDir{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterDir) Name() string {
return "directory enumeration"
}
// PreRun is the pre run implementation of gobusterdir
func (d *GobusterDir) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
// add trailing slash
if !strings.HasSuffix(d.options.URL, "/") {
d.options.URL = fmt.Sprintf("%s/", d.options.URL)
}
_, _, _, _, err := d.http.Request(ctx, d.options.URL, libgobuster.RequestOptions{})
if err != nil {
return fmt.Errorf("unable to connect to %s: %w", d.options.URL, err)
}
guid := uuid.New()
url := fmt.Sprintf("%s%s", d.options.URL, guid)
if d.options.UseSlash {
url = fmt.Sprintf("%s/", url)
}
wildcardResp, wildcardLength, _, _, err := d.http.Request(ctx, url, libgobuster.RequestOptions{})
if err != nil {
return err
}
if d.options.ExcludeLengthParsed.Contains(int(wildcardLength)) {
// we are done and ignore the request as the length is excluded
return nil
}
if d.options.StatusCodesBlacklistParsed.Length() > 0 {
if !d.options.StatusCodesBlacklistParsed.Contains(wildcardResp) {
return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
}
} else if d.options.StatusCodesParsed.Length() > 0 {
if d.options.StatusCodesParsed.Contains(wildcardResp) {
return &ErrWildcard{url: url, statusCode: wildcardResp, length: wildcardLength}
}
} else {
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
}
return nil
}
func getBackupFilenames(word string) []string {
ret := make([]string, len(backupExtensions)+len(backupDotExtensions))
i := 0
for _, b := range backupExtensions {
ret[i] = fmt.Sprintf("%s%s", word, b)
i++
}
for _, b := range backupDotExtensions {
ret[i] = fmt.Sprintf(".%s%s", word, b)
i++
}
return ret
}
func (d *GobusterDir) AdditionalWords(word string) []string {
var words []string
// build list of urls to check
// 1: No extension
// 2: With extension
// 3: backupextension
if d.options.DiscoverBackup {
words = append(words, getBackupFilenames(word)...)
}
for ext := range d.options.ExtensionsParsed.Set {
filename := fmt.Sprintf("%s.%s", word, ext)
words = append(words, filename)
if d.options.DiscoverBackup {
words = append(words, getBackupFilenames(filename)...)
}
}
return words
}
// ProcessWord is the process implementation of gobusterdir
func (d *GobusterDir) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
suffix := ""
if d.options.UseSlash {
suffix = "/"
}
entity := fmt.Sprintf("%s%s", word, suffix)
// make sure the url ends with a slash
if !strings.HasSuffix(d.options.URL, "/") {
d.options.URL = fmt.Sprintf("%s/", d.options.URL)
}
// prevent double slashes by removing leading /
if strings.HasPrefix(entity, "/") {
// get size of first rune and trim it
_, i := utf8.DecodeRuneInString(entity)
entity = entity[i:]
}
url := fmt.Sprintf("%s%s", d.options.URL, entity)
tries := 1
if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += d.options.RetryAttempts
}
var statusCode int
var size int64
var header http.Header
for i := 1; i <= tries; i++ {
var err error
statusCode, size, header, _, err = d.http.Request(ctx, url, libgobuster.RequestOptions{})
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
continue
} else if strings.Contains(err.Error(), "invalid control character in URL") {
// put error in error chan so it's printed out and ignore it
// so gobuster will not quit
progress.ErrorChan <- err
continue
} else {
return err
}
}
break
}
if statusCode != 0 {
resultStatus := false
if d.options.StatusCodesBlacklistParsed.Length() > 0 {
if !d.options.StatusCodesBlacklistParsed.Contains(statusCode) {
resultStatus = true
}
} else if d.options.StatusCodesParsed.Length() > 0 {
if d.options.StatusCodesParsed.Contains(statusCode) {
resultStatus = true
}
} else {
return fmt.Errorf("StatusCodes and StatusCodesBlacklist are both not set which should not happen")
}
if (resultStatus && !d.options.ExcludeLengthParsed.Contains(int(size))) || d.globalopts.Verbose {
progress.ResultChan <- Result{
URL: d.options.URL,
Path: entity,
Verbose: d.globalopts.Verbose,
Expanded: d.options.Expanded,
NoStatus: d.options.NoStatus,
HideLength: d.options.HideLength,
Found: resultStatus,
Header: header,
StatusCode: statusCode,
Size: size,
}
}
}
return nil
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDir) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.StatusCodesBlacklistParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Negative Status codes:\t%s\n", o.StatusCodesBlacklistParsed.Stringify()); err != nil {
return "", err
}
} else if o.StatusCodesParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Status codes:\t%s\n", o.StatusCodesParsed.Stringify()); err != nil {
return "", err
}
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.HideLength {
if _, err := fmt.Fprintf(tw, "[+] Show length:\tfalse\n"); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if o.Extensions != "" || o.ExtensionsFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Extensions:\t%s\n", o.ExtensionsParsed.Stringify()); err != nil {
return "", err
}
}
if o.ExtensionsFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Extensions file:\t%s\n", o.ExtensionsFile); err != nil {
return "", err
}
}
if o.UseSlash {
if _, err := fmt.Fprintf(tw, "[+] Add Slash:\ttrue\n"); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
return "", err
}
}
if o.Expanded {
if _, err := fmt.Fprintf(tw, "[+] Expanded:\ttrue\n"); err != nil {
return "", err
}
}
if o.NoStatus {
if _, err := fmt.Fprintf(tw, "[+] No status:\ttrue\n"); err != nil {
return "", err
}
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}

34
gobusterdir/options.go Normal file
View File

@ -0,0 +1,34 @@
package gobusterdir
import (
"git.sual.in/casual/gobuster-lib/libgobuster"
)
// OptionsDir is the struct to hold all options for this plugin
type OptionsDir struct {
libgobuster.HTTPOptions
Extensions string
ExtensionsParsed libgobuster.Set[string]
ExtensionsFile string
StatusCodes string
StatusCodesParsed libgobuster.Set[int]
StatusCodesBlacklist string
StatusCodesBlacklistParsed libgobuster.Set[int]
UseSlash bool
HideLength bool
Expanded bool
NoStatus bool
DiscoverBackup bool
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
}
// NewOptionsDir returns a new initialized OptionsDir
func NewOptionsDir() *OptionsDir {
return &OptionsDir{
StatusCodesParsed: libgobuster.NewSet[int](),
StatusCodesBlacklistParsed: libgobuster.NewSet[int](),
ExtensionsParsed: libgobuster.NewSet[string](),
ExcludeLengthParsed: libgobuster.NewSet[int](),
}
}

View File

@ -0,0 +1,16 @@
package gobusterdir
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptionsDir()
if o.StatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
if o.ExtensionsParsed.Set == nil {
t.Fatal("ExtensionsParsed not initialized")
}
}

100
gobusterdir/result.go Normal file
View File

@ -0,0 +1,100 @@
package gobusterdir
import (
"bytes"
"fmt"
"net/http"
"github.com/fatih/color"
)
var NoStdout bool
var (
white = color.New(color.FgWhite).FprintfFunc()
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
blue = color.New(color.FgBlue).FprintfFunc()
red = color.New(color.FgRed).FprintfFunc()
cyan = color.New(color.FgCyan).FprintfFunc()
)
// Result represents a single result
type Result struct {
URL string
Path string
Verbose bool
Expanded bool
NoStatus bool
HideLength bool
NoStdout bool
Found bool
Header http.Header
StatusCode int
Size int64
}
// ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
// Prefix if we're in verbose mode
if r.Verbose {
if r.Found {
if _, err := fmt.Fprintf(buf, "Found: "); err != nil {
return "", err
}
} else {
if _, err := fmt.Fprintf(buf, "Missed: "); err != nil {
return "", err
}
}
}
if r.Expanded {
if _, err := fmt.Fprintf(buf, "%s", r.URL); err != nil {
return "", err
}
} else {
if _, err := fmt.Fprintf(buf, "/"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(buf, "%-20s", r.Path); err != nil {
return "", err
}
if !r.NoStatus {
color := white
if r.StatusCode == 200 {
color = green
} else if r.StatusCode >= 300 && r.StatusCode < 400 {
color = cyan
} else if r.StatusCode >= 400 && r.StatusCode < 500 {
color = yellow
} else if r.StatusCode >= 500 && r.StatusCode < 600 {
color = red
}
color(buf, " (Status: %d)", r.StatusCode)
}
if !r.HideLength {
if _, err := fmt.Fprintf(buf, " [Size: %d]", r.Size); err != nil {
return "", err
}
}
location := r.Header.Get("Location")
if location != "" {
blue(buf, " [--> %s]", location)
}
if _, err := fmt.Fprintf(buf, "\n"); err != nil {
return "", err
}
s := buf.String()
return s, nil
}

244
gobusterdns/gobusterdns.go Normal file
View File

@ -0,0 +1,244 @@
package gobusterdns
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"net/netip"
"strings"
"text/tabwriter"
"time"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/google/uuid"
)
// ErrWildcard is returned if a wildcard response is found
type ErrWildcard struct {
wildcardIps libgobuster.Set[netip.Addr]
}
// Error is the implementation of the error interface
func (e *ErrWildcard) Error() string {
return fmt.Sprintf("the DNS Server returned the same IP for every domain. IP address(es) returned: %s", e.wildcardIps.Stringify())
}
// GobusterDNS is the main type to implement the interface
type GobusterDNS struct {
resolver *net.Resolver
globalopts *libgobuster.Options
options *OptionsDNS
isWildcard bool
wildcardIps libgobuster.Set[netip.Addr]
}
func newCustomDialer(server string) func(ctx context.Context, network, address string) (net.Conn, error) {
return func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{}
if !strings.Contains(server, ":") {
server = fmt.Sprintf("%s:53", server)
}
return d.DialContext(ctx, "udp", server)
}
}
// NewGobusterDNS creates a new initialized GobusterDNS
func NewGobusterDNS(globalopts *libgobuster.Options, opts *OptionsDNS) (*GobusterDNS, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
resolver := net.DefaultResolver
if opts.Resolver != "" {
resolver = &net.Resolver{
PreferGo: true,
Dial: newCustomDialer(opts.Resolver),
}
}
g := GobusterDNS{
options: opts,
globalopts: globalopts,
wildcardIps: libgobuster.NewSet[netip.Addr](),
resolver: resolver,
}
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterDNS) Name() string {
return "DNS enumeration"
}
// PreRun is the pre run implementation of gobusterdns
func (d *GobusterDNS) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
// Resolve a subdomain that probably shouldn't exist
guid := uuid.New()
wildcardIps, err := d.dnsLookup(ctx, fmt.Sprintf("%s.%s", guid, d.options.Domain))
if err == nil {
d.isWildcard = true
d.wildcardIps.AddRange(wildcardIps)
if !d.options.WildcardForced {
return &ErrWildcard{wildcardIps: d.wildcardIps}
}
}
if !d.globalopts.Quiet {
// Provide a warning if the base domain doesn't resolve (in case of typo)
_, err = d.dnsLookup(ctx, d.options.Domain)
if err != nil {
// Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.yp.to` does!
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelInfo,
Message: fmt.Sprintf("[-] Unable to validate base domain: %s (%v)", d.options.Domain, err),
}
progress.MessageChan <- libgobuster.Message{
Level: libgobuster.LevelDebug,
Message: fmt.Sprintf("%#v", err),
}
}
}
return nil
}
// ProcessWord is the process implementation of gobusterdns
func (d *GobusterDNS) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
subdomain := fmt.Sprintf("%s.%s", word, d.options.Domain)
if !d.options.NoFQDN && !strings.HasSuffix(subdomain, ".") {
// add a . to indicate this is the full domain and we do not want to traverse the search domains on the system
subdomain = fmt.Sprintf("%s.", subdomain)
}
ips, err := d.dnsLookup(ctx, subdomain)
if err == nil {
if !d.isWildcard || !d.wildcardIps.ContainsAny(ips) {
result := Result{
Subdomain: subdomain,
Found: true,
ShowIPs: d.options.ShowIPs,
ShowCNAME: d.options.ShowCNAME,
NoFQDN: d.options.NoFQDN,
}
if d.options.ShowIPs {
result.IPs = ips
} else if d.options.ShowCNAME {
cname, err := d.dnsLookupCname(ctx, subdomain)
if err == nil {
result.CNAME = cname
}
}
progress.ResultChan <- result
}
} else if d.globalopts.Verbose {
progress.ResultChan <- Result{
Subdomain: subdomain,
Found: false,
ShowIPs: d.options.ShowIPs,
ShowCNAME: d.options.ShowCNAME,
}
}
return nil
}
func (d *GobusterDNS) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterDNS) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Domain:\t%s\n", o.Domain); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
if o.Resolver != "" {
if _, err := fmt.Fprintf(tw, "[+] Resolver:\t%s\n", o.Resolver); err != nil {
return "", err
}
}
if o.ShowCNAME {
if _, err := fmt.Fprintf(tw, "[+] Show CNAME:\ttrue\n"); err != nil {
return "", err
}
}
if o.ShowIPs {
if _, err := fmt.Fprintf(tw, "[+] Show IPs:\ttrue\n"); err != nil {
return "", err
}
}
if o.WildcardForced {
if _, err := fmt.Fprintf(tw, "[+] Wildcard forced:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}
func (d *GobusterDNS) dnsLookup(ctx context.Context, domain string) ([]netip.Addr, error) {
ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
defer cancel()
return d.resolver.LookupNetIP(ctx2, "ip", domain)
}
func (d *GobusterDNS) dnsLookupCname(ctx context.Context, domain string) (string, error) {
ctx2, cancel := context.WithTimeout(ctx, d.options.Timeout)
defer cancel()
time.Sleep(time.Second)
return d.resolver.LookupCNAME(ctx2, domain)
}

21
gobusterdns/options.go Normal file
View File

@ -0,0 +1,21 @@
package gobusterdns
import (
"time"
)
// OptionsDNS holds all options for the dns plugin
type OptionsDNS struct {
Domain string
ShowIPs bool
ShowCNAME bool
WildcardForced bool
Resolver string
NoFQDN bool
Timeout time.Duration
}
// NewOptionsDNS returns a new initialized OptionsDNS
func NewOptionsDNS() *OptionsDNS {
return &OptionsDNS{}
}

57
gobusterdns/result.go Normal file
View File

@ -0,0 +1,57 @@
package gobusterdns
import (
"bytes"
"net/netip"
"strings"
"github.com/fatih/color"
)
var (
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
)
// Result represents a single result
type Result struct {
ShowIPs bool
ShowCNAME bool
Found bool
Subdomain string
NoFQDN bool
IPs []netip.Addr
CNAME string
}
// ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
c := green
if !r.NoFQDN {
r.Subdomain = strings.TrimSuffix(r.Subdomain, ".")
}
if r.Found {
c(buf, "Found: ")
} else {
c = yellow
c(buf, "Missed: ")
}
if r.ShowIPs && r.Found {
ips := make([]string, len(r.IPs))
for i := range r.IPs {
ips[i] = r.IPs[i].String()
}
c(buf, "%s [%s]\n", r.Subdomain, strings.Join(ips, ","))
} else if r.ShowCNAME && r.Found && r.CNAME != "" {
c(buf, "%s [%s]\n", r.Subdomain, r.CNAME)
} else {
c(buf, "%s\n", r.Subdomain)
}
s := buf.String()
return s, nil
}

View File

@ -0,0 +1,275 @@
package gobusterfuzz
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"strings"
"text/tabwriter"
"git.sual.in/casual/gobuster-lib/libgobuster"
)
const FuzzKeyword = "FUZZ"
// ErrWildcard is returned if a wildcard response is found
type ErrWildcard struct {
url string
statusCode int
}
// Error is the implementation of the error interface
func (e *ErrWildcard) Error() string {
return fmt.Sprintf("the server returns a status code that matches the provided options for non existing urls. %s => %d", e.url, e.statusCode)
}
// GobusterFuzz is the main type to implement the interface
type GobusterFuzz struct {
options *OptionsFuzz
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
}
// NewGobusterFuzz creates a new initialized GobusterFuzz
func NewGobusterFuzz(globalopts *libgobuster.Options, opts *OptionsFuzz) (*GobusterFuzz, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterFuzz{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterFuzz) Name() string {
return "fuzzing"
}
// PreRun is the pre run implementation of gobusterfuzz
func (d *GobusterFuzz) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
return nil
}
// ProcessWord is the process implementation of gobusterfuzz
func (d *GobusterFuzz) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
url := strings.ReplaceAll(d.options.URL, FuzzKeyword, word)
requestOptions := libgobuster.RequestOptions{}
if len(d.options.Headers) > 0 {
requestOptions.ModifiedHeaders = make([]libgobuster.HTTPHeader, len(d.options.Headers))
for i := range d.options.Headers {
requestOptions.ModifiedHeaders[i] = libgobuster.HTTPHeader{
Name: strings.ReplaceAll(d.options.Headers[i].Name, FuzzKeyword, word),
Value: strings.ReplaceAll(d.options.Headers[i].Value, FuzzKeyword, word),
}
}
}
if d.options.RequestBody != "" {
data := strings.ReplaceAll(d.options.RequestBody, FuzzKeyword, word)
buffer := strings.NewReader(data)
requestOptions.Body = buffer
}
// fuzzing of basic auth
if strings.Contains(d.options.Username, FuzzKeyword) || strings.Contains(d.options.Password, FuzzKeyword) {
requestOptions.UpdatedBasicAuthUsername = strings.ReplaceAll(d.options.Username, FuzzKeyword, word)
requestOptions.UpdatedBasicAuthPassword = strings.ReplaceAll(d.options.Password, FuzzKeyword, word)
}
tries := 1
if d.options.RetryOnTimeout && d.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += d.options.RetryAttempts
}
var statusCode int
var size int64
for i := 1; i <= tries; i++ {
var err error
statusCode, size, _, _, err = d.http.Request(ctx, url, requestOptions)
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
continue
} else if strings.Contains(err.Error(), "invalid control character in URL") {
// put error in error chan so it's printed out and ignore it
// so gobuster will not quit
progress.ErrorChan <- err
continue
} else {
return err
}
}
break
}
if statusCode != 0 {
resultStatus := true
if d.options.ExcludeLengthParsed.Contains(int(size)) {
resultStatus = false
}
if d.options.ExcludedStatusCodesParsed.Length() > 0 {
if d.options.ExcludedStatusCodesParsed.Contains(statusCode) {
resultStatus = false
}
}
if resultStatus || d.globalopts.Verbose {
progress.ResultChan <- Result{
Verbose: d.globalopts.Verbose,
Found: resultStatus,
Path: url,
StatusCode: statusCode,
Size: size,
Word: word,
}
}
}
return nil
}
func (d *GobusterFuzz) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterFuzz) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.ExcludedStatusCodesParsed.Length() > 0 {
if _, err := fmt.Fprintf(tw, "[+] Excluded Status codes:\t%s\n", o.ExcludedStatusCodesParsed.Stringify()); err != nil {
return "", err
}
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", d.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if o.FollowRedirect {
if _, err := fmt.Fprintf(tw, "[+] Follow Redirect:\ttrue\n"); err != nil {
return "", err
}
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}

23
gobusterfuzz/options.go Normal file
View File

@ -0,0 +1,23 @@
package gobusterfuzz
import (
"git.sual.in/casual/gobuster-lib/libgobuster"
)
// OptionsFuzz is the struct to hold all options for this plugin
type OptionsFuzz struct {
libgobuster.HTTPOptions
ExcludedStatusCodes string
ExcludedStatusCodesParsed libgobuster.Set[int]
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
RequestBody string
}
// NewOptionsFuzz returns a new initialized OptionsFuzz
func NewOptionsFuzz() *OptionsFuzz {
return &OptionsFuzz{
ExcludedStatusCodesParsed: libgobuster.NewSet[int](),
ExcludeLengthParsed: libgobuster.NewSet[int](),
}
}

View File

@ -0,0 +1,12 @@
package gobusterfuzz
import "testing"
func TestNewOptions(t *testing.T) {
t.Parallel()
o := NewOptionsFuzz()
if o.ExcludedStatusCodesParsed.Set == nil {
t.Fatal("StatusCodesParsed not initialized")
}
}

47
gobusterfuzz/result.go Normal file
View File

@ -0,0 +1,47 @@
package gobusterfuzz
import (
"bytes"
"github.com/fatih/color"
)
var (
yellow = color.New(color.FgYellow).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
)
// Result represents a single result
type Result struct {
Word string
Verbose bool
Found bool
Path string
StatusCode int
Size int64
}
// ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
c := green
// Prefix if we're in verbose mode
if r.Verbose {
if r.Found {
c(buf, "Found: ")
} else {
c = yellow
c(buf, "Missed: ")
}
} else if r.Found {
c(buf, "Found: ")
}
c(buf, "[Status=%d] [Length=%d] [Word=%s] %s", r.StatusCode, r.Size, r.Word, r.Path)
c(buf, "\n")
s := buf.String()
return s, nil
}

266
gobustergcs/gobustersgcs.go Normal file
View File

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

16
gobustergcs/options.go Normal file
View File

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

35
gobustergcs/result.go Normal file
View File

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

25
gobustergcs/types.go Normal file
View File

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

260
gobusters3/gobusters3.go Normal file
View File

@ -0,0 +1,260 @@
package gobusters3
import (
"bufio"
"bytes"
"context"
"encoding/xml"
"fmt"
"net"
"net/http"
"regexp"
"strings"
"text/tabwriter"
"git.sual.in/casual/gobuster-lib/libgobuster"
)
// GobusterS3 is the main type to implement the interface
type GobusterS3 struct {
options *OptionsS3
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
bucketRegex *regexp.Regexp
}
// NewGobusterS3 creates a new initialized GobusterS3
func NewGobusterS3(globalopts *libgobuster.Options, opts *OptionsS3) (*GobusterS3, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterS3{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
// needed so we can list bucket contents
FollowRedirect: true,
}
h, err := libgobuster.NewHTTPClient(&httpOpts)
if err != nil {
return nil, err
}
g.http = h
g.bucketRegex = regexp.MustCompile(`^[a-z0-9\-.]{3,63}$`)
return &g, nil
}
// Name should return the name of the plugin
func (s *GobusterS3) Name() string {
return "S3 bucket enumeration"
}
// PreRun is the pre run implementation of GobusterS3
func (s *GobusterS3) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
return nil
}
// ProcessWord is the process implementation of GobusterS3
func (s *GobusterS3) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
// only check for valid bucket names
if !s.isValidBucketName(word) {
return nil
}
bucketURL := fmt.Sprintf("https://%s.s3.amazonaws.com/?max-keys=%d", word, s.options.MaxFilesToList)
tries := 1
if s.options.RetryOnTimeout && s.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += s.options.RetryAttempts
}
var statusCode int
var body []byte
for i := 1; i <= tries; i++ {
var err error
statusCode, _, _, body, err = s.http.Request(ctx, bucketURL, libgobuster.RequestOptions{ReturnBody: true})
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
continue
} else if strings.Contains(err.Error(), "invalid control character in URL") {
// put error in error chan so it's printed out and ignore it
// so gobuster will not quit
progress.ErrorChan <- err
continue
} else {
return err
}
}
break
}
if statusCode == 0 || body == nil {
return nil
}
// looks like 404 and 400 are the only negative status codes
found := false
switch statusCode {
case http.StatusBadRequest:
case http.StatusNotFound:
found = false
case http.StatusOK:
// listing enabled
found = true
// parse xml
default:
// default to found as we use negative status codes
found = true
}
// nothing found, bail out
// may add the result later if we want to enable verbose output
if !found {
return nil
}
extraStr := ""
if s.globalopts.Verbose {
// get status
if bytes.Contains(body, []byte("<Error>")) {
awsError := AWSError{}
err := xml.Unmarshal(body, &awsError)
if err != nil {
return fmt.Errorf("could not parse error xml: %w", err)
}
// https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html#ErrorCodeList
extraStr = fmt.Sprintf("Error: %s (%s)", awsError.Message, awsError.Code)
} else if bytes.Contains(body, []byte("<ListBucketResult ")) {
// bucket listing enabled
awsListing := AWSListing{}
err := xml.Unmarshal(body, &awsListing)
if err != nil {
return fmt.Errorf("could not parse result xml: %w", err)
}
extraStr = "Bucket Listing enabled: "
for _, x := range awsListing.Contents {
extraStr += fmt.Sprintf("%s (%db), ", x.Key, x.Size)
}
extraStr = strings.TrimRight(extraStr, ", ")
}
}
progress.ResultChan <- Result{
Found: found,
BucketName: word,
Status: extraStr,
}
return nil
}
func (d *GobusterS3) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (s *GobusterS3) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := s.options
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", s.globalopts.Threads); err != nil {
return "", err
}
if s.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", s.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if s.globalopts.Wordlist != "-" {
wordlist = s.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if s.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", s.globalopts.PatternFile, len(s.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if s.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Maximum files to list:\t%d\n", o.MaxFilesToList); err != nil {
return "", err
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}
// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
func (s *GobusterS3) isValidBucketName(bucketName string) bool {
if !s.bucketRegex.MatchString(bucketName) {
return false
}
if strings.HasSuffix(bucketName, "-") ||
strings.HasPrefix(bucketName, ".") ||
strings.HasPrefix(bucketName, "-") ||
strings.Contains(bucketName, "..") ||
strings.Contains(bucketName, ".-") ||
strings.Contains(bucketName, "-.") {
return false
}
return true
}

16
gobusters3/options.go Normal file
View File

@ -0,0 +1,16 @@
package gobusters3
import (
"git.sual.in/casual/gobuster-lib/libgobuster"
)
// OptionsS3 is the struct to hold all options for this plugin
type OptionsS3 struct {
libgobuster.BasicHTTPOptions
MaxFilesToList int
}
// NewOptionsS3 returns a new initialized OptionsS3
func NewOptionsS3() *OptionsS3 {
return &OptionsS3{}
}

35
gobusters3/result.go Normal file
View File

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

24
gobusters3/types.go Normal file
View File

@ -0,0 +1,24 @@
package gobusters3
import "encoding/xml"
// AWSError represents a returned error from AWS
type AWSError struct {
XMLName xml.Name `xml:"Error"`
Code string `xml:"Code"`
Message string `xml:"Message"`
RequestID string `xml:"RequestId"`
HostID string `xml:"HostId"`
}
// AWSListing contains only a subset of returned properties
type AWSListing struct {
XMLName xml.Name `xml:"ListBucketResult"`
Name string `xml:"Name"`
IsTruncated string `xml:"IsTruncated"`
Contents []struct {
Key string `xml:"Key"`
LastModified string `xml:"LastModified"`
Size int `xml:"Size"`
} `xml:"Contents"`
}

View File

@ -0,0 +1,142 @@
package gobustertftp
import (
"bufio"
"bytes"
"context"
"fmt"
"strings"
"text/tabwriter"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/pin/tftp/v3"
)
// GobusterTFTP is the main type to implement the interface
type GobusterTFTP struct {
globalopts *libgobuster.Options
options *OptionsTFTP
}
// NewGobusterTFTP creates a new initialized NewGobusterTFTP
func NewGobusterTFTP(globalopts *libgobuster.Options, opts *OptionsTFTP) (*GobusterTFTP, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterTFTP{
options: opts,
globalopts: globalopts,
}
return &g, nil
}
// Name should return the name of the plugin
func (d *GobusterTFTP) Name() string {
return "TFTP enumeration"
}
// PreRun is the pre run implementation of gobustertftp
func (d *GobusterTFTP) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
_, err := tftp.NewClient(d.options.Server)
if err != nil {
return err
}
return nil
}
// ProcessWord is the process implementation of gobustertftp
func (d *GobusterTFTP) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
c, err := tftp.NewClient(d.options.Server)
if err != nil {
return err
}
c.SetTimeout(d.options.Timeout)
wt, err := c.Receive(word, "octet")
if err != nil {
// file not found
if d.globalopts.Verbose {
progress.ResultChan <- Result{
Filename: word,
Found: false,
ErrorMessage: err.Error(),
}
}
return nil
}
result := Result{
Filename: word,
Found: true,
}
if n, ok := wt.(tftp.IncomingTransfer).Size(); ok {
result.Size = n
}
progress.ResultChan <- result
return nil
}
func (d *GobusterTFTP) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (d *GobusterTFTP) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := d.options
if _, err := fmt.Fprintf(tw, "[+] Server:\t%s\n", o.Server); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", d.globalopts.Threads); err != nil {
return "", err
}
if d.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", d.globalopts.Delay); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
wordlist := "stdin (pipe)"
if d.globalopts.Wordlist != "-" {
wordlist = d.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if d.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", d.globalopts.PatternFile, len(d.globalopts.Patterns)); err != nil {
return "", err
}
}
if d.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}

16
gobustertftp/options.go Normal file
View File

@ -0,0 +1,16 @@
package gobustertftp
import (
"time"
)
// OptionsTFTP holds all options for the tftp plugin
type OptionsTFTP struct {
Server string
Timeout time.Duration
}
// NewOptionsTFTP returns a new initialized OptionsTFTP
func NewOptionsTFTP() *OptionsTFTP {
return &OptionsTFTP{}
}

46
gobustertftp/result.go Normal file
View File

@ -0,0 +1,46 @@
package gobustertftp
import (
"bytes"
"fmt"
"github.com/fatih/color"
)
var (
red = color.New(color.FgRed).FprintfFunc()
green = color.New(color.FgGreen).FprintfFunc()
)
// Result represents a single result
type Result struct {
Filename string
Found bool
Size int64
ErrorMessage string
}
// ResultToString converts the Result to it's textual representation
func (r Result) ResultToString() (string, error) {
buf := &bytes.Buffer{}
if r.Found {
green(buf, "Found: ")
if _, err := fmt.Fprintf(buf, "%s", r.Filename); err != nil {
return "", err
}
if r.Size > 0 {
if _, err := fmt.Fprintf(buf, " [%d]", r.Size); err != nil {
return "", err
}
}
} else {
red(buf, "Missed: ")
if _, err := fmt.Fprintf(buf, "%s - %s", r.Filename, r.ErrorMessage); err != nil {
return "", err
}
}
s := buf.String()
return s, nil
}

View File

@ -0,0 +1,265 @@
package gobustervhost
import (
"bufio"
"bytes"
"context"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"text/tabwriter"
"git.sual.in/casual/gobuster-lib/libgobuster"
"github.com/google/uuid"
)
// GobusterVhost is the main type to implement the interface
type GobusterVhost struct {
options *OptionsVhost
globalopts *libgobuster.Options
http *libgobuster.HTTPClient
domain string
normalBody []byte
abnormalBody []byte
}
// NewGobusterVhost creates a new initialized GobusterDir
func NewGobusterVhost(globalopts *libgobuster.Options, opts *OptionsVhost) (*GobusterVhost, error) {
if globalopts == nil {
return nil, fmt.Errorf("please provide valid global options")
}
if opts == nil {
return nil, fmt.Errorf("please provide valid plugin options")
}
g := GobusterVhost{
options: opts,
globalopts: globalopts,
}
basicOptions := libgobuster.BasicHTTPOptions{
Proxy: opts.Proxy,
Timeout: opts.Timeout,
UserAgent: opts.UserAgent,
NoTLSValidation: opts.NoTLSValidation,
RetryOnTimeout: opts.RetryOnTimeout,
RetryAttempts: opts.RetryAttempts,
TLSCertificate: opts.TLSCertificate,
}
httpOpts := libgobuster.HTTPOptions{
BasicHTTPOptions: basicOptions,
FollowRedirect: opts.FollowRedirect,
Username: opts.Username,
Password: opts.Password,
Headers: opts.Headers,
NoCanonicalizeHeaders: opts.NoCanonicalizeHeaders,
Cookies: opts.Cookies,
Method: opts.Method,
}
h, err := libgobuster.NewHTTPClient(&httpOpts)
if err != nil {
return nil, err
}
g.http = h
return &g, nil
}
// Name should return the name of the plugin
func (v *GobusterVhost) Name() string {
return "VHOST enumeration"
}
// PreRun is the pre run implementation of gobusterdir
func (v *GobusterVhost) PreRun(ctx context.Context, progress *libgobuster.Progress) error {
// add trailing slash
if !strings.HasSuffix(v.options.URL, "/") {
v.options.URL = fmt.Sprintf("%s/", v.options.URL)
}
urlParsed, err := url.Parse(v.options.URL)
if err != nil {
return fmt.Errorf("invalid url %s: %w", v.options.URL, err)
}
if v.options.Domain != "" {
v.domain = v.options.Domain
} else {
v.domain = urlParsed.Host
}
// request default vhost for normalBody
_, _, _, body, err := v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{ReturnBody: true})
if err != nil {
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
}
v.normalBody = body
// request non existent vhost for abnormalBody
subdomain := fmt.Sprintf("%s.%s", uuid.New(), v.domain)
_, _, _, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
if err != nil {
return fmt.Errorf("unable to connect to %s: %w", v.options.URL, err)
}
v.abnormalBody = body
return nil
}
// ProcessWord is the process implementation of gobusterdir
func (v *GobusterVhost) ProcessWord(ctx context.Context, word string, progress *libgobuster.Progress) error {
var subdomain string
if v.options.AppendDomain {
subdomain = fmt.Sprintf("%s.%s", word, v.domain)
} else {
// wordlist needs to include full domains
subdomain = word
}
tries := 1
if v.options.RetryOnTimeout && v.options.RetryAttempts > 0 {
// add it so it will be the overall max requests
tries += v.options.RetryAttempts
}
var statusCode int
var size int64
var header http.Header
var body []byte
for i := 1; i <= tries; i++ {
var err error
statusCode, size, header, body, err = v.http.Request(ctx, v.options.URL, libgobuster.RequestOptions{Host: subdomain, ReturnBody: true})
if err != nil {
// check if it's a timeout and if we should try again and try again
// otherwise the timeout error is raised
if netErr, ok := err.(net.Error); ok && netErr.Timeout() && i != tries {
continue
} else if strings.Contains(err.Error(), "invalid control character in URL") {
// put error in error chan so it's printed out and ignore it
// so gobuster will not quit
progress.ErrorChan <- err
continue
} else {
return err
}
}
break
}
// subdomain must not match default vhost and non existent vhost
// or verbose mode is enabled
found := body != nil && !bytes.Equal(body, v.normalBody) && !bytes.Equal(body, v.abnormalBody)
if (found && !v.options.ExcludeLengthParsed.Contains(int(size))) || v.globalopts.Verbose {
resultStatus := false
if found {
resultStatus = true
}
progress.ResultChan <- Result{
Found: resultStatus,
Vhost: subdomain,
StatusCode: statusCode,
Size: size,
Header: header,
}
}
return nil
}
func (v *GobusterVhost) AdditionalWords(word string) []string {
return []string{}
}
// GetConfigString returns the string representation of the current config
func (v *GobusterVhost) GetConfigString() (string, error) {
var buffer bytes.Buffer
bw := bufio.NewWriter(&buffer)
tw := tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)
o := v.options
if _, err := fmt.Fprintf(tw, "[+] Url:\t%s\n", o.URL); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Method:\t%s\n", o.Method); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Threads:\t%d\n", v.globalopts.Threads); err != nil {
return "", err
}
if v.globalopts.Delay > 0 {
if _, err := fmt.Fprintf(tw, "[+] Delay:\t%s\n", v.globalopts.Delay); err != nil {
return "", err
}
}
wordlist := "stdin (pipe)"
if v.globalopts.Wordlist != "-" {
wordlist = v.globalopts.Wordlist
}
if _, err := fmt.Fprintf(tw, "[+] Wordlist:\t%s\n", wordlist); err != nil {
return "", err
}
if v.globalopts.PatternFile != "" {
if _, err := fmt.Fprintf(tw, "[+] Patterns:\t%s (%d entries)\n", v.globalopts.PatternFile, len(v.globalopts.Patterns)); err != nil {
return "", err
}
}
if o.Proxy != "" {
if _, err := fmt.Fprintf(tw, "[+] Proxy:\t%s\n", o.Proxy); err != nil {
return "", err
}
}
if o.Cookies != "" {
if _, err := fmt.Fprintf(tw, "[+] Cookies:\t%s\n", o.Cookies); err != nil {
return "", err
}
}
if o.UserAgent != "" {
if _, err := fmt.Fprintf(tw, "[+] User Agent:\t%s\n", o.UserAgent); err != nil {
return "", err
}
}
if o.Username != "" {
if _, err := fmt.Fprintf(tw, "[+] Auth User:\t%s\n", o.Username); err != nil {
return "", err
}
}
if v.globalopts.Verbose {
if _, err := fmt.Fprintf(tw, "[+] Verbose:\ttrue\n"); err != nil {
return "", err
}
}
if _, err := fmt.Fprintf(tw, "[+] Timeout:\t%s\n", o.Timeout.String()); err != nil {
return "", err
}
if _, err := fmt.Fprintf(tw, "[+] Append Domain:\t%t\n", v.options.AppendDomain); err != nil {
return "", err
}
if len(o.ExcludeLength) > 0 {
if _, err := fmt.Fprintf(tw, "[+] Exclude Length:\t%s\n", v.options.ExcludeLengthParsed.Stringify()); err != nil {
return "", err
}
}
if err := tw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
if err := bw.Flush(); err != nil {
return "", fmt.Errorf("error on tostring: %w", err)
}
return strings.TrimSpace(buffer.String()), nil
}

21
gobustervhost/options.go Normal file
View File

@ -0,0 +1,21 @@
package gobustervhost
import (
"git.sual.in/casual/gobuster-lib/libgobuster"
)
// OptionsVhost is the struct to hold all options for this plugin
type OptionsVhost struct {
libgobuster.HTTPOptions
AppendDomain bool
ExcludeLength string
ExcludeLengthParsed libgobuster.Set[int]
Domain string
}
// NewOptionsVhost returns a new initialized OptionsVhost
func NewOptionsVhost() *OptionsVhost {
return &OptionsVhost{
ExcludeLengthParsed: libgobuster.NewSet[int](),
}
}

55
gobustervhost/result.go Normal file
View File

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

11
libgobuster/go.mod Normal file
View File

@ -0,0 +1,11 @@
module git.sual.in/casual/gobuster-lib/libgobuster
go 1.22.5
require github.com/fatih/color v1.17.0
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.18.0 // indirect
)

11
libgobuster/go.sum Normal file
View File

@ -0,0 +1,11 @@
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

198
libgobuster/helpers.go Normal file
View File

@ -0,0 +1,198 @@
package libgobuster
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"regexp"
"strconv"
"strings"
)
// Set is a set of Ts
type Set[T comparable] struct {
Set map[T]bool
}
// NewSSet creates a new initialized Set
func NewSet[T comparable]() Set[T] {
return Set[T]{Set: map[T]bool{}}
}
// Add an element to a set
func (set *Set[T]) Add(s T) bool {
_, found := set.Set[s]
set.Set[s] = true
return !found
}
// AddRange adds a list of elements to a set
func (set *Set[T]) AddRange(ss []T) {
for _, s := range ss {
set.Set[s] = true
}
}
// Contains tests if an element is in a set
func (set *Set[T]) Contains(s T) bool {
_, found := set.Set[s]
return found
}
// ContainsAny checks if any of the elements exist
func (set *Set[T]) ContainsAny(ss []T) bool {
for _, s := range ss {
if set.Set[s] {
return true
}
}
return false
}
// Length returns the length of the Set
func (set *Set[T]) Length() int {
return len(set.Set)
}
// Stringify the set
func (set *Set[T]) Stringify() string {
values := make([]string, len(set.Set))
i := 0
for s := range set.Set {
values[i] = fmt.Sprint(s)
i++
}
return strings.Join(values, ",")
}
func lineCounter(r io.Reader) (int, error) {
buf := make([]byte, 32*1024)
count := 1
lineSep := []byte{'\n'}
for {
c, err := r.Read(buf)
count += bytes.Count(buf[:c], lineSep)
switch {
case errors.Is(err, io.EOF):
return count, nil
case err != nil:
return count, err
}
}
}
// DefaultUserAgent returns the default user agent to use in HTTP requests
func DefaultUserAgent() string {
return fmt.Sprintf("gobuster/%s", VERSION)
}
// ParseExtensions parses the extensions provided as a comma separated list
func ParseExtensions(extensions string) (Set[string], error) {
ret := NewSet[string]()
if extensions == "" {
return ret, nil
}
for _, e := range strings.Split(extensions, ",") {
e = strings.TrimSpace(e)
// remove leading . from extensions
ret.Add(strings.TrimPrefix(e, "."))
}
return ret, nil
}
func ParseExtensionsFile(file string) ([]string, error) {
var ret []string
stream, err := os.Open(file)
if err != nil {
return ret, err
}
defer stream.Close()
scanner := bufio.NewScanner(stream)
for scanner.Scan() {
e := scanner.Text()
e = strings.TrimSpace(e)
// remove leading . from extensions
ret = append(ret, (strings.TrimPrefix(e, ".")))
}
if err := scanner.Err(); err != nil {
return nil, err
}
return ret, nil
}
// ParseCommaSeparatedInt parses the status codes provided as a comma separated list
func ParseCommaSeparatedInt(inputString string) (Set[int], error) {
ret := NewSet[int]()
if inputString == "" {
return ret, nil
}
for _, part := range strings.Split(inputString, ",") {
part = strings.TrimSpace(part)
// check for range
if strings.Contains(part, "-") {
re := regexp.MustCompile(`^\s*(\d+)\s*-\s*(\d+)\s*$`)
match := re.FindStringSubmatch(part)
if match == nil || len(match) != 3 {
return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
}
from := strings.TrimSpace(match[1])
to := strings.TrimSpace(match[2])
fromI, err := strconv.Atoi(from)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, from)
}
toI, err := strconv.Atoi(to)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string in range %s: %s", part, to)
}
if toI < fromI {
return NewSet[int](), fmt.Errorf("invalid range given: %s", part)
}
for i := fromI; i <= toI; i++ {
ret.Add(i)
}
} else {
i, err := strconv.Atoi(part)
if err != nil {
return NewSet[int](), fmt.Errorf("invalid string given: %s", part)
}
ret.Add(i)
}
}
return ret, nil
}
// SliceContains checks if an integer slice contains a specific value
func SliceContains(s []int, e int) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// JoinIntSlice joins an int slice by ,
func JoinIntSlice(s []int) string {
valuesText := make([]string, len(s))
for i, number := range s {
text := strconv.Itoa(number)
valuesText[i] = text
}
result := strings.Join(valuesText, ",")
return result
}

323
libgobuster/helpers_test.go Normal file
View File

@ -0,0 +1,323 @@
package libgobuster
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
"testing/iotest"
)
func TestNewSet(t *testing.T) {
t.Parallel()
if NewSet[string]().Set == nil {
t.Fatal("NewSet[string] returned nil Set")
}
if NewSet[int]().Set == nil {
t.Fatal("NewSet[int] returned nil Set")
}
}
func TestSetAdd(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.Add("test")
if len(x.Set) != 1 {
t.Fatalf("Unexpected string size. Should have 1 Got %v", len(x.Set))
}
y := NewSet[int]()
y.Add(1)
if len(y.Set) != 1 {
t.Fatalf("Unexpected int size. Should have 1 Got %v", len(y.Set))
}
}
func TestSetAddDouble(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.Add("test")
x.Add("test")
if len(x.Set) != 1 {
t.Fatalf("Unexpected string size. Should be 1 (unique) Got %v", len(x.Set))
}
y := NewSet[int]()
y.Add(1)
y.Add(1)
if len(y.Set) != 1 {
t.Fatalf("Unexpected int size. Should be 1 (unique) Got %v", len(y.Set))
}
}
func TestSetAddRange(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.AddRange([]string{"string1", "string2"})
if len(x.Set) != 2 {
t.Fatalf("Unexpected string size. Should have 2 Got %v", len(x.Set))
}
y := NewSet[int]()
y.AddRange([]int{1, 2})
if len(y.Set) != 2 {
t.Fatalf("Unexpected int size. Should have 2 Got %v", len(y.Set))
}
}
func TestSetAddRangeDouble(t *testing.T) {
t.Parallel()
x := NewSet[string]()
x.AddRange([]string{"string1", "string2", "string1", "string2"})
if len(x.Set) != 2 {
t.Fatalf("Unexpected string size. Should be 2 (unique) Got %v", len(x.Set))
}
y := NewSet[int]()
y.AddRange([]int{1, 2, 1, 2})
if len(y.Set) != 2 {
t.Fatalf("Unexpected int size. Should be 2 (unique) Got %v", len(y.Set))
}
}
func TestSetContains(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
for _, i := range v {
if !x.Contains(i) {
t.Fatalf("Did not find value %s in array. %v", i, x.Set)
}
}
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
for _, i := range v2 {
if !y.Contains(i) {
t.Fatalf("Did not find value %d in array. %v", i, y.Set)
}
}
}
func TestSetContainsAny(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
if !x.ContainsAny(v) {
t.Fatalf("Did not find any")
}
// test not found
if x.ContainsAny([]string{"mmmm", "nnnnn"}) {
t.Fatal("Found unexpected values")
}
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
if !y.ContainsAny(v2) {
t.Fatalf("Did not find any")
}
// test not found
if y.ContainsAny([]int{9235, 2398532}) {
t.Fatal("Found unexpected values")
}
}
func TestSetStringify(t *testing.T) {
t.Parallel()
x := NewSet[string]()
v := []string{"string1", "string2", "1234", "5678"}
x.AddRange(v)
z := x.Stringify()
// order is random
for _, i := range v {
if !strings.Contains(z, i) {
t.Fatalf("Did not find value %q in %q", i, z)
}
}
y := NewSet[int]()
v2 := []int{1, 2312, 123121, 999, -99}
y.AddRange(v2)
z = y.Stringify()
// order is random
for _, i := range v2 {
if !strings.Contains(z, fmt.Sprint(i)) {
t.Fatalf("Did not find value %q in %q", i, z)
}
}
}
func TestLineCounter(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
s string
expected int
}{
{"One Line", "test", 1},
{"3 Lines", "TestString\nTest\n1234", 3},
{"Trailing newline", "TestString\nTest\n1234\n", 4},
{"3 Lines cr lf", "TestString\r\nTest\r\n1234", 3},
{"Empty", "", 1},
}
for _, x := range tt {
x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
r := strings.NewReader(x.s)
l, err := lineCounter(r)
if err != nil {
t.Fatalf("Got error: %v", err)
}
if l != x.expected {
t.Fatalf("wrong line count! Got %d expected %d", l, x.expected)
}
})
}
}
func TestLineCounterError(t *testing.T) {
t.Parallel()
r := iotest.TimeoutReader(strings.NewReader("test"))
_, err := lineCounter(r)
if !errors.Is(err, iotest.ErrTimeout) {
t.Fatalf("Got wrong error! %v", err)
}
}
func TestParseExtensions(t *testing.T) {
t.Parallel()
var tt = []struct {
testName string
extensions string
expectedExtensions Set[string]
expectedError string
}{
{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", NewSet[string](), "invalid extension string provided"},
}
for _, x := range tt {
x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
t.Run(x.testName, func(t *testing.T) {
t.Parallel()
ret, err := ParseExtensions(x.extensions)
if x.expectedError != "" {
if err != nil && err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(x.expectedExtensions, ret) {
t.Fatalf("Expected %v but got %v", x.expectedExtensions, ret)
}
})
}
}
func TestParseCommaSeparatedInt(t *testing.T) {
t.Parallel()
var tt = []struct {
stringCodes string
expectedCodes []int
expectedError string
}{
{"200,100,202", []int{200, 100, 202}, ""},
{"200, 100 , 202", []int{200, 100, 202}, ""},
{"200, 100, 202, 100", []int{200, 100, 202}, ""},
{"200,AAA", []int{}, "invalid string given: AAA"},
{"2000000000000000000000000000000", []int{}, "invalid string given: 2000000000000000000000000000000"},
{"", []int{}, "invalid string provided"},
{"200-205", []int{200, 201, 202, 203, 204, 205}, ""},
{"200-202,203-205", []int{200, 201, 202, 203, 204, 205}, ""},
{"200-202,204-205", []int{200, 201, 202, 204, 205}, ""},
{"200-202,205", []int{200, 201, 202, 205}, ""},
{"205,200,100-101,103-105", []int{100, 101, 103, 104, 105, 200, 205}, ""},
{"200-200", []int{200}, ""},
{"200 - 202", []int{200, 201, 202}, ""},
{"200 -202", []int{200, 201, 202}, ""},
{"200- 202", []int{200, 201, 202}, ""},
{"200 - 202", []int{200, 201, 202}, ""},
{"230-200", []int{}, "invalid range given: 230-200"},
{"A-200", []int{}, "invalid range given: A-200"},
{"230-A", []int{}, "invalid range given: 230-A"},
{"200,202-205,A,206-210", []int{}, "invalid string given: A"},
{"200,202-205,A-1,206-210", []int{}, "invalid range given: A-1"},
{"200,202-205,1-A,206-210", []int{}, "invalid range given: 1-A"},
}
for _, x := range tt {
x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
t.Run(x.stringCodes, func(t *testing.T) {
t.Parallel()
want := NewSet[int]()
want.AddRange(x.expectedCodes)
ret, err := ParseCommaSeparatedInt(x.stringCodes)
if x.expectedError != "" {
if err != nil && err.Error() != x.expectedError {
t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error())
}
} else if !reflect.DeepEqual(want, ret) {
t.Fatalf("Expected %v but got %v", want, ret)
}
})
}
}
func BenchmarkParseExtensions(b *testing.B) {
var tt = []struct {
testName string
extensions string
expectedExtensions Set[string]
expectedError string
}{
{"Valid extensions", "php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Spaces", "php, asp , txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Double extensions", "php,asp,txt,php,asp,txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Leading dot", ".php,asp,.txt", Set[string]{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""},
{"Empty string", "", NewSet[string](), "invalid extension string provided"},
}
for _, x := range tt {
x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
b.Run(x.testName, func(b2 *testing.B) {
for y := 0; y < b2.N; y++ {
_, _ = ParseExtensions(x.extensions)
}
})
}
}
func BenchmarkParseCommaSeparatedInt(b *testing.B) {
var tt = []struct {
testName string
stringCodes string
expectedCodes Set[int]
expectedError string
}{
{"Valid codes", "200,100,202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Spaces", "200, 100 , 202", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Double codes", "200, 100, 202, 100", Set[int]{Set: map[int]bool{100: true, 200: true, 202: true}}, ""},
{"Invalid code", "200,AAA", NewSet[int](), "invalid string given: AAA"},
{"Invalid integer", "2000000000000000000000000000000", NewSet[int](), "invalid string given: 2000000000000000000000000000000"},
{"Empty string", "", NewSet[int](), "invalid string string provided"},
}
for _, x := range tt {
x := x // NOTE: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables
b.Run(x.testName, func(b2 *testing.B) {
for y := 0; y < b2.N; y++ {
_, _ = ParseCommaSeparatedInt(x.stringCodes)
}
})
}
}

210
libgobuster/http.go Normal file
View File

@ -0,0 +1,210 @@
package libgobuster
import (
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
// HTTPHeader holds a single key value pair of a HTTP header
type HTTPHeader struct {
Name string
Value string
}
// HTTPClient represents a http object
type HTTPClient struct {
client *http.Client
userAgent string
defaultUserAgent string
username string
password string
headers []HTTPHeader
noCanonicalizeHeaders bool
cookies string
method string
host string
}
// RequestOptions is used to pass options to a single individual request
type RequestOptions struct {
Host string
Body io.Reader
ReturnBody bool
ModifiedHeaders []HTTPHeader
UpdatedBasicAuthUsername string
UpdatedBasicAuthPassword string
}
// NewHTTPClient returns a new HTTPClient
func NewHTTPClient(opt *HTTPOptions) (*HTTPClient, error) {
var proxyURLFunc func(*http.Request) (*url.URL, error)
var client HTTPClient
proxyURLFunc = http.ProxyFromEnvironment
if opt == nil {
return nil, fmt.Errorf("options is nil")
}
if opt.Proxy != "" {
proxyURL, err := url.Parse(opt.Proxy)
if err != nil {
return nil, fmt.Errorf("proxy URL is invalid (%w)", err)
}
proxyURLFunc = http.ProxyURL(proxyURL)
}
var redirectFunc func(req *http.Request, via []*http.Request) error
if !opt.FollowRedirect {
redirectFunc = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
} else {
redirectFunc = nil
}
tlsConfig := tls.Config{
InsecureSkipVerify: opt.NoTLSValidation,
// enable TLS1.0 and TLS1.1 support
MinVersion: tls.VersionTLS10,
}
if opt.TLSCertificate != nil {
tlsConfig.Certificates = []tls.Certificate{*opt.TLSCertificate}
}
client.client = &http.Client{
Timeout: opt.Timeout,
CheckRedirect: redirectFunc,
Transport: &http.Transport{
Proxy: proxyURLFunc,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
TLSClientConfig: &tlsConfig,
}}
client.username = opt.Username
client.password = opt.Password
client.userAgent = opt.UserAgent
client.defaultUserAgent = DefaultUserAgent()
client.headers = opt.Headers
client.noCanonicalizeHeaders = opt.NoCanonicalizeHeaders
client.cookies = opt.Cookies
client.method = opt.Method
if client.method == "" {
client.method = http.MethodGet
}
// Host header needs to be set separately
for _, h := range opt.Headers {
if h.Name == "Host" {
client.host = h.Value
break
}
}
return &client, nil
}
// Request makes an http request and returns the status, the content length, the headers, the body and an error
// if you want the body returned set the corresponding property inside RequestOptions
func (client *HTTPClient) Request(ctx context.Context, fullURL string, opts RequestOptions) (int, int64, http.Header, []byte, error) {
resp, err := client.makeRequest(ctx, fullURL, opts)
if err != nil {
// ignore context canceled errors
if errors.Is(ctx.Err(), context.Canceled) {
return 0, 0, nil, nil, nil
}
return 0, 0, nil, nil, err
}
defer resp.Body.Close()
var body []byte
var length int64
if opts.ReturnBody {
body, err = io.ReadAll(resp.Body)
if err != nil {
return 0, 0, nil, nil, fmt.Errorf("could not read body %w", err)
}
length = int64(len(body))
} else {
// DO NOT REMOVE!
// absolutely needed so golang will reuse connections!
length, err = io.Copy(io.Discard, resp.Body)
if err != nil {
return 0, 0, nil, nil, err
}
}
return resp.StatusCode, length, resp.Header, body, nil
}
func (client *HTTPClient) makeRequest(ctx context.Context, fullURL string, opts RequestOptions) (*http.Response, error) {
req, err := http.NewRequest(client.method, fullURL, opts.Body)
if err != nil {
return nil, err
}
// add the context so we can easily cancel out
req = req.WithContext(ctx)
if client.cookies != "" {
req.Header.Set("Cookie", client.cookies)
}
// Use host for VHOST mode on a per request basis, otherwise the one provided from headers
if opts.Host != "" {
req.Host = opts.Host
} else if client.host != "" {
req.Host = client.host
}
if client.userAgent != "" {
req.Header.Set("User-Agent", client.userAgent)
} else {
req.Header.Set("User-Agent", client.defaultUserAgent)
}
// add custom headers
// if ModifiedHeaders are supplied use those, otherwise use the original ones
// currently only relevant on fuzzing
if len(opts.ModifiedHeaders) > 0 {
for _, h := range opts.ModifiedHeaders {
if client.noCanonicalizeHeaders {
// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
req.Header[h.Name] = []string{h.Value}
} else {
req.Header.Set(h.Name, h.Value)
}
}
} else {
for _, h := range client.headers {
if client.noCanonicalizeHeaders {
// https://stackoverflow.com/questions/26351716/how-to-keep-key-case-sensitive-in-request-header-using-golang
req.Header[h.Name] = []string{h.Value}
} else {
req.Header.Set(h.Name, h.Value)
}
}
}
if opts.UpdatedBasicAuthUsername != "" {
req.SetBasicAuth(opts.UpdatedBasicAuthUsername, opts.UpdatedBasicAuthPassword)
} else if client.username != "" {
req.SetBasicAuth(client.username, client.password)
}
resp, err := client.client.Do(req)
if err != nil {
var ue *url.Error
if errors.As(err, &ue) {
if strings.HasPrefix(ue.Err.Error(), "x509") {
return nil, fmt.Errorf("invalid certificate: %w", ue.Err)
}
}
return nil, err
}
return resp, nil
}

127
libgobuster/http_test.go Normal file
View File

@ -0,0 +1,127 @@
package libgobuster
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"math/big"
"net/http"
"net/http/httptest"
"testing"
)
func httpServerB(b *testing.B, content string) *httptest.Server {
b.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, content)
}))
return ts
}
func httpServerT(t *testing.T, content string) *httptest.Server {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, content)
}))
return ts
}
func randomString(length int) (string, error) {
var letter = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
letterLen := len(letter)
b := make([]byte, length)
for i := range b {
n, err := rand.Int(rand.Reader, big.NewInt(int64(letterLen)))
if err != nil {
return "", err
}
b[i] = letter[n.Int64()]
}
return string(b), nil
}
func TestRequest(t *testing.T) {
t.Parallel()
ret, err := randomString(100)
if err != nil {
t.Fatal(err)
}
h := httpServerT(t, ret)
defer h.Close()
var o HTTPOptions
c, err := NewHTTPClient(&o)
if err != nil {
t.Fatalf("Got Error: %v", err)
}
status, length, _, body, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: true})
if err != nil {
t.Fatalf("Got Error: %v", err)
}
if status != 200 {
t.Fatalf("Invalid status returned: %d", status)
}
if length != int64(len(ret)) {
t.Fatalf("Invalid length returned: %d", length)
}
if body == nil || !bytes.Equal(body, []byte(ret)) {
t.Fatalf("Invalid body returned: %d", body)
}
}
func BenchmarkRequestWithoutBody(b *testing.B) {
r, err := randomString(10000)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
c, err := NewHTTPClient(&o)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
for x := 0; x < b.N; x++ {
_, _, _, _, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: false})
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}
func BenchmarkRequestWitBody(b *testing.B) {
r, err := randomString(10000)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
c, err := NewHTTPClient(&o)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
for x := 0; x < b.N; x++ {
_, _, _, _, err := c.Request(context.Background(), h.URL, RequestOptions{ReturnBody: true})
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}
func BenchmarkNewHTTPClient(b *testing.B) {
r, err := randomString(500)
if err != nil {
b.Fatal(err)
}
h := httpServerB(b, r)
defer h.Close()
var o HTTPOptions
for x := 0; x < b.N; x++ {
_, err := NewHTTPClient(&o)
if err != nil {
b.Fatalf("Got Error: %v", err)
}
}
}

17
libgobuster/interfaces.go Normal file
View File

@ -0,0 +1,17 @@
package libgobuster
import "context"
// GobusterPlugin is an interface which plugins must implement
type GobusterPlugin interface {
Name() string
PreRun(context.Context, *Progress) error
ProcessWord(context.Context, string, *Progress) error
AdditionalWords(string) []string
GetConfigString() (string, error)
}
// Result is an interface for the Result object
type Result interface {
ResultToString() (string, error)
}

219
libgobuster/libgobuster.go Normal file
View File

@ -0,0 +1,219 @@
package libgobuster
import (
"bufio"
"context"
"fmt"
"os"
"strings"
"sync"
"time"
)
// PATTERN is the pattern for wordlist replacements in pattern file
const PATTERN = "{GOBUSTER}"
// SetupFunc is the "setup" function prototype for implementations
type SetupFunc func(*Gobuster) error
// ProcessFunc is the "process" function prototype for implementations
type ProcessFunc func(*Gobuster, string) ([]Result, error)
// ResultToStringFunc is the "to string" function prototype for implementations
type ResultToStringFunc func(*Gobuster, *Result) (*string, error)
// Gobuster is the main object when creating a new run
type Gobuster struct {
Opts *Options
Logger Logger
plugin GobusterPlugin
Progress *Progress
}
// NewGobuster returns a new Gobuster object
func NewGobuster(opts *Options, plugin GobusterPlugin, logger Logger) (*Gobuster, error) {
var g Gobuster
g.Opts = opts
g.plugin = plugin
g.Logger = logger
g.Progress = NewProgress()
return &g, nil
}
func (g *Gobuster) worker(ctx context.Context, wordChan <-chan string, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case word, ok := <-wordChan:
// worker finished
if !ok {
return
}
g.Progress.incrementRequests()
wordCleaned := strings.TrimSpace(word)
// Skip "comment" (starts with #), as well as empty lines
if strings.HasPrefix(wordCleaned, "#") || len(wordCleaned) == 0 {
break
}
// Mode-specific processing
err := g.plugin.ProcessWord(ctx, wordCleaned, g.Progress)
if err != nil {
// do not exit and continue
g.Progress.ErrorChan <- err
continue
}
select {
case <-ctx.Done():
case <-time.After(g.Opts.Delay):
}
}
}
}
func (g *Gobuster) getWordlist() (*bufio.Scanner, error) {
if g.Opts.Wordlist == "-" {
// Read directly from stdin
return bufio.NewScanner(os.Stdin), nil
}
// Pull content from the wordlist
wordlist, err := os.Open(g.Opts.Wordlist)
if err != nil {
return nil, fmt.Errorf("failed to open wordlist: %w", err)
}
lines, err := lineCounter(wordlist)
if err != nil {
return nil, fmt.Errorf("failed to get number of lines: %w", err)
}
if lines-g.Opts.WordlistOffset <= 0 {
return nil, fmt.Errorf("offset is greater than the number of lines in the wordlist")
}
// calcutate expected requests
g.Progress.IncrementTotalRequests(lines)
// add offset if needed (offset defaults to 0)
g.Progress.incrementRequestsIssues(g.Opts.WordlistOffset)
// call the function once with a dummy entry to receive the number
// of custom words per wordlist word
customWordsLen := len(g.plugin.AdditionalWords("dummy"))
if customWordsLen > 0 {
origExpected := g.Progress.RequestsExpected()
inc := origExpected * customWordsLen
g.Progress.IncrementTotalRequests(inc)
}
// rewind wordlist
_, err = wordlist.Seek(0, 0)
if err != nil {
return nil, fmt.Errorf("failed to rewind wordlist: %w", err)
}
wordlistScanner := bufio.NewScanner(wordlist)
// skip lines
for i := 0; i < g.Opts.WordlistOffset; i++ {
if !wordlistScanner.Scan() {
if err := wordlistScanner.Err(); err != nil {
return nil, fmt.Errorf("failed to skip lines in wordlist: %w", err)
}
return nil, fmt.Errorf("failed to skip lines in wordlist")
}
}
return wordlistScanner, nil
}
// Run the busting of the website with the given
// set of settings from the command line.
func (g *Gobuster) Run(ctx context.Context) error {
defer close(g.Progress.ResultChan)
defer close(g.Progress.ErrorChan)
defer close(g.Progress.MessageChan)
if err := g.plugin.PreRun(ctx, g.Progress); err != nil {
return err
}
var workerGroup sync.WaitGroup
workerGroup.Add(g.Opts.Threads)
wordChan := make(chan string, g.Opts.Threads)
// Create goroutines for each of the number of threads
// specified.
for i := 0; i < g.Opts.Threads; i++ {
go g.worker(ctx, wordChan, &workerGroup)
}
scanner, err := g.getWordlist()
if err != nil {
return err
}
Scan:
for scanner.Scan() {
select {
case <-ctx.Done():
break Scan
default:
word := scanner.Text()
perms := g.processPatterns(word)
// add the original word
wordChan <- word
// now create perms
for _, w := range perms {
select {
// need to check here too otherwise wordChan will block
case <-ctx.Done():
break Scan
case wordChan <- w:
}
}
for _, w := range g.plugin.AdditionalWords(word) {
select {
// need to check here too otherwise wordChan will block
case <-ctx.Done():
break Scan
case wordChan <- w:
}
}
}
}
close(wordChan)
workerGroup.Wait()
if err := scanner.Err(); err != nil {
return err
}
return nil
}
// GetConfigString returns the current config as a printable string
func (g *Gobuster) GetConfigString() (string, error) {
return g.plugin.GetConfigString()
}
func (g *Gobuster) processPatterns(word string) []string {
if g.Opts.PatternFile == "" {
return nil
}
//nolint:prealloc
var pat []string
for _, x := range g.Opts.Patterns {
repl := strings.ReplaceAll(x, PATTERN, word)
pat = append(pat, repl)
}
return pat
}

80
libgobuster/logger.go Normal file
View File

@ -0,0 +1,80 @@
package libgobuster
import (
"log"
"os"
"github.com/fatih/color"
)
type Logger struct {
log *log.Logger
errorLog *log.Logger
debugLog *log.Logger
infoLog *log.Logger
debug bool
}
func NewLogger(debug bool) Logger {
return Logger{
log: log.New(os.Stdout, "", 0),
errorLog: log.New(os.Stderr, color.New(color.FgRed).Sprint("[ERROR] "), 0),
debugLog: log.New(os.Stderr, color.New(color.FgBlue).Sprint("[DEBUG] "), 0),
infoLog: log.New(os.Stderr, color.New(color.FgCyan).Sprint("[INFO] "), 0),
debug: debug,
}
}
func (l Logger) Debug(v ...any) {
if !l.debug {
return
}
l.debugLog.Print(v...)
}
func (l Logger) Debugf(format string, v ...any) {
if !l.debug {
return
}
l.debugLog.Printf(format, v...)
}
func (l Logger) Info(v ...any) {
l.infoLog.Print(v...)
}
func (l Logger) Infof(format string, v ...any) {
l.infoLog.Printf(format, v...)
}
func (l Logger) Print(v ...any) {
l.log.Print(v...)
}
func (l Logger) Printf(format string, v ...any) {
l.log.Printf(format, v...)
}
func (l Logger) Println(v ...any) {
l.log.Println(v...)
}
func (l Logger) Error(v ...any) {
l.errorLog.Print(v...)
}
func (l Logger) Errorf(format string, v ...any) {
l.errorLog.Printf(format, v...)
}
func (l Logger) Fatal(v ...any) {
l.errorLog.Fatal(v...)
}
func (l Logger) Fatalf(format string, v ...any) {
l.errorLog.Fatalf(format, v...)
}
func (l Logger) Fatalln(v ...any) {
l.errorLog.Fatalln(v...)
}

26
libgobuster/options.go Normal file
View File

@ -0,0 +1,26 @@
package libgobuster
import "time"
// Options holds all options that can be passed to libgobuster
type Options struct {
Threads int
Debug bool
Wordlist string
WordlistOffset int
PatternFile string
Patterns []string
OutputFilename string
NoStatus bool
NoProgress bool
NoError bool
NoStdout bool
Quiet bool
Verbose bool
Delay time.Duration
}
// NewOptions returns a new initialized Options object
func NewOptions() *Options {
return &Options{}
}

View File

@ -0,0 +1,30 @@
package libgobuster
import (
"crypto/tls"
"time"
)
// BasicHTTPOptions defines only core http options
type BasicHTTPOptions struct {
UserAgent string
Proxy string
NoTLSValidation bool
Timeout time.Duration
RetryOnTimeout bool
RetryAttempts int
TLSCertificate *tls.Certificate
}
// HTTPOptions is the struct to pass in all http options to Gobuster
type HTTPOptions struct {
BasicHTTPOptions
Password string
URL string
Username string
Cookies string
Headers []HTTPHeader
NoCanonicalizeHeaders bool
FollowRedirect bool
Method string
}

67
libgobuster/progress.go Normal file
View File

@ -0,0 +1,67 @@
package libgobuster
import "sync"
type MessageLevel int
const (
LevelDebug MessageLevel = iota
LevelInfo
LevelError
)
type Message struct {
Level MessageLevel
Message string
}
type Progress struct {
requestsExpectedMutex *sync.RWMutex
requestsExpected int
requestsCountMutex *sync.RWMutex
requestsIssued int
ResultChan chan Result
ErrorChan chan error
MessageChan chan Message
}
func NewProgress() *Progress {
var p Progress
p.requestsIssued = 0
p.requestsExpectedMutex = new(sync.RWMutex)
p.requestsCountMutex = new(sync.RWMutex)
p.ResultChan = make(chan Result)
p.ErrorChan = make(chan error)
p.MessageChan = make(chan Message)
return &p
}
func (p *Progress) RequestsExpected() int {
p.requestsExpectedMutex.RLock()
defer p.requestsExpectedMutex.RUnlock()
return p.requestsExpected
}
func (p *Progress) RequestsIssued() int {
p.requestsCountMutex.RLock()
defer p.requestsCountMutex.RUnlock()
return p.requestsIssued
}
func (p *Progress) incrementRequestsIssues(by int) {
p.requestsCountMutex.Lock()
defer p.requestsCountMutex.Unlock()
p.requestsIssued += by
}
func (p *Progress) incrementRequests() {
p.requestsCountMutex.Lock()
defer p.requestsCountMutex.Unlock()
p.requestsIssued++
}
func (p *Progress) IncrementTotalRequests(by int) {
p.requestsCountMutex.Lock()
defer p.requestsCountMutex.Unlock()
p.requestsExpected += by
}

4676
libgobuster/useragents.go Normal file

File diff suppressed because it is too large Load Diff

6
libgobuster/version.go Normal file
View File

@ -0,0 +1,6 @@
package libgobuster
const (
// VERSION contains the current gobuster version
VERSION = "3.6"
)

23
main.go Normal file
View File

@ -0,0 +1,23 @@
package main
import "git.sual.in/casual/gobuster-lib/cli/cmd"
//----------------------------------------------------
// Gobuster -- by OJ Reeves
//
// A crap attempt at building something that resembles
// dirbuster or dirb using Go. The goal was to build
// a tool that would help learn Go and to actually do
// something useful. The idea of having this compile
// to native code is also appealing.
//
// Run: gobuster -h
//
// Please see THANKS file for contributors.
// Please see LICENSE file for license details.
//
//----------------------------------------------------
func main() {
cmd.Execute()
}