pax_global_header00006660000000000000000000000064133416234570014522gustar00rootroot0000000000000052 comment=c361d774a3a3f9c20e4e8d9f669462cf402f8e12 gobuster-2.0.1/000077500000000000000000000000001334162345700133545ustar00rootroot00000000000000gobuster-2.0.1/.gitignore000066400000000000000000000004461334162345700153500ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof *.txt *.swp gobuster build gobuster-2.0.1/.travis.yml000066400000000000000000000000761334162345700154700ustar00rootroot00000000000000language: go go: - "1.x" - "1.8" - "1.10.x" - master gobuster-2.0.1/LICENSE000066400000000000000000000260751334162345700143730ustar00rootroot00000000000000Apache 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. gobuster-2.0.1/Makefile000066400000000000000000000015701334162345700150170ustar00rootroot00000000000000TARGET=./build OSES=darwin linux windows ARCHS=amd64 386 current: outputdir @go build -o ./gobuster; \ echo "Done." outputdir: @mkdir -p ${TARGET} windows: outputdir @for GOARCH in ${ARCHS}; do \ echo "Building for windows $${GOARCH} ..." ; \ GOOS=windows GARCH=$${GOARCH} go build -o ${TARGET}/gobuster-$${GOARCH}.exe ; \ done; \ echo "Done." linux: outputdir @for GOARCH in ${ARCHS}; do \ echo "Building for linux $${GOARCH} ..." ; \ GOOS=linux GARCH=$${GOARCH} go build -o ${TARGET}/gobuster-linux-$${GOARCH} ; \ done; \ echo "Done." darwin: outputdir @for GOARCH in ${ARCHS}; do \ echo "Building for darwin $${GOARCH} ..." ; \ GOOS=darwin GARCH=$${GOARCH} go build -o ${TARGET}/gobuster-darwin-$${GOARCH} ; \ done; \ echo "Done." all: darwin linux windows test: @go test -v -race ./... ; \ echo "Done." clean: @rm -rf ${TARGET}/* ; \ echo "Done." gobuster-2.0.1/README.md000066400000000000000000000341371334162345700146430ustar00rootroot00000000000000Gobuster v2.0.1 (OJ Reeves @TheColonial) ======================================== Gobuster is a tool used to brute-force: * URIs (directories and files) in web sites. * DNS subdomains (with wildcard support). ### Travis CI Status Because all the cool kids are doing it: [![Build Status](https://travis-ci.com/OJ/gobuster.svg?branch=master)](https://travis-ci.com/OJ/gobuster) ### Oh dear God.. WHY!? Because I wanted: 1. ... something that didn't have a fat Java GUI (console FTW). 1. ... to build something that just worked on the command line. 1. ... something that did not do recursive brute force. 1. ... something that allowed me to brute force folders and multiple extensions at once. 1. ... something that compiled to native on multiple platforms. 1. ... something that was faster than an interpreted script (such as Python). 1. ... something that didn't require a runtime. 1. ... use something that was good with concurrency (hence Go). 1. ... to build something in Go that wasn't totally useless. ### But it's shit! And your implementation sucks! Yes, you're probably correct. Feel free to: * Not use it. * Show me how to do it better. ### Common Command line options * `-fw` - force processing of a domain with wildcard results. * `-np` - hide the progress output. * `-m ` - which mode to use, either `dir` or `dns` (default: `dir`). * `-q` - disables banner/underline output. * `-t ` - number of threads to run (default: `10`). * `-u ` - full URL (including scheme), or base domain name. * `-v` - verbose output (show all results). * `-w ` - path to the wordlist used for brute forcing (use `-` for stdin). ### Command line options for `dns` mode * `-cn` - show CNAME records (cannot be used with '-i' option). * `-i` - show all IP addresses for the result. ### Command line options for `dir` mode * `-a ` - specify a user agent string to send in the request header. * `-c ` - use this to specify any cookies that you might need (simulating auth). * `-e` - specify extended mode that renders the full URL. * `-f` - append `/` for directory brute forces. * `-k` - Skip verification of SSL certificates. * `-l` - show the length of the response. * `-n` - "no status" mode, disables the output of the result's status code. * `-o ` - specify a file name to write the output to. * `-p ` - specify a proxy to use for all requests (scheme much match the URL scheme). * `-r` - follow redirects. * `-s ` - comma-separated set of the list of status codes to be deemed a "positive" (default: `200,204,301,302,307`). * `-x ` - list of extensions to check for, if any. * `-P ` - HTTP Authorization password (Basic Auth only, prompted if missing). * `-U ` - HTTP Authorization username (Basic Auth only). * `-to ` - HTTP timeout. Examples: 10s, 100ms, 1m (default: 10s). ### Building Since this tool is written in [Go](https://golang.org/) you need 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. #### Compiling `gobuster` now has external dependencies, and so they need to be pulled in first: ``` gobuster $ 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: ``` gobuster $ go install ``` If you have all the dependencies already, you can make use of the build scripts: * `make` - builds for the current Go configuration (ie. runs `go build`). * `make windows` - builds 32 and 64 bit binaries for windows, and writes them to the `build` subfolder. * `make linux` - builds 32 and 64 bit binaries for linux, and writes them to the `build` subfolder. * `make darwin` - builds 32 and 64 bit binaries for darwin, and writes them to the `build` subfolder. * `make all` - builds for all platforms and architectures, and writes the resulting binaries to the `build` subfolder. * `make clean` - clears out the `build` subfolder. * `make test` - runs the tests (requires you to `go get githubcom/h2non/gock` first). #### Running as a script ``` gobuster $ go run main.go ``` ### Wordlists via STDIN Wordlists can be piped into `gobuster` via stdin by providing a `-` to the `-w` option: ``` hashcat -a 3 --stdout ?l | gobuster -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. ### Examples #### `dir` mode Command line might look like this: ``` $ gobuster -u https://mysite.com/path/to/folder -c 'session=123456' -t 50 -w common-files.txt -x .php,.html ``` Default options looks like this: ``` $ gobuster -u https://buffered.io -w ~/wordlists/shortlist.txt ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dir [+] Url/Domain : https://buffered.io/ [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/shortlist.txt [+] Status codes : 200,204,301,302,307,403 [+] Timeout : 10s ===================================================== 2018/08/27 11:49:43 Starting gobuster ===================================================== /categories (Status: 301) /contact (Status: 301) /posts (Status: 301) /index (Status: 200) ===================================================== 2018/08/27 11:49:44 Finished ===================================================== ``` Default options with status codes disabled looks like this: ``` $ gobuster -u https://buffered.io -w ~/wordlists/shortlist.txt -n ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dir [+] Url/Domain : https://buffered.io/ [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/shortlist.txt [+] Status codes : 200,204,301,302,307,403 [+] No status : true [+] Timeout : 10s ===================================================== 2018/08/27 11:50:18 Starting gobuster ===================================================== /categories /contact /index /posts ===================================================== 2018/08/27 11:50:18 Finished ===================================================== ``` Verbose output looks like this: ``` $ gobuster -u https://buffered.io -w ~/wordlists/shortlist.txt -v ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dir [+] Url/Domain : https://buffered.io/ [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/shortlist.txt [+] Status codes : 200,204,301,302,307,403 [+] Verbose : true [+] Timeout : 10s ===================================================== 2018/08/27 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) ===================================================== 2018/08/27 11:50:51 Finished ===================================================== ``` Example showing content length: ``` $ gobuster -u https://buffered.io -w ~/wordlists/shortlist.txt -l ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dir [+] Url/Domain : https://buffered.io/ [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/shortlist.txt [+] Status codes : 200,204,301,302,307,403 [+] Show length : true [+] Timeout : 10s ===================================================== 2018/08/27 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] ===================================================== 2018/08/27 11:51:17 Finished ===================================================== ``` Quiet output, with status disabled and expanded mode looks like this ("grep mode"): ``` $ gobuster -u https://buffered.io -w ~/wordlists/shortlist.txt -q -n -e https://buffered.io/index https://buffered.io/contact https://buffered.io/posts https://buffered.io/categories ``` #### `dns` mode Command line might look like this: ``` $ gobuster -m dns -u mysite.com -t 50 -w common-names.txt ``` Normal sample run goes like this: ``` $ gobuster -m dns -w ~/wordlists/subdomains.txt -u google.com ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dns [+] Url/Domain : google.com [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/subdomains.txt ===================================================== 2018/08/27 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 ===================================================== 2018/08/27 11:54:20 Finished ===================================================== ``` Show IP sample run goes like this: ``` $ gobuster -m dns -w ~/wordlists/subdomains.txt -u google.com -i ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dns [+] Url/Domain : google.com [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/subdomains.txt ===================================================== 2018/08/27 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] ===================================================== 2018/08/27 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. ``` $ gobuster -m dns -w ~/wordlists/subdomains.txt -u yp.to -i ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dns [+] Url/Domain : yp.to [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/subdomains.txt ===================================================== 2018/08/27 11:56:43 Starting gobuster ===================================================== 2018/08/27 11:56:53 [-] Unable to validate base domain: yp.to Found: cr.yp.to [131.193.32.108, 131.193.32.109] ===================================================== 2018/08/27 11:56:53 Finished ===================================================== ``` Wildcard DNS is also detected properly: ``` $ gobuster -m dns -w ~/wordlists/subdomains.txt -u 0.0.1.xip.io ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dns [+] Url/Domain : 0.0.1.xip.io [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/subdomains.txt ===================================================== 2018/08/27 12:13:48 Starting gobuster ===================================================== 2018/08/27 12:13:48 [-] Wildcard DNS found. IP address(es): 1.0.0.0 2018/08/27 12:13:48 [!] To force processing of Wildcard DNS, specify the '-fw' switch. ===================================================== 2018/08/27 12:13:48 Finished ===================================================== ``` If the user wants to force processing of a domain that has wildcard entries, use `-fw`: ``` $ gobuster -m dns -w ~/wordlists/subdomains.txt -u 0.0.1.xip.io -fw ===================================================== Gobuster v2.0.1 OJ Reeves (@TheColonial) ===================================================== [+] Mode : dns [+] Url/Domain : 0.0.1.xip.io [+] Threads : 10 [+] Wordlist : /home/oj/wordlists/subdomains.txt ===================================================== 2018/08/27 12:13:51 Starting gobuster ===================================================== 2018/08/27 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 ===================================================== 2018/08/27 12:13:53 Finished ===================================================== ``` ### License See the LICENSE file. ### Thanks See the THANKS file for people who helped out. gobuster-2.0.1/THANKS000066400000000000000000000014101334162345700142630ustar00rootroot00000000000000@0x42424242 - initial DNS support @0xdevalias - Refactoring of code, and lots of other stuff @FireFart - Supporting connection reuse, resulting in been speed ups, refactoring, progress output and more @Ne0nd0g - STDIN support for wordlists @UID1K - initial DNS wildcard check support @averagesecurityguy - quiet mode support @eur0pa - Compiler error fixes for updated dependencies @g0tmi1k - content length, wordlist and command line parsing fixes @gehaxelt - DIR mode UUID wildcard detection @ilyaglow - Refactoring and tidying of code @justinsteven - HTTP basic auth support @kevinnz - custom user agent support @knapsy - saving output to file, and CNAME resolution for DNS mode @rverton - CLI flag to skip SSL verification @viaMorgoth - base domain validation for DNS mode gobuster-2.0.1/TODO.md000066400000000000000000000003501334162345700144410ustar00rootroot00000000000000* return specific errors and do not mention command line switches in libgobuster * no log.Printf and fmt.Printf inside libgobuster * use smth like `tabwriter.NewWriter(bw, 0, 5, 3, ' ', 0)` for outputting options (`GetConfigString`)gobuster-2.0.1/gobusterdir/000077500000000000000000000000001334162345700157055ustar00rootroot00000000000000gobuster-2.0.1/gobusterdir/gobusterdir.go000066400000000000000000000057751334162345700206030ustar00rootroot00000000000000package gobusterdir import ( "bytes" "fmt" "log" "github.com/OJ/gobuster/libgobuster" "github.com/google/uuid" ) // GobusterDir is the main type to implement the interface type GobusterDir struct{} // Setup is the setup implementation of gobusterdir func (d GobusterDir) Setup(g *libgobuster.Gobuster) error { _, _, err := g.GetRequest(g.Opts.URL) if err != nil { return fmt.Errorf("unable to connect to %s: %v", g.Opts.URL, err) } guid := uuid.New() url := fmt.Sprintf("%s%s", g.Opts.URL, guid) wildcardResp, _, err := g.GetRequest(url) if err != nil { return err } if g.Opts.StatusCodesParsed.Contains(*wildcardResp) { g.IsWildcard = true log.Printf("[-] Wildcard response found: %s => %d", url, *wildcardResp) if !g.Opts.WildcardForced { return fmt.Errorf("To force processing of Wildcard responses, specify the '-fw' switch.") } } return nil } // Process is the process implementation of gobusterdir func (d GobusterDir) Process(g *libgobuster.Gobuster, word string) ([]libgobuster.Result, error) { suffix := "" if g.Opts.UseSlash { suffix = "/" } // Try the DIR first url := fmt.Sprintf("%s%s%s", g.Opts.URL, word, suffix) dirResp, dirSize, err := g.GetRequest(url) if err != nil { return nil, err } var ret []libgobuster.Result if dirResp != nil { ret = append(ret, libgobuster.Result{ Entity: fmt.Sprintf("%s%s", word, suffix), Status: *dirResp, Size: dirSize, }) } // Follow up with files using each ext. for ext := range g.Opts.ExtensionsParsed.Set { file := fmt.Sprintf("%s.%s", word, ext) url = fmt.Sprintf("%s%s", g.Opts.URL, file) fileResp, fileSize, err := g.GetRequest(url) if err != nil { return nil, err } if fileResp != nil { ret = append(ret, libgobuster.Result{ Entity: file, Status: *fileResp, Size: fileSize, }) } } return ret, nil } // ResultToString is the to string implementation of gobusterdir func (d GobusterDir) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Result) (*string, error) { buf := &bytes.Buffer{} // Prefix if we're in verbose mode if g.Opts.Verbose { if g.Opts.StatusCodesParsed.Contains(r.Status) { if _, err := fmt.Fprintf(buf, "Found: "); err != nil { return nil, err } } else { if _, err := fmt.Fprintf(buf, "Missed: "); err != nil { return nil, err } } } if g.Opts.StatusCodesParsed.Contains(r.Status) || g.Opts.Verbose { if g.Opts.Expanded { if _, err := fmt.Fprintf(buf, g.Opts.URL); err != nil { return nil, err } } else { if _, err := fmt.Fprintf(buf, "/"); err != nil { return nil, err } } if _, err := fmt.Fprintf(buf, r.Entity); err != nil { return nil, err } if !g.Opts.NoStatus { if _, err := fmt.Fprintf(buf, " (Status: %d)", r.Status); err != nil { return nil, err } } if r.Size != nil { if _, err := fmt.Fprintf(buf, " [Size: %d]", *r.Size); err != nil { return nil, err } } if _, err := fmt.Fprintf(buf, "\n"); err != nil { return nil, err } } s := buf.String() return &s, nil } gobuster-2.0.1/gobusterdns/000077500000000000000000000000001334162345700157135ustar00rootroot00000000000000gobuster-2.0.1/gobusterdns/gobusterdns.go000066400000000000000000000050461334162345700206060ustar00rootroot00000000000000package gobusterdns import ( "bytes" "fmt" "log" "strings" "github.com/OJ/gobuster/libgobuster" "github.com/google/uuid" ) // GobusterDNS is the main type to implement the interface type GobusterDNS struct{} // Setup is the setup implementation of gobusterdns func (d GobusterDNS) Setup(g *libgobuster.Gobuster) error { // Resolve a subdomain sthat probably shouldn't exist guid := uuid.New() wildcardIps, err := g.DNSLookup(fmt.Sprintf("%s.%s", guid, g.Opts.URL)) if err == nil { g.IsWildcard = true g.WildcardIps.AddRange(wildcardIps) log.Printf("[-] Wildcard DNS found. IP address(es): %s", g.WildcardIps.Stringify()) if !g.Opts.WildcardForced { return fmt.Errorf("To force processing of Wildcard DNS, specify the '-fw' switch.") } } if !g.Opts.Quiet { // Provide a warning if the base domain doesn't resolve (in case of typo) _, err = g.DNSLookup(g.Opts.URL) if err != nil { // Not an error, just a warning. Eg. `yp.to` doesn't resolve, but `cr.py.to` does! log.Printf("[-] Unable to validate base domain: %s", g.Opts.URL) } } return nil } // Process is the process implementation of gobusterdns func (d GobusterDNS) Process(g *libgobuster.Gobuster, word string) ([]libgobuster.Result, error) { subdomain := fmt.Sprintf("%s.%s", word, g.Opts.URL) ips, err := g.DNSLookup(subdomain) var ret []libgobuster.Result if err == nil { if !g.IsWildcard || !g.WildcardIps.ContainsAny(ips) { result := libgobuster.Result{ Entity: subdomain, } if g.Opts.ShowIPs { result.Extra = strings.Join(ips, ", ") } else if g.Opts.ShowCNAME { cname, err := g.DNSLookupCname(subdomain) if err == nil { result.Extra = cname } } ret = append(ret, result) } } else if g.Opts.Verbose { ret = append(ret, libgobuster.Result{ Entity: subdomain, Status: 404, }) } return ret, nil } // ResultToString is the to string implementation of gobusterdns func (d GobusterDNS) ResultToString(g *libgobuster.Gobuster, r *libgobuster.Result) (*string, error) { buf := &bytes.Buffer{} if r.Status == 404 { if _, err := fmt.Fprintf(buf, "Missing: %s\n", r.Entity); err != nil { return nil, err } } else if g.Opts.ShowIPs { if _, err := fmt.Fprintf(buf, "Found: %s [%s]\n", r.Entity, r.Extra); err != nil { return nil, err } } else if g.Opts.ShowCNAME { if _, err := fmt.Fprintf(buf, "Found: %s [%s]\n", r.Entity, r.Extra); err != nil { return nil, err } } else { if _, err := fmt.Fprintf(buf, "Found: %s\n", r.Entity); err != nil { return nil, err } } s := buf.String() return &s, nil } gobuster-2.0.1/libgobuster/000077500000000000000000000000001334162345700156755ustar00rootroot00000000000000gobuster-2.0.1/libgobuster/helpers.go000066400000000000000000000034541334162345700176740ustar00rootroot00000000000000package libgobuster import ( "bytes" "fmt" "io" "sort" "strings" ) type intSet struct { Set map[int]bool } type stringSet struct { Set map[string]bool } func newStringSet() stringSet { return stringSet{Set: map[string]bool{}} } // Add an element to a set func (set *stringSet) Add(s string) bool { _, found := set.Set[s] set.Set[s] = true return !found } // Add a list of elements to a set func (set *stringSet) AddRange(ss []string) { for _, s := range ss { set.Set[s] = true } } // Test if an element is in a set func (set *stringSet) Contains(s string) bool { _, found := set.Set[s] return found } // Check if any of the elements exist func (set *stringSet) ContainsAny(ss []string) bool { for _, s := range ss { if set.Set[s] { return true } } return false } // Stringify the set func (set *stringSet) Stringify() string { values := []string{} for s := range set.Set { values = append(values, s) } return strings.Join(values, ",") } func newIntSet() intSet { return intSet{Set: map[int]bool{}} } // Add an element to a set func (set *intSet) Add(i int) bool { _, found := set.Set[i] set.Set[i] = true return !found } // Test if an element is in a set func (set *intSet) Contains(i int) bool { _, found := set.Set[i] return found } // Stringify the set func (set *intSet) Stringify() string { values := []int{} for s := range set.Set { values = append(values, s) } sort.Ints(values) delim := "," return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(values)), delim), "[]") } 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 err == io.EOF: return count, nil case err != nil: return count, err } } } gobuster-2.0.1/libgobuster/helpers_test.go000066400000000000000000000066651334162345700207420ustar00rootroot00000000000000package libgobuster import ( "strings" "testing" "testing/iotest" ) func TestNewStringSet(t *testing.T) { if newStringSet().Set == nil { t.Fatal("newStringSet returned nil Set") } } func TestNewIntSet(t *testing.T) { if newIntSet().Set == nil { t.Fatal("newIntSet returned nil Set") } } func TestStringSetAdd(t *testing.T) { x := newStringSet() x.Add("test") if len(x.Set) != 1 { t.Fatalf("Unexptected size. Should have 1 Got %v", len(x.Set)) } } func TestStringSetAddDouble(t *testing.T) { x := newStringSet() x.Add("test") x.Add("test") if len(x.Set) != 1 { t.Fatalf("Unexptected size. Should have 1 Got %d", len(x.Set)) } } func TestStringSetAddRange(t *testing.T) { x := newStringSet() x.AddRange([]string{"asdf", "ghjk"}) if len(x.Set) != 2 { t.Fatalf("Unexptected size. Should have 2 Got %d", len(x.Set)) } } func TestStringSetAddRangeDouble(t *testing.T) { x := newStringSet() x.AddRange([]string{"asdf", "ghjk", "asdf", "ghjk"}) if len(x.Set) != 2 { t.Fatalf("Unexptected size. Should have 2 Got %d", len(x.Set)) } } func TestStringSetContains(t *testing.T) { x := newStringSet() v := []string{"asdf", "ghjk", "1234", "5678"} x.AddRange(v) for _, y := range v { if !x.Contains(y) { t.Fatalf("Did not find value %s in array. %v", y, x.Set) } } } func TestStringSetContainsAny(t *testing.T) { x := newStringSet() v := []string{"asdf", "ghjk", "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") } } func TestStringSetStringify(t *testing.T) { x := newStringSet() v := []string{"asdf", "ghjk", "1234", "5678"} x.AddRange(v) z := x.Stringify() // order is random for _, y := range v { if !strings.Contains(z, y) { t.Fatalf("Did not find value %q in %q", y, z) } } } func TestIntSetAdd(t *testing.T) { x := newIntSet() x.Add(1) if len(x.Set) != 1 { t.Fatalf("Unexptected size. Should have 1 Got %d", len(x.Set)) } } func TestIntSetAddDouble(t *testing.T) { x := newIntSet() x.Add(1) x.Add(1) if len(x.Set) != 1 { t.Fatalf("Unexptected size. Should have 1 Got %d", len(x.Set)) } } func TestIntSetContains(t *testing.T) { x := newIntSet() v := []int{1, 2, 3, 4} for _, y := range v { x.Add(y) } for _, y := range v { if !x.Contains(y) { t.Fatalf("Did not find value %d in array. %v", y, x.Set) } } } func TestIntSetStringify(t *testing.T) { x := newIntSet() v := []int{1, 3, 2, 4} expected := "1,2,3,4" for _, y := range v { x.Add(y) } z := x.Stringify() // should be sorted if expected != z { t.Fatalf("Expected %q got %q", expected, z) } } func TestLineCounter(t *testing.T) { 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 { t.Run(x.testName, func(t *testing.T) { 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) { r := iotest.TimeoutReader(strings.NewReader("test")) _, err := lineCounter(r) if err != iotest.ErrTimeout { t.Fatalf("Got wrong error! %v", err) } } gobuster-2.0.1/libgobuster/http.go000066400000000000000000000054121334162345700172050ustar00rootroot00000000000000package libgobuster import ( "context" "crypto/tls" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" "unicode/utf8" ) type httpClient struct { client *http.Client context context.Context userAgent string username string password string includeLength bool } // NewHTTPClient returns a new HTTPClient func newHTTPClient(c context.Context, opt *Options) (*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 (%v)", 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 } client.client = &http.Client{ Timeout: opt.Timeout, CheckRedirect: redirectFunc, Transport: &http.Transport{ Proxy: proxyURLFunc, TLSClientConfig: &tls.Config{ InsecureSkipVerify: opt.InsecureSSL, }, }} client.context = c client.username = opt.Username client.password = opt.Password client.includeLength = opt.IncludeLength client.userAgent = opt.UserAgent return &client, nil } // MakeRequest makes a request to the specified url func (client *httpClient) makeRequest(fullURL, cookie string) (*int, *int64, error) { req, err := http.NewRequest(http.MethodGet, fullURL, nil) if err != nil { return nil, nil, err } // add the context so we can easily cancel out req = req.WithContext(client.context) if cookie != "" { req.Header.Set("Cookie", cookie) } ua := fmt.Sprintf("gobuster %s", VERSION) if client.userAgent != "" { ua = client.userAgent } req.Header.Set("User-Agent", ua) if client.username != "" { req.SetBasicAuth(client.username, client.password) } resp, err := client.client.Do(req) if err != nil { if ue, ok := err.(*url.Error); ok { if strings.HasPrefix(ue.Err.Error(), "x509") { return nil, nil, fmt.Errorf("Invalid certificate: %v", ue.Err) } } return nil, nil, err } defer resp.Body.Close() var length *int64 if client.includeLength { length = new(int64) if resp.ContentLength <= 0 { body, err2 := ioutil.ReadAll(resp.Body) if err2 == nil { *length = int64(utf8.RuneCountInString(string(body))) } } else { *length = resp.ContentLength } } else { // DO NOT REMOVE! // absolutely needed so golang will reuse connections! _, err = io.Copy(ioutil.Discard, resp.Body) if err != nil { return nil, nil, err } } return &resp.StatusCode, length, nil } gobuster-2.0.1/libgobuster/http_test.go000066400000000000000000000012441334162345700202430ustar00rootroot00000000000000package libgobuster import ( "context" "testing" "github.com/h2non/gock" ) func TestMakeRequest(t *testing.T) { defer gock.Off() gock.New("http://server.com"). Get("/bar"). Reply(200). BodyString("test") o := NewOptions() c, err := newHTTPClient(context.Background(), o) if err != nil { t.Fatalf("Got Error: %v", err) } gock.InterceptClient(c.client) defer gock.RestoreClient(c.client) a, b, err := c.makeRequest("http://server.com/bar", "") if err != nil { t.Fatalf("Got Error: %v", err) } if *a != 200 { t.Fatalf("Invalid status returned: %d", a) } if b != nil && *b != int64(len("test")) { t.Fatalf("Invalid length returned: %d", b) } } gobuster-2.0.1/libgobuster/libgobuster.go000066400000000000000000000170701334162345700205520ustar00rootroot00000000000000package libgobuster import ( "bufio" "bytes" "context" "fmt" "net" "os" "strings" "sync" ) const ( // VERSION contains the current gobuster version VERSION = "2.0.1" ) // 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 http *httpClient WildcardIps stringSet context context.Context requestsExpected int requestsIssued int mu *sync.RWMutex plugin GobusterPlugin IsWildcard bool resultChan chan Result errorChan chan error } // GobusterPlugin is an interface which plugins must implement type GobusterPlugin interface { Setup(*Gobuster) error Process(*Gobuster, string) ([]Result, error) ResultToString(*Gobuster, *Result) (*string, error) } // NewGobuster returns a new Gobuster object func NewGobuster(c context.Context, opts *Options, plugin GobusterPlugin) (*Gobuster, error) { // validate given options multiErr := opts.validate() if multiErr != nil { return nil, multiErr } var g Gobuster g.WildcardIps = newStringSet() g.context = c g.Opts = opts h, err := newHTTPClient(c, opts) if err != nil { return nil, err } g.http = h g.plugin = plugin g.mu = new(sync.RWMutex) g.resultChan = make(chan Result) g.errorChan = make(chan error) return &g, nil } // Results returns a channel of Results func (g *Gobuster) Results() <-chan Result { return g.resultChan } // Errors returns a channel of errors func (g *Gobuster) Errors() <-chan error { return g.errorChan } func (g *Gobuster) incrementRequests() { g.mu.Lock() g.requestsIssued++ g.mu.Unlock() } // PrintProgress outputs the current wordlist progress to stderr func (g *Gobuster) PrintProgress() { if !g.Opts.Quiet && !g.Opts.NoProgress { g.mu.RLock() if g.Opts.Wordlist == "-" { fmt.Fprintf(os.Stderr, "\rProgress: %d", g.requestsIssued) // only print status if we already read in the wordlist } else if g.requestsExpected > 0 { fmt.Fprintf(os.Stderr, "\rProgress: %d / %d (%3.2f%%)", g.requestsIssued, g.requestsExpected, float32(g.requestsIssued)*100.0/float32(g.requestsExpected)) } g.mu.RUnlock() } } // ClearProgress removes the last status line from stderr func (g *Gobuster) ClearProgress() { fmt.Fprint(os.Stderr, resetTerminal()) } // GetRequest issues a GET request to the target and returns // the status code, length and an error func (g *Gobuster) GetRequest(url string) (*int, *int64, error) { return g.http.makeRequest(url, g.Opts.Cookies) } // DNSLookup looks up a domain via system default DNS servers func (g *Gobuster) DNSLookup(domain string) ([]string, error) { return net.LookupHost(domain) } // DNSLookupCname looks up a CNAME record via system default DNS servers func (g *Gobuster) DNSLookupCname(domain string) (string, error) { return net.LookupCNAME(domain) } func (g *Gobuster) worker(wordChan <-chan string, wg *sync.WaitGroup) { defer wg.Done() for { select { case <-g.context.Done(): return case word, ok := <-wordChan: // worker finished if !ok { return } g.incrementRequests() // Mode-specific processing res, err := g.plugin.Process(g, word) if err != nil { // do not exit and continue g.errorChan <- err continue } else { for _, r := range res { g.resultChan <- r } } } } } 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: %v", err) } lines, err := lineCounter(wordlist) if err != nil { return nil, fmt.Errorf("failed to get number of lines: %v", err) } g.requestsExpected = lines g.requestsIssued = 0 // rewind wordlist _, err = wordlist.Seek(0, 0) if err != nil { return nil, fmt.Errorf("failed to rewind wordlist: %v", err) } return bufio.NewScanner(wordlist), nil } // Start the busting of the website with the given // set of settings from the command line. func (g *Gobuster) Start() error { if err := g.plugin.Setup(g); 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(wordChan, &workerGroup) } scanner, err := g.getWordlist() if err != nil { return err } Scan: for scanner.Scan() { select { case <-g.context.Done(): break Scan default: word := strings.TrimSpace(scanner.Text()) // Skip "comment" (starts with #), as well as empty lines if !strings.HasPrefix(word, "#") && len(word) > 0 { wordChan <- word } } } close(wordChan) workerGroup.Wait() close(g.resultChan) close(g.errorChan) return nil } // GetConfigString returns the current config as a printable string func (g *Gobuster) GetConfigString() (string, error) { buf := &bytes.Buffer{} o := g.Opts if _, err := fmt.Fprintf(buf, "[+] Mode : %s\n", o.Mode); err != nil { return "", err } if _, err := fmt.Fprintf(buf, "[+] Url/Domain : %s\n", o.URL); err != nil { return "", err } if _, err := fmt.Fprintf(buf, "[+] Threads : %d\n", o.Threads); err != nil { return "", err } wordlist := "stdin (pipe)" if o.Wordlist != "-" { wordlist = o.Wordlist } if _, err := fmt.Fprintf(buf, "[+] Wordlist : %s\n", wordlist); err != nil { return "", err } if o.Mode == ModeDir { if _, err := fmt.Fprintf(buf, "[+] Status codes : %s\n", o.StatusCodesParsed.Stringify()); err != nil { return "", err } if o.Proxy != "" { if _, err := fmt.Fprintf(buf, "[+] Proxy : %s\n", o.Proxy); err != nil { return "", err } } if o.Cookies != "" { if _, err := fmt.Fprintf(buf, "[+] Cookies : %s\n", o.Cookies); err != nil { return "", err } } if o.UserAgent != "" { if _, err := fmt.Fprintf(buf, "[+] User Agent : %s\n", o.UserAgent); err != nil { return "", err } } if o.IncludeLength { if _, err := fmt.Fprintf(buf, "[+] Show length : true\n"); err != nil { return "", err } } if o.Username != "" { if _, err := fmt.Fprintf(buf, "[+] Auth User : %s\n", o.Username); err != nil { return "", err } } if len(o.Extensions) > 0 { if _, err := fmt.Fprintf(buf, "[+] Extensions : %s\n", o.ExtensionsParsed.Stringify()); err != nil { return "", err } } if o.UseSlash { if _, err := fmt.Fprintf(buf, "[+] Add Slash : true\n"); err != nil { return "", err } } if o.FollowRedirect { if _, err := fmt.Fprintf(buf, "[+] Follow Redir : true\n"); err != nil { return "", err } } if o.Expanded { if _, err := fmt.Fprintf(buf, "[+] Expanded : true\n"); err != nil { return "", err } } if o.NoStatus { if _, err := fmt.Fprintf(buf, "[+] No status : true\n"); err != nil { return "", err } } if o.Verbose { if _, err := fmt.Fprintf(buf, "[+] Verbose : true\n"); err != nil { return "", err } } if _, err := fmt.Fprintf(buf, "[+] Timeout : %s\n", o.Timeout.String()); err != nil { return "", err } } return strings.TrimSpace(buf.String()), nil } gobuster-2.0.1/libgobuster/options.go000066400000000000000000000103131334162345700177150ustar00rootroot00000000000000package libgobuster import ( "fmt" "os" "regexp" "strconv" "strings" "time" multierror "github.com/hashicorp/go-multierror" ) const ( // ModeDir represents -m dir ModeDir = "dir" // ModeDNS represents -m dns ModeDNS = "dns" ) // Options helds all options that can be passed to libgobuster type Options struct { Extensions string ExtensionsParsed stringSet Mode string Password string StatusCodes string StatusCodesParsed intSet Threads int URL string UserAgent string Username string Wordlist string Proxy string Cookies string Timeout time.Duration FollowRedirect bool IncludeLength bool NoStatus bool NoProgress bool Expanded bool Quiet bool ShowIPs bool ShowCNAME bool InsecureSSL bool WildcardForced bool Verbose bool UseSlash bool } // NewOptions returns a new initialized Options object func NewOptions() *Options { return &Options{ StatusCodesParsed: newIntSet(), ExtensionsParsed: newStringSet(), } } // Validate validates the given options func (opt *Options) validate() *multierror.Error { var errorList *multierror.Error if strings.ToLower(opt.Mode) != ModeDir && strings.ToLower(opt.Mode) != ModeDNS { errorList = multierror.Append(errorList, fmt.Errorf("Mode (-m): Invalid value: %s", opt.Mode)) } if opt.Threads < 0 { errorList = multierror.Append(errorList, fmt.Errorf("Threads (-t): Invalid value: %d", opt.Threads)) } if opt.Wordlist == "" { errorList = multierror.Append(errorList, fmt.Errorf("WordList (-w): Must be specified (use `-w -` for stdin)")) } else if opt.Wordlist == "-" { // STDIN } else if _, err := os.Stat(opt.Wordlist); os.IsNotExist(err) { errorList = multierror.Append(errorList, fmt.Errorf("Wordlist (-w): File does not exist: %s", opt.Wordlist)) } if opt.URL == "" { errorList = multierror.Append(errorList, fmt.Errorf("Url/Domain (-u): Must be specified")) } if opt.StatusCodes != "" { if err := opt.parseStatusCodes(); err != nil { errorList = multierror.Append(errorList, err) } } if opt.Extensions != "" { if err := opt.parseExtensions(); err != nil { errorList = multierror.Append(errorList, err) } } if opt.Mode == ModeDir { if !strings.HasSuffix(opt.URL, "/") { opt.URL = fmt.Sprintf("%s/", opt.URL) } if err := opt.validateDirMode(); err != nil { errorList = multierror.Append(errorList, err) } } return errorList } // ParseExtensions parses the extensions provided as a comma seperated list func (opt *Options) parseExtensions() error { if opt.Extensions == "" { return fmt.Errorf("invalid extension string provided") } exts := strings.Split(opt.Extensions, ",") for _, e := range exts { e = strings.TrimSpace(e) // remove leading . from extensions opt.ExtensionsParsed.Add(strings.TrimPrefix(e, ".")) } return nil } // ParseStatusCodes parses the status codes provided as a comma seperated list func (opt *Options) parseStatusCodes() error { if opt.StatusCodes == "" { return fmt.Errorf("invalid status code string provided") } for _, c := range strings.Split(opt.StatusCodes, ",") { c = strings.TrimSpace(c) i, err := strconv.Atoi(c) if err != nil { return fmt.Errorf("invalid status code given: %s", c) } opt.StatusCodesParsed.Add(i) } return nil } func (opt *Options) validateDirMode() error { // bail out if we are not in dir mode if opt.Mode != ModeDir { return nil } if !strings.HasPrefix(opt.URL, "http") { // check to see if a port was specified re := regexp.MustCompile(`^[^/]+:(\d+)`) match := re.FindStringSubmatch(opt.URL) if len(match) < 2 { // no port, default to http on 80 opt.URL = fmt.Sprintf("http://%s", opt.URL) } else { port, err := strconv.Atoi(match[1]) if err != nil || (port != 80 && port != 443) { return fmt.Errorf("url scheme not specified") } else if port == 80 { opt.URL = fmt.Sprintf("http://%s", opt.URL) } else { opt.URL = fmt.Sprintf("https://%s", opt.URL) } } } if opt.Username != "" && opt.Password == "" { return fmt.Errorf("username was provided but password is missing") } return nil } gobuster-2.0.1/libgobuster/options_test.go000066400000000000000000000052701334162345700207620ustar00rootroot00000000000000package libgobuster import ( "reflect" "testing" ) func TestNewOptions(t *testing.T) { t.Parallel() o := NewOptions() if o.StatusCodesParsed.Set == nil { t.Fatal("StatusCodesParsed not initialized") } if o.ExtensionsParsed.Set == nil { t.Fatal("ExtensionsParsed not initialized") } } func TestParseExtensions(t *testing.T) { t.Parallel() var tt = []struct { testName string Extensions string expectedExtensions stringSet expectedError string }{ {"Valid extensions", "php,asp,txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Spaces", "php, asp , txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Double extensions", "php,asp,txt,php,asp,txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Leading dot", ".php,asp,.txt", stringSet{Set: map[string]bool{"php": true, "asp": true, "txt": true}}, ""}, {"Empty string", "", newStringSet(), "invalid extension string provided"}, } for _, x := range tt { t.Run(x.testName, func(t *testing.T) { o := NewOptions() o.Extensions = x.Extensions err := o.parseExtensions() if x.expectedError != "" { if err.Error() != x.expectedError { t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error()) } } else if !reflect.DeepEqual(x.expectedExtensions, o.ExtensionsParsed) { t.Fatalf("Expected %v but got %v", x.expectedExtensions, o.ExtensionsParsed) } }) } } func TestParseStatusCodes(t *testing.T) { t.Parallel() var tt = []struct { testName string stringCodes string expectedCodes intSet expectedError string }{ {"Valid codes", "200,100,202", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Spaces", "200, 100 , 202", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Double codes", "200, 100, 202, 100", intSet{Set: map[int]bool{100: true, 200: true, 202: true}}, ""}, {"Invalid code", "200,AAA", newIntSet(), "invalid status code given: AAA"}, {"Invalid integer", "2000000000000000000000000000000", newIntSet(), "invalid status code given: 2000000000000000000000000000000"}, {"Empty string", "", newIntSet(), "invalid status code string provided"}, } for _, x := range tt { t.Run(x.testName, func(t *testing.T) { o := NewOptions() o.StatusCodes = x.stringCodes err := o.parseStatusCodes() if x.expectedError != "" { if err.Error() != x.expectedError { t.Fatalf("Expected error %q but got %q", x.expectedError, err.Error()) } } else if !reflect.DeepEqual(x.expectedCodes, o.StatusCodesParsed) { t.Fatalf("Expected %v but got %v", x.expectedCodes, o.StatusCodesParsed) } }) } } gobuster-2.0.1/libgobuster/output_reset.go000066400000000000000000000001351334162345700207650ustar00rootroot00000000000000// +build !windows package libgobuster func resetTerminal() string { return "\r\x1b[2K" } gobuster-2.0.1/libgobuster/output_reset_win.go000066400000000000000000000001271334162345700216430ustar00rootroot00000000000000// +build windows package libgobuster func resetTerminal() string { return "\r\r" } gobuster-2.0.1/libgobuster/result.go000066400000000000000000000005541334162345700175460ustar00rootroot00000000000000package libgobuster // Result represents a single gobuster result type Result struct { Entity string Status int Extra string Size *int64 } // ToString converts the Result to it's textual representation func (r *Result) ToString(g *Gobuster) (string, error) { s, err := g.plugin.ResultToString(g, r) if err != nil { return "", err } return *s, nil } gobuster-2.0.1/main.go000066400000000000000000000140201334162345700146240ustar00rootroot00000000000000package main //---------------------------------------------------- // 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. // //---------------------------------------------------- import ( "context" "flag" "fmt" "log" "os" "os/signal" "strings" "sync" "syscall" "time" "github.com/OJ/gobuster/gobusterdir" "github.com/OJ/gobuster/gobusterdns" "github.com/OJ/gobuster/libgobuster" "golang.org/x/crypto/ssh/terminal" ) func ruler() { fmt.Println("=====================================================") } func banner() { fmt.Printf("Gobuster v%s OJ Reeves (@TheColonial)\n", libgobuster.VERSION) } func resultWorker(g *libgobuster.Gobuster, filename string, wg *sync.WaitGroup) { defer wg.Done() var f *os.File var err error if filename != "" { f, err = os.Create(filename) if err != nil { log.Fatalf("error on creating output file: %v", err) } } for r := range g.Results() { s, err := r.ToString(g) if err != nil { log.Fatal(err) } if s != "" { g.ClearProgress() s = strings.TrimSpace(s) fmt.Println(s) if f != nil { err = writeToFile(f, s) if err != nil { log.Fatalf("error on writing output file: %v", err) } } } } } func errorWorker(g *libgobuster.Gobuster, wg *sync.WaitGroup) { defer wg.Done() for e := range g.Errors() { if !g.Opts.Quiet { g.ClearProgress() log.Printf("[!] %v", e) } } } func progressWorker(c context.Context, g *libgobuster.Gobuster) { tick := time.NewTicker(1 * time.Second) for { select { case <-tick.C: g.PrintProgress() case <-c.Done(): 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 %v", err) } return nil } func main() { var outputFilename string o := libgobuster.NewOptions() flag.IntVar(&o.Threads, "t", 10, "Number of concurrent threads") flag.StringVar(&o.Mode, "m", "dir", "Directory/File mode (dir) or DNS mode (dns)") flag.StringVar(&o.Wordlist, "w", "", "Path to the wordlist") flag.StringVar(&o.StatusCodes, "s", "200,204,301,302,307,403", "Positive status codes (dir mode only)") flag.StringVar(&outputFilename, "o", "", "Output file to write results to (defaults to stdout)") flag.StringVar(&o.URL, "u", "", "The target URL or Domain") flag.StringVar(&o.Cookies, "c", "", "Cookies to use for the requests (dir mode only)") flag.StringVar(&o.Username, "U", "", "Username for Basic Auth (dir mode only)") flag.StringVar(&o.Password, "P", "", "Password for Basic Auth (dir mode only)") flag.StringVar(&o.Extensions, "x", "", "File extension(s) to search for (dir mode only)") flag.StringVar(&o.UserAgent, "a", "", "Set the User-Agent string (dir mode only)") flag.StringVar(&o.Proxy, "p", "", "Proxy to use for requests [http(s)://host:port] (dir mode only)") flag.DurationVar(&o.Timeout, "to", 10*time.Second, "HTTP Timeout in seconds (dir mode only)") flag.BoolVar(&o.Verbose, "v", false, "Verbose output (errors)") flag.BoolVar(&o.ShowIPs, "i", false, "Show IP addresses (dns mode only)") flag.BoolVar(&o.ShowCNAME, "cn", false, "Show CNAME records (dns mode only, cannot be used with '-i' option)") flag.BoolVar(&o.FollowRedirect, "r", false, "Follow redirects") flag.BoolVar(&o.Quiet, "q", false, "Don't print the banner and other noise") flag.BoolVar(&o.Expanded, "e", false, "Expanded mode, print full URLs") flag.BoolVar(&o.NoStatus, "n", false, "Don't print status codes") flag.BoolVar(&o.IncludeLength, "l", false, "Include the length of the body in the output (dir mode only)") flag.BoolVar(&o.UseSlash, "f", false, "Append a forward-slash to each directory request (dir mode only)") flag.BoolVar(&o.WildcardForced, "fw", false, "Force continued operation when wildcard found") flag.BoolVar(&o.InsecureSSL, "k", false, "Skip SSL certificate verification") flag.BoolVar(&o.NoProgress, "np", false, "Don't display progress") flag.Parse() // Prompt for PW if not provided if o.Username != "" && o.Password == "" { fmt.Printf("[?] Auth Password: ") passBytes, err := terminal.ReadPassword(int(syscall.Stdin)) // 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 { log.Fatal("[!] Auth username given but reading of password failed") } o.Password = string(passBytes) } ctx, cancel := context.WithCancel(context.Background()) defer cancel() var plugin libgobuster.GobusterPlugin switch o.Mode { case libgobuster.ModeDir: plugin = gobusterdir.GobusterDir{} case libgobuster.ModeDNS: plugin = gobusterdns.GobusterDNS{} } gobuster, err := libgobuster.NewGobuster(ctx, o, plugin) if err != nil { log.Fatalf("[!] %v", err) } if !o.Quiet { fmt.Println("") ruler() banner() ruler() c, err := gobuster.GetConfigString() if err != nil { log.Fatalf("error on creating config string: %v", err) } fmt.Println(c) ruler() log.Println("Starting gobuster") ruler() } signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt) go func() { for range signalChan { // caught CTRL+C if !gobuster.Opts.Quiet { fmt.Println("\n[!] Keyboard interrupt detected, terminating.") } cancel() } }() var wg sync.WaitGroup wg.Add(2) go errorWorker(gobuster, &wg) go resultWorker(gobuster, outputFilename, &wg) if !o.Quiet && !o.NoProgress { go progressWorker(ctx, gobuster) } if err := gobuster.Start(); err != nil { log.Printf("[!] %v", err) } else { // call cancel func to free ressources and stop progressFunc cancel() // wait for all output funcs to finish wg.Wait() } if !o.Quiet { gobuster.ClearProgress() ruler() log.Println("Finished") ruler() } } gobuster-2.0.1/make.bat000066400000000000000000000023301334162345700147570ustar00rootroot00000000000000@echo off SET ARG=%1 SET TARGET=.\build IF "%ARG%"=="test" ( go test -v -race ./... echo Done. GOTO Done ) IF "%ARG%"=="clean" ( del /F /Q %TARGET%\*.* echo Done. GOTO Done ) IF "%ARG%"=="windows" ( CALL :Windows GOTO Done ) IF "%ARG%"=="darwin" ( CALL :Darwin GOTO Done ) IF "%ARG%"=="linux" ( CALL :Linux GOTO Done ) IF "%ARG%"=="all" ( CALL :Darwin CALL :Linux CALL :Windows GOTO Done ) IF "%ARG%"=="" ( go build -o .\gobuster.exe GOTO Done ) GOTO Done :Darwin set GOOS=darwin set GOARCH=amd64 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOOS%-%GOARCH% set GOARCH=386 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOOS%-%GOARCH% echo Done. EXIT /B 0 :Linux set GOOS=linux set GOARCH=amd64 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOOS%-%GOARCH% set GOARCH=386 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOOS%-%GOARCH% echo Done. EXIT /B 0 :Windows set GOOS=windows set GOARCH=amd64 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOARCH%.exe set GOARCH=386 echo Building for %GOOS% %GOARCH% ... go build -o %TARGET%\gobuster-%GOARCH%.exe echo Done. EXIT /B 0 :Done