stupid go modules
This commit is contained in:
commit
c2c0dcde76
23
Dockerfile
Normal file
23
Dockerfile
Normal 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
202
LICENSE
Normal 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
41
Makefile
Normal 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
977
README.md
Normal 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
188
cli/cmd/dir.go
Normal 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
88
cli/cmd/dir_test.go
Normal 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
115
cli/cmd/dns.go
Normal 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
148
cli/cmd/fuzz.go
Normal 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
79
cli/cmd/gcs.go
Normal 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
258
cli/cmd/http.go
Normal 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
194
cli/cmd/root.go
Normal 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
79
cli/cmd/s3.go
Normal 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
80
cli/cmd/tftp.go
Normal 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
27
cli/cmd/version.go
Normal 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
105
cli/cmd/vhost.go
Normal 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
65
cli/cmd/vhost_test.go
Normal 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
7
cli/const.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
const (
|
||||||
|
TERMINAL_CLEAR_LINE = "\r\x1b[2K"
|
||||||
|
)
|
7
cli/const_windows.go
Normal file
7
cli/const_windows.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
const (
|
||||||
|
TERMINAL_CLEAR_LINE = "\r\r"
|
||||||
|
)
|
232
cli/gobuster.go
Normal file
232
cli/gobuster.go
Normal 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
43
cspell.json
Normal 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
22
go.mod
Normal 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
37
go.sum
Normal 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
392
gobusterdir/gobusterdir.go
Normal 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
34
gobusterdir/options.go
Normal 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](),
|
||||||
|
}
|
||||||
|
}
|
16
gobusterdir/options_test.go
Normal file
16
gobusterdir/options_test.go
Normal 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
100
gobusterdir/result.go
Normal 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
244
gobusterdns/gobusterdns.go
Normal 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
21
gobusterdns/options.go
Normal 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
57
gobusterdns/result.go
Normal 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
|
||||||
|
}
|
275
gobusterfuzz/gobusterfuzz.go
Normal file
275
gobusterfuzz/gobusterfuzz.go
Normal 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
23
gobusterfuzz/options.go
Normal 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](),
|
||||||
|
}
|
||||||
|
}
|
12
gobusterfuzz/options_test.go
Normal file
12
gobusterfuzz/options_test.go
Normal 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
47
gobusterfuzz/result.go
Normal 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
266
gobustergcs/gobustersgcs.go
Normal 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
16
gobustergcs/options.go
Normal 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
35
gobustergcs/result.go
Normal 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
25
gobustergcs/types.go
Normal 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
260
gobusters3/gobusters3.go
Normal 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
16
gobusters3/options.go
Normal 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
35
gobusters3/result.go
Normal 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
24
gobusters3/types.go
Normal 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"`
|
||||||
|
}
|
142
gobustertftp/gobustertftp.go
Normal file
142
gobustertftp/gobustertftp.go
Normal 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
16
gobustertftp/options.go
Normal 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
46
gobustertftp/result.go
Normal 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
|
||||||
|
}
|
265
gobustervhost/gobustervhost.go
Normal file
265
gobustervhost/gobustervhost.go
Normal 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
21
gobustervhost/options.go
Normal 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
55
gobustervhost/result.go
Normal 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
11
libgobuster/go.mod
Normal 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
11
libgobuster/go.sum
Normal 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
198
libgobuster/helpers.go
Normal 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
323
libgobuster/helpers_test.go
Normal 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
210
libgobuster/http.go
Normal 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
127
libgobuster/http_test.go
Normal 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
17
libgobuster/interfaces.go
Normal 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
219
libgobuster/libgobuster.go
Normal 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
80
libgobuster/logger.go
Normal 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
26
libgobuster/options.go
Normal 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{}
|
||||||
|
}
|
30
libgobuster/options_http.go
Normal file
30
libgobuster/options_http.go
Normal 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
67
libgobuster/progress.go
Normal 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
4676
libgobuster/useragents.go
Normal file
File diff suppressed because it is too large
Load Diff
6
libgobuster/version.go
Normal file
6
libgobuster/version.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package libgobuster
|
||||||
|
|
||||||
|
const (
|
||||||
|
// VERSION contains the current gobuster version
|
||||||
|
VERSION = "3.6"
|
||||||
|
)
|
23
main.go
Normal file
23
main.go
Normal 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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user