mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-24 05:41:12 -03:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0898914d19 | ||
|
|
d97d2714ce | ||
|
|
c1bbd10f51 | ||
|
|
cda1628aa6 | ||
|
|
9e08766c07 | ||
|
|
b1e4c3fd6f | ||
|
|
08abb044e3 | ||
|
|
bc4893970d | ||
|
|
fae6f96f3a | ||
|
|
a627841058 | ||
|
|
b5c640cc4f | ||
|
|
5285f22dae | ||
|
|
96a4fb1139 | ||
|
|
95aca72670 | ||
|
|
39f8f38204 | ||
|
|
db5509cb52 | ||
|
|
231752194f | ||
|
|
f64f02135e | ||
|
|
db5e1e2e2d | ||
|
|
f649da359f | ||
|
|
6e981e6d3a | ||
|
|
12b46a44e1 | ||
|
|
e35f86876d | ||
|
|
6fe5ae0d0c | ||
|
|
dc89f3b5aa | ||
|
|
5918554754 | ||
|
|
39241594ae | ||
|
|
665564bbfe | ||
|
|
ffed3820a5 | ||
|
|
254f502ed3 | ||
|
|
d3ddefa0b7 | ||
|
|
acf16c92cd | ||
|
|
2d67336b86 | ||
|
|
9db0dc505b | ||
|
|
702cc8f18e | ||
|
|
737d347121 | ||
|
|
a4b7a8a8e6 | ||
|
|
afacb13787 | ||
|
|
610379c6a9 | ||
|
|
1bb132f157 | ||
|
|
c9601d4fe9 | ||
|
|
fae404ff9a | ||
|
|
ab5ff1b2e0 | ||
|
|
045719b25a | ||
|
|
154d8ae408 | ||
|
|
8bebc7b81d | ||
|
|
204b90e1fa | ||
|
|
6ceba1170f | ||
|
|
6f7e4564e7 | ||
|
|
e8041df0cd | ||
|
|
1c364b0a21 | ||
|
|
6caa6b864c | ||
|
|
962e22010f | ||
|
|
fcc27f6770 | ||
|
|
404b231c67 | ||
|
|
43e5ad14c9 | ||
|
|
52d05e613c | ||
|
|
b84ee91c2e | ||
|
|
81456c7074 | ||
|
|
5d564c5f28 | ||
|
|
21eb70bdfa | ||
|
|
48b58664c7 | ||
|
|
c85cf21d4f | ||
|
|
27f649d164 | ||
|
|
4f53bc7b49 | ||
|
|
9fa963bb8c | ||
|
|
0d6ae79c46 |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -41,6 +41,9 @@ jobs:
|
||||
use-cross: true
|
||||
command: build
|
||||
args: --release --target=${{ matrix.target }}
|
||||
- name: Strip symbols from binary
|
||||
run: |
|
||||
strip -s ${{ matrix.path }}
|
||||
- name: Build tar.gz for homebrew installs
|
||||
if: matrix.type == 'ubuntu-x64'
|
||||
run: |
|
||||
@@ -83,6 +86,9 @@ jobs:
|
||||
use-cross: true
|
||||
command: build
|
||||
args: --release --target=x86_64-apple-darwin
|
||||
- name: Strip symbols from binary
|
||||
run: |
|
||||
strip -u -r target/x86_64-apple-darwin/release/feroxbuster
|
||||
- name: Build tar.gz for homebrew installs
|
||||
run: |
|
||||
tar czf x86_64-macos-feroxbuster.tar.gz -C target/x86_64-apple-darwin/release feroxbuster
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "1.1.0"
|
||||
version = "1.4.1"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
@@ -19,18 +19,20 @@ futures = { version = "0.3"}
|
||||
tokio = { version = "0.2", features = ["full"] }
|
||||
tokio-util = {version = "0.3", features = ["codec"]}
|
||||
log = "0.4"
|
||||
env_logger = "0.7"
|
||||
env_logger = "0.8"
|
||||
reqwest = { version = "0.10", features = ["socks"] }
|
||||
clap = "2"
|
||||
lazy_static = "1.4"
|
||||
toml = "0.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
indicatif = "0.15"
|
||||
console = "0.12"
|
||||
openssl = { version = "0.10", features = ["vendored"] }
|
||||
dirs = "3.0"
|
||||
regex = "1"
|
||||
crossterm = "0.18"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1"
|
||||
|
||||
187
README.md
187
README.md
@@ -71,6 +71,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
- [ferox-config.toml](#ferox-configtoml)
|
||||
- [Command Line Parsing](#command-line-parsing)
|
||||
- [Example Usage](#-example-usage)
|
||||
- [Pause and Resume Scans (new in `v1.4.0`)](#pause-and-resume-scans-new-in-v140)
|
||||
- [Multiple Values](#multiple-values)
|
||||
- [Extract Links from Response Body (new in `v1.1.0`)](#extract-links-from-response-body-new-in-v110)
|
||||
- [Include Headers](#include-headers)
|
||||
@@ -79,7 +80,12 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
- [Proxy traffic through Burp](#proxy-traffic-through-burp)
|
||||
- [Proxy traffic through a SOCKS proxy](#proxy-traffic-through-a-socks-proxy)
|
||||
- [Pass auth token via query parameter](#pass-auth-token-via-query-parameter)
|
||||
- [Limit Total Number of Concurrent Scans (new in `v1.2.0`)](#limit-total-number-of-concurrent-scans-new-in-v120)
|
||||
- [Filter Response by Status Code (new in `v1.3.0`)](#filter-response-by-status-code--new-in-v130)
|
||||
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
||||
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
||||
- [No file descriptors available](#no-file-descriptors-available)
|
||||
- [Progress bars print one line at a time](#progress-bars-print-one-line-at-a-time)
|
||||
|
||||
## 💿 Installation
|
||||
|
||||
@@ -87,28 +93,9 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
||||
|
||||
Releases for multiple architectures can be found in the [Releases](https://github.com/epi052/feroxbuster/releases) section. The latest release for each of the following systems can be downloaded and executed as shown below.
|
||||
|
||||
#### Linux x86
|
||||
#### Linux (32 and 64-bit) & MacOS
|
||||
```
|
||||
curl -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86-linux-feroxbuster.zip
|
||||
unzip x86-linux-feroxbuster.zip
|
||||
chmod +x ./feroxbuster
|
||||
./feroxbuster -V
|
||||
```
|
||||
#### Linux x86_64
|
||||
|
||||
```
|
||||
curl -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-linux-feroxbuster.zip
|
||||
unzip x86_64-linux-feroxbuster.zip
|
||||
chmod +x ./feroxbuster
|
||||
./feroxbuster -V
|
||||
```
|
||||
|
||||
#### MacOS x86_64
|
||||
```
|
||||
curl -sLO https://github.com/epi052/feroxbuster/releases/latest/download/x86_64-macos-feroxbuster.zip
|
||||
unzip x86_64-macos-feroxbuster.zip
|
||||
chmod +x ./feroxbuster
|
||||
./feroxbuster -V
|
||||
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/master/install-nix.sh | bash
|
||||
```
|
||||
|
||||
#### Windows x86
|
||||
@@ -235,8 +222,9 @@ Configuration begins with with the following built-in default values baked into
|
||||
- wordlist: `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
|
||||
- threads: `50`
|
||||
- verbosity: `0` (no logging enabled)
|
||||
- statuscodes: `200 204 301 302 307 308 401 403 405`
|
||||
- useragent: `feroxbuster/VERSION`
|
||||
- scan_limit: `0` (no limit imposed on concurrent scans)
|
||||
- status_codes: `200 204 301 302 307 308 401 403 405`
|
||||
- user_agent: `feroxbuster/VERSION`
|
||||
- recursion depth: `4`
|
||||
- auto-filter wildcards - `true`
|
||||
- output: `stdout`
|
||||
@@ -286,24 +274,26 @@ A pre-made configuration file with examples of all available settings can be fou
|
||||
# Any setting used here can be overridden by the corresponding command line option/argument
|
||||
#
|
||||
# wordlist = "/wordlists/jhaddix/all.txt"
|
||||
# statuscodes = [200, 500]
|
||||
# status_codes = [200, 500]
|
||||
# filter_status = [301]
|
||||
# threads = 1
|
||||
# timeout = 5
|
||||
# proxy = "http://127.0.0.1:8080"
|
||||
# verbosity = 1
|
||||
# scan_limit = 6
|
||||
# quiet = true
|
||||
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
||||
# useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||
# redirects = true
|
||||
# insecure = true
|
||||
# extensions = ["php", "html"]
|
||||
# norecursion = true
|
||||
# addslash = true
|
||||
# no_recursion = true
|
||||
# add_slash = true
|
||||
# stdin = true
|
||||
# dontfilter = true
|
||||
# dont_filter = true
|
||||
# extract_links = true
|
||||
# depth = 1
|
||||
# sizefilters = [5174]
|
||||
# filter_size = [5174]
|
||||
# queries = [["name","value"], ["rick", "astley"]]
|
||||
|
||||
# headers can be specified on multiple lines or as an inline table
|
||||
@@ -328,13 +318,13 @@ USAGE:
|
||||
feroxbuster [FLAGS] [OPTIONS] --url <URL>...
|
||||
|
||||
FLAGS:
|
||||
-f, --addslash Append / to each request
|
||||
-D, --dontfilter Don't auto-filter wildcard responses
|
||||
-f, --add-slash Append / to each request
|
||||
-D, --dont-filter Don't auto-filter wildcard responses
|
||||
-e, --extract-links Extract links from response body (html, javascript, etc...); make new requests based on
|
||||
findings (default: false)
|
||||
-h, --help Prints help information
|
||||
-k, --insecure Disables TLS certificate validation
|
||||
-n, --norecursion Do not scan recursively
|
||||
-n, --no-recursion Do not scan recursively
|
||||
-q, --quiet Only print URLs; Don't print status codes, response size, running config, etc...
|
||||
-r, --redirects Follow redirects
|
||||
--stdin Read url(s) from STDIN
|
||||
@@ -344,21 +334,30 @@ FLAGS:
|
||||
OPTIONS:
|
||||
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
||||
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
||||
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -S 401)
|
||||
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
||||
-o, --output <FILE> Output file to write results to (default: stdout)
|
||||
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
|
||||
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
|
||||
-S, --sizefilter <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||
-s, --statuscodes <STATUS_CODE>... Status Codes of interest (default: 200 204 301 302 307 308 401 403 405)
|
||||
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
|
||||
-s, --status-codes <STATUS_CODE>... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401
|
||||
403 405)
|
||||
-t, --threads <THREADS> Number of concurrent threads (default: 50)
|
||||
-T, --timeout <SECONDS> Number of seconds before a request times out (default: 7)
|
||||
-u, --url <URL>... The target URL(s) (required, unless --stdin used)
|
||||
-a, --useragent <USER_AGENT> Sets the User-Agent (default: feroxbuster/VERSION)
|
||||
-a, --user-agent <USER_AGENT> Sets the User-Agent (default: feroxbuster/VERSION)
|
||||
-w, --wordlist <FILE> Path to the wordlist
|
||||
```
|
||||
|
||||
## 🧰 Example Usage
|
||||
|
||||
### Pause and Resume Scans (new in `v1.4.0`)
|
||||
|
||||
Scans can be paused and resumed by pressing the ENTER key (shown below)
|
||||
|
||||

|
||||
|
||||
### Multiple Values
|
||||
|
||||
Options that take multiple values are very flexible. Consider the following ways of specifying extensions:
|
||||
@@ -397,10 +396,21 @@ Example request/response with `--extract-links` enabled:
|
||||
./feroxbuster -u http://127.1 --extract-links
|
||||
```
|
||||
|
||||
Here's a comparison of a wordlist-only scan vs `--extract-links` using [Feline](https://www.hackthebox.eu/home/machines/profile/274) from Hack the Box:
|
||||
|
||||
Wordlist only
|
||||
|
||||

|
||||
|
||||
With `--extract-links`
|
||||
|
||||

|
||||
|
||||
|
||||
### IPv6, non-recursive scan with INFO-level logging enabled
|
||||
|
||||
```
|
||||
./feroxbuster -u http://[::1] --norecursion -vv
|
||||
./feroxbuster -u http://[::1] --no-recursion -vv
|
||||
```
|
||||
|
||||
### Read urls from STDIN; pipe only resulting urls out to another tool
|
||||
@@ -421,12 +431,33 @@ cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | f
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
```
|
||||
|
||||
### Pass auth token via query parameter
|
||||
### Pass auth token via query parameter
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
|
||||
```
|
||||
|
||||
### Limit Total Number of Concurrent Scans (new in `v1.2.0`)
|
||||
|
||||
Limit the number of scans permitted to run at any given time. Recursion will still identify new directories, but newly
|
||||
discovered directories can only begin scanning when the total number of active scans drops below the value passed to
|
||||
`--scan-limit`.
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --scan-limit 2
|
||||
```
|
||||
|
||||

|
||||
|
||||
### Filter Response by Status Code (new in `v1.3.0`)
|
||||
|
||||
Version 1.3.0 included an overhaul to the filtering system which will allow for a wide array of filters to be added
|
||||
with minimal effort. The first such filter is a Status Code Filter. As responses come back from the scanned server,
|
||||
each one is checked against a list of known filters and either displayed or not according to which filters are set.
|
||||
|
||||
```
|
||||
./feroxbuster -u http://127.1 --filter-status 301
|
||||
```
|
||||
|
||||
## 🧐 Comparison w/ Similar Tools
|
||||
|
||||
@@ -450,7 +481,7 @@ a few of the use-cases in which feroxbuster may be a better fit:
|
||||
|------------------------------------------------------------------|---|---|---|
|
||||
| fast | ✔ | ✔ | ✔ |
|
||||
| easy to use | ✔ | ✔ | |
|
||||
| blacklist status codes (in addition to whitelist) | | ✔ | ✔ |
|
||||
| filter out responses by status code (new in `v1.3.0`) | ✔ | ✔ | ✔ |
|
||||
| allows recursion | ✔ | | ✔ |
|
||||
| can specify query parameters | ✔ | | ✔ |
|
||||
| SOCKS proxy support | ✔ | | |
|
||||
@@ -470,3 +501,83 @@ came across rustbuster when I was naming my tool (😢). I don't have any experi
|
||||
be able to do POST requests with an HTTP body, has SOCKS support, and has an 8.3 shortname scanner (in addition to vhost
|
||||
dns, directory, etc...). In short, it definitely looks interesting and may be what you're looking for as it has some
|
||||
capability I haven't seen in similar tools.
|
||||
|
||||
## 🤯 Common Problems/Issues (FAQ)
|
||||
|
||||
### No file descriptors available
|
||||
|
||||
Why do I get a bunch of `No file descriptors available (os error 24)` errors?
|
||||
|
||||
---
|
||||
|
||||
There are a few potential causes of this error. The simplest is that your operating system sets an open file limit that is aggressively low. Through personal testing, I've found that `4096` is a reasonable open file limit (this will vary based on your exact setup).
|
||||
|
||||
There are quite a few options to solve this particular problem, of which a handful are shown below.
|
||||
|
||||
#### Increase the Number of Open Files
|
||||
|
||||
We'll start by increasing the number of open files the OS allows. On my Kali install, the default was `1024`, and I know some MacOS installs use `256` 😕.
|
||||
|
||||
##### Edit `/etc/security/limits.conf`
|
||||
|
||||
One option to up the limit is to edit `/etc/security/limits.conf` so that it includes the two lines below.
|
||||
|
||||
- `*` represents all users
|
||||
- `hard` and `soft` indicate the hard and soft limits for the OS
|
||||
- `nofile` is the number of open files option.
|
||||
|
||||
```
|
||||
/etc/security/limits.conf
|
||||
-------------------------
|
||||
...
|
||||
* soft nofile 4096
|
||||
* hard nofile 8192
|
||||
...
|
||||
```
|
||||
|
||||
##### Use `ulimit` directly
|
||||
|
||||
A faster option, that is **not** persistent, is to simply use the `ulimit` command to change the setting.
|
||||
|
||||
```
|
||||
ulimit -n 4096
|
||||
```
|
||||
|
||||
#### Additional Tweaks (may not be needed)
|
||||
|
||||
If you still find yourself hitting the file limit with the above changes, there are a few additional tweaks that may help.
|
||||
|
||||
> This section was shamelessly stolen from this [stackoverflow answer](https://stackoverflow.com/a/3923785). More information is included in that post and is recommended reading if you end up needing to use this section.
|
||||
|
||||
✨ Special thanks to HTB user [@sparkla](https://www.hackthebox.eu/home/users/profile/221599) for their help with identifying these additional tweaks ✨
|
||||
|
||||
##### Increase the ephemeral port range, and decrease the tcp_fin_timeout.
|
||||
|
||||
The ephermal port range defines the maximum number of outbound sockets a host can create from a particular I.P. address. The fin_timeout defines the minimum time these sockets will stay in TIME_WAIT state (unusable after being used once). Usual system defaults are
|
||||
|
||||
- `net.ipv4.ip_local_port_range = 32768 61000`
|
||||
- `net.ipv4.tcp_fin_timeout = 60`
|
||||
|
||||
This basically means your system cannot consistently guarantee more than `(61000 - 32768) / 60 = 470` sockets per second.
|
||||
|
||||
```
|
||||
sudo sysctl net.ipv4.ip_local_port_range="15000 61000"
|
||||
sudo sysctl net.ipv4.tcp_fin_timeout=30
|
||||
```
|
||||
|
||||
##### Allow socket reuse while in a `TIME_WAIT` status
|
||||
|
||||
This allows fast cycling of sockets in time_wait state and re-using them. Make sure to read post [Coping with the TCP TIME-WAIT](https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux) from Vincent Bernat to understand the implications.
|
||||
|
||||
```
|
||||
sudo sysctl net.ipv4.tcp_tw_reuse=1
|
||||
```
|
||||
|
||||
### Progress bars print one line at a time
|
||||
|
||||
`feroxbuster` needs a terminal width of at least the size of what's being printed in order to do progress bar printing correctly. If your width is too small, you may see output like what's shown below.
|
||||
|
||||

|
||||
|
||||
If you can, simply make the terminal wider and rerun. If you're unable to make your terminal wider
|
||||
consider using `-q` to suppress the progress bars.
|
||||
@@ -8,24 +8,26 @@
|
||||
# Any setting used here can be overridden by the corresponding command line option/argument
|
||||
#
|
||||
# wordlist = "/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt"
|
||||
# statuscodes = [200, 500]
|
||||
# status_codes = [200, 500]
|
||||
# filter_status = [301]
|
||||
# threads = 1
|
||||
# timeout = 5
|
||||
# proxy = "http://127.0.0.1:8080"
|
||||
# verbosity = 1
|
||||
# scan_limit = 6
|
||||
# quiet = true
|
||||
# output = "/targets/ellingson_mineral_company/gibson.txt"
|
||||
# useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||
# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0"
|
||||
# redirects = true
|
||||
# insecure = true
|
||||
# extensions = ["php", "html"]
|
||||
# norecursion = true
|
||||
# addslash = true
|
||||
# no_recursion = true
|
||||
# add_slash = true
|
||||
# stdin = true
|
||||
# dontfilter = true
|
||||
# dont_filter = true
|
||||
# extract_links = true
|
||||
# depth = 1
|
||||
# sizefilters = [5174]
|
||||
# filter_size = [5174]
|
||||
# queries = [["name","value"], ["rick", "astley"]]
|
||||
|
||||
# headers can be specified on multiple lines or as an inline table
|
||||
|
||||
BIN
img/extract-scan-cmp-normal.gif
Normal file
BIN
img/extract-scan-cmp-normal.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 860 KiB |
BIN
img/limit-demo.gif
Normal file
BIN
img/limit-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 725 KiB |
BIN
img/normal-scan-cmp-extract.gif
Normal file
BIN
img/normal-scan-cmp-extract.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 640 KiB |
BIN
img/pause-resume-demo.gif
Normal file
BIN
img/pause-resume-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 MiB |
BIN
img/small-term.png
Normal file
BIN
img/small-term.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
56
install-nix.sh
Executable file
56
install-nix.sh
Executable file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
BASE_URL=https://github.com/epi052/feroxbuster/releases/latest/download
|
||||
|
||||
MAC_ZIP=x86_64-macos-feroxbuster.zip
|
||||
MAC_URL="${BASE_URL}/${MAC_ZIP}"
|
||||
|
||||
LIN32_ZIP=x86-linux-feroxbuster.zip
|
||||
LIN32_URL="${BASE_URL}/${LIN32_ZIP}"
|
||||
|
||||
LIN64_ZIP=x86_64-linux-feroxbuster.zip
|
||||
LIN64_URL="${BASE_URL}/${LIN64_ZIP}"
|
||||
|
||||
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
|
||||
|
||||
echo "[+] Installing feroxbuster!"
|
||||
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
echo "[=] Found MacOS, downloading from ${MAC_URL}"
|
||||
|
||||
curl -sLO "${MAC_URL}"
|
||||
unzip -o "${MAC_ZIP}" > /dev/null
|
||||
rm "${MAC_ZIP}"
|
||||
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
|
||||
if [[ $(getconf LONG_BIT) == 32 ]]; then
|
||||
echo "[=] Found 32-bit Linux, downloading from ${LIN32_URL}"
|
||||
|
||||
curl -sLO "${LIN32_URL}"
|
||||
unzip -o "${LIN32_ZIP}" > /dev/null
|
||||
rm "${LIN32_ZIP}"
|
||||
else
|
||||
echo "[=] Found 64-bit Linux, downloading from ${LIN64_URL}"
|
||||
|
||||
curl -sLO "${LIN64_URL}"
|
||||
unzip -o "${LIN64_ZIP}" > /dev/null
|
||||
rm "${LIN64_ZIP}"
|
||||
fi
|
||||
|
||||
echo "[=] Installing Noto Emoji Font"
|
||||
mkdir -p ~/.fonts
|
||||
pushd ~/.fonts 2>&1 >/dev/null
|
||||
|
||||
curl -sLO "${EMOJI_URL}"
|
||||
|
||||
fc-cache -f -v >/dev/null
|
||||
|
||||
popd 2>&1 >/dev/null
|
||||
echo "[+] Noto Emoji Font installed"
|
||||
fi
|
||||
|
||||
chmod +x ./feroxbuster
|
||||
|
||||
echo "[+] Installed feroxbuster version $(./feroxbuster -V)"
|
||||
|
||||
|
||||
|
||||
494
src/banner.rs
494
src/banner.rs
@@ -1,4 +1,9 @@
|
||||
use crate::{config::Configuration, utils::status_colorizer, VERSION};
|
||||
use crate::config::{Configuration, CONFIGURATION};
|
||||
use crate::utils::{make_request, status_colorizer};
|
||||
use console::style;
|
||||
use reqwest::{Client, Url};
|
||||
use serde_json::Value;
|
||||
use std::io::Write;
|
||||
|
||||
/// macro helper to abstract away repetitive string formatting
|
||||
macro_rules! format_banner_entry_helper {
|
||||
@@ -40,250 +45,581 @@ macro_rules! format_banner_entry {
|
||||
};
|
||||
}
|
||||
|
||||
/// Url used to query github's api; specifically used to look for the latest tagged release name
|
||||
const UPDATE_URL: &str = "https://api.github.com/repos/epi052/feroxbuster/releases/latest";
|
||||
|
||||
/// Simple enum to hold three different update states
|
||||
#[derive(Debug)]
|
||||
enum UpdateStatus {
|
||||
/// this version and latest release are the same
|
||||
UpToDate,
|
||||
|
||||
/// this version and latest release are not the same
|
||||
OutOfDate,
|
||||
|
||||
/// some error occurred during version check
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Makes a request to the given url, expecting to receive a JSON response that contains a field
|
||||
/// named `tag_name` that holds a value representing the latest tagged release of this tool.
|
||||
///
|
||||
/// ex: v1.1.0
|
||||
///
|
||||
/// Returns `UpdateStatus`
|
||||
async fn needs_update(client: &Client, url: &str, bin_version: &str) -> UpdateStatus {
|
||||
log::trace!("enter: needs_update({:?}, {})", client, url);
|
||||
|
||||
let unknown = UpdateStatus::Unknown;
|
||||
|
||||
let api_url = match Url::parse(url) {
|
||||
Ok(url) => url,
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
log::trace!("exit: needs_update -> {:?}", unknown);
|
||||
return unknown;
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(response) = make_request(&client, &api_url).await {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
let json_response: Value = serde_json::from_str(&body).unwrap_or_default();
|
||||
|
||||
if json_response.is_null() {
|
||||
// unwrap_or_default above should result in a null value for the json_response variable
|
||||
log::error!("Could not parse JSON from response body");
|
||||
log::trace!("exit: needs_update -> {:?}", unknown);
|
||||
return unknown;
|
||||
}
|
||||
|
||||
let latest_version = match json_response["tag_name"].as_str() {
|
||||
Some(tag) => tag.trim_start_matches('v'),
|
||||
None => {
|
||||
log::error!("Could not get version field from JSON response");
|
||||
log::debug!("{}", json_response);
|
||||
log::trace!("exit: needs_update -> {:?}", unknown);
|
||||
return unknown;
|
||||
}
|
||||
};
|
||||
|
||||
// if we've gotten this far, we have a string in the form of X.X.X where X is a number
|
||||
// all that's left is to compare the current version with the version found above
|
||||
|
||||
return if latest_version == bin_version {
|
||||
// there's really only two possible outcomes if we accept that the tag conforms to
|
||||
// the X.X.X pattern:
|
||||
// 1. the version strings match, meaning we're up to date
|
||||
// 2. the version strings do not match, meaning we're out of date
|
||||
//
|
||||
// except for developers working on this code, nobody should ever be in a situation
|
||||
// where they have a version greater than the latest tagged release
|
||||
log::trace!("exit: needs_update -> UpdateStatus::UpToDate");
|
||||
UpdateStatus::UpToDate
|
||||
} else {
|
||||
log::trace!("exit: needs_update -> UpdateStatus::OutOfDate");
|
||||
UpdateStatus::OutOfDate
|
||||
};
|
||||
}
|
||||
|
||||
log::trace!("exit: needs_update -> {:?}", unknown);
|
||||
unknown
|
||||
}
|
||||
|
||||
/// Prints the banner to stdout.
|
||||
///
|
||||
/// Only prints those settings which are either always present, or passed in by the user.
|
||||
pub fn initialize(targets: &[String], config: &Configuration) {
|
||||
pub async fn initialize<W>(targets: &[String], config: &Configuration, version: &str, mut writer: W)
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
let artwork = format!(
|
||||
r#"
|
||||
___ ___ __ __ __ __ __ ___
|
||||
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
|
||||
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
|
||||
by Ben "epi" Risher {} ver: {}"#,
|
||||
'\u{1F913}', VERSION
|
||||
'\u{1F913}', version
|
||||
);
|
||||
|
||||
let status = needs_update(&CONFIGURATION.client, UPDATE_URL, version).await;
|
||||
|
||||
let top = "───────────────────────────┬──────────────────────";
|
||||
let addl_section = "──────────────────────────────────────────────────";
|
||||
let bottom = "───────────────────────────┴──────────────────────";
|
||||
|
||||
eprintln!("{}", artwork);
|
||||
eprintln!("{}", top);
|
||||
writeln!(&mut writer, "{}", artwork).unwrap_or_default();
|
||||
writeln!(&mut writer, "{}", top).unwrap_or_default();
|
||||
|
||||
// begin with always printed items
|
||||
for target in targets {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1F3af}", "Target Url", target)
|
||||
); // 🎯
|
||||
)
|
||||
.unwrap_or_default(); // 🎯
|
||||
}
|
||||
|
||||
let mut codes = vec![];
|
||||
|
||||
for code in &config.statuscodes {
|
||||
for code in &config.status_codes {
|
||||
codes.push(status_colorizer(&code.to_string()))
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1F680}", "Threads", config.threads)
|
||||
); // 🚀
|
||||
)
|
||||
.unwrap_or_default(); // 🚀
|
||||
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4d6}", "Wordlist", config.wordlist)
|
||||
); // 📖
|
||||
)
|
||||
.unwrap_or_default(); // 📖
|
||||
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!(
|
||||
"\u{1F197}",
|
||||
"Status Codes",
|
||||
format!("[{}]", codes.join(", "))
|
||||
)
|
||||
); // 🆗
|
||||
)
|
||||
.unwrap_or_default(); // 🆗
|
||||
|
||||
eprintln!(
|
||||
if !config.filter_status.is_empty() {
|
||||
// exception here for optional print due to me wanting the allows and denys to be printed
|
||||
// one after the other
|
||||
let mut code_filters = vec![];
|
||||
|
||||
for code in &config.filter_status {
|
||||
code_filters.push(status_colorizer(&code.to_string()))
|
||||
}
|
||||
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!(
|
||||
"\u{1f5d1}",
|
||||
"Status Code Filters",
|
||||
format!("[{}]", code_filters.join(", "))
|
||||
)
|
||||
)
|
||||
.unwrap_or_default(); // 🗑
|
||||
}
|
||||
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4a5}", "Timeout (secs)", config.timeout)
|
||||
); // 💥
|
||||
)
|
||||
.unwrap_or_default(); // 💥
|
||||
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1F9a1}", "User-Agent", config.useragent)
|
||||
); // 🦡
|
||||
format_banner_entry!("\u{1F9a1}", "User-Agent", config.user_agent)
|
||||
)
|
||||
.unwrap_or_default(); // 🦡
|
||||
|
||||
// followed by the maybe printed or variably displayed values
|
||||
if !config.config.is_empty() {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f489}", "Config File", config.config)
|
||||
); // 💉
|
||||
)
|
||||
.unwrap_or_default(); // 💉
|
||||
}
|
||||
|
||||
if !config.proxy.is_empty() {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f48e}", "Proxy", config.proxy)
|
||||
); // 💎
|
||||
)
|
||||
.unwrap_or_default(); // 💎
|
||||
}
|
||||
|
||||
if !config.headers.is_empty() {
|
||||
for (name, value) in &config.headers {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f92f}", "Header", name, value)
|
||||
); // 🤯
|
||||
)
|
||||
.unwrap_or_default(); // 🤯
|
||||
}
|
||||
}
|
||||
|
||||
if !config.sizefilters.is_empty() {
|
||||
for filter in &config.sizefilters {
|
||||
eprintln!(
|
||||
if !config.filter_size.is_empty() {
|
||||
for filter in &config.filter_size {
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4a2}", "Size Filter", filter)
|
||||
); // 💢
|
||||
)
|
||||
.unwrap_or_default(); // 💢
|
||||
}
|
||||
}
|
||||
|
||||
if config.extract_links {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1F50E}", "Extract Links", config.extract_links)
|
||||
); // 🔎
|
||||
)
|
||||
.unwrap_or_default(); // 🔎
|
||||
}
|
||||
|
||||
if !config.queries.is_empty() {
|
||||
for query in &config.queries {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!(
|
||||
"\u{1f914}",
|
||||
"Query Parameter",
|
||||
format!("{}={}", query.0, query.1)
|
||||
)
|
||||
); // 🤔
|
||||
)
|
||||
.unwrap_or_default(); // 🤔
|
||||
}
|
||||
}
|
||||
|
||||
if !config.output.is_empty() {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4be}", "Output File", config.output)
|
||||
); // 💾
|
||||
)
|
||||
.unwrap_or_default(); // 💾
|
||||
}
|
||||
|
||||
if !config.extensions.is_empty() {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!(
|
||||
"\u{1f4b2}",
|
||||
"Extensions",
|
||||
format!("[{}]", config.extensions.join(", "))
|
||||
)
|
||||
); // 💲
|
||||
)
|
||||
.unwrap_or_default(); // 💲
|
||||
}
|
||||
|
||||
if config.insecure {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f513}", "Insecure", config.insecure)
|
||||
); // 🔓
|
||||
)
|
||||
.unwrap_or_default(); // 🔓
|
||||
}
|
||||
|
||||
if config.redirects {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4cd}", "Follow Redirects", config.redirects)
|
||||
); // 📍
|
||||
)
|
||||
.unwrap_or_default(); // 📍
|
||||
}
|
||||
|
||||
if config.dontfilter {
|
||||
eprintln!(
|
||||
if config.dont_filter {
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dontfilter)
|
||||
); // 🤪
|
||||
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dont_filter)
|
||||
)
|
||||
.unwrap_or_default(); // 🤪
|
||||
}
|
||||
|
||||
match config.verbosity {
|
||||
//speaker medium volume (increasing with verbosity to loudspeaker)
|
||||
1 => {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f508}", "Verbosity", config.verbosity)
|
||||
); // 🔈
|
||||
)
|
||||
.unwrap_or_default(); // 🔈
|
||||
}
|
||||
2 => {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f509}", "Verbosity", config.verbosity)
|
||||
); // 🔉
|
||||
)
|
||||
.unwrap_or_default(); // 🔉
|
||||
}
|
||||
3 => {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f50a}", "Verbosity", config.verbosity)
|
||||
); // 🔊
|
||||
)
|
||||
.unwrap_or_default(); // 🔊
|
||||
}
|
||||
4 => {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f4e2}", "Verbosity", config.verbosity)
|
||||
); // 📢
|
||||
)
|
||||
.unwrap_or_default(); // 📢
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if config.addslash {
|
||||
eprintln!(
|
||||
if config.add_slash {
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1fa93}", "Add Slash", config.addslash)
|
||||
); // 🪓
|
||||
format_banner_entry!("\u{1fa93}", "Add Slash", config.add_slash)
|
||||
)
|
||||
.unwrap_or_default(); // 🪓
|
||||
}
|
||||
|
||||
if !config.norecursion {
|
||||
if !config.no_recursion {
|
||||
if config.depth == 0 {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f503}", "Recursion Depth", "INFINITE")
|
||||
); // 🔃
|
||||
)
|
||||
.unwrap_or_default(); // 🔃
|
||||
} else {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f503}", "Recursion Depth", config.depth)
|
||||
); // 🔃
|
||||
)
|
||||
.unwrap_or_default(); // 🔃
|
||||
}
|
||||
} else {
|
||||
eprintln!(
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.norecursion)
|
||||
); // 🚫
|
||||
format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.no_recursion)
|
||||
)
|
||||
.unwrap_or_default(); // 🚫
|
||||
}
|
||||
|
||||
eprintln!("{}", bottom);
|
||||
if CONFIGURATION.scan_limit > 0 {
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f9a5}", "Concurrent Scan Limit", config.scan_limit)
|
||||
)
|
||||
.unwrap_or_default(); // 🦥
|
||||
}
|
||||
|
||||
if matches!(status, UpdateStatus::OutOfDate) {
|
||||
writeln!(
|
||||
&mut writer,
|
||||
"{}",
|
||||
format_banner_entry!(
|
||||
"\u{1f389}",
|
||||
"New Version Available",
|
||||
"https://github.com/epi052/feroxbuster/releases/latest"
|
||||
)
|
||||
)
|
||||
.unwrap_or_default(); // 🎉
|
||||
}
|
||||
|
||||
writeln!(&mut writer, "{}", bottom).unwrap_or_default();
|
||||
// ⏯
|
||||
writeln!(
|
||||
&mut writer,
|
||||
" \u{23ef} Press [{}] to {}|{} your scan",
|
||||
style("ENTER").yellow(),
|
||||
style("pause").red(),
|
||||
style("resume").green()
|
||||
)
|
||||
.unwrap_or_default();
|
||||
writeln!(&mut writer, "{}", addl_section).unwrap_or_default();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::VERSION;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::{Mock, MockServer};
|
||||
use std::fs::read_to_string;
|
||||
use std::io::stderr;
|
||||
use std::time::Duration;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test to hit no execution of targets for loop in banner
|
||||
fn banner_without_targets() {
|
||||
async fn banner_intialize_without_targets() {
|
||||
let config = Configuration::default();
|
||||
initialize(&[], &config);
|
||||
initialize(&[], &config, VERSION, stderr()).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test to hit no execution of statuscode for loop in banner
|
||||
fn banner_without_status_codes() {
|
||||
async fn banner_intialize_without_status_codes() {
|
||||
let mut config = Configuration::default();
|
||||
config.statuscodes = vec![];
|
||||
initialize(&[String::from("http://localhost")], &config);
|
||||
config.status_codes = vec![];
|
||||
initialize(
|
||||
&[String::from("http://localhost")],
|
||||
&config,
|
||||
VERSION,
|
||||
stderr(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test to hit an empty config file
|
||||
fn banner_without_config_file() {
|
||||
async fn banner_intialize_without_config_file() {
|
||||
let mut config = Configuration::default();
|
||||
config.config = String::new();
|
||||
initialize(&[String::from("http://localhost")], &config);
|
||||
initialize(
|
||||
&[String::from("http://localhost")],
|
||||
&config,
|
||||
VERSION,
|
||||
stderr(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test to hit an empty config file
|
||||
fn banner_without_queries() {
|
||||
async fn banner_intialize_without_queries() {
|
||||
let mut config = Configuration::default();
|
||||
config.queries = vec![(String::new(), String::new())];
|
||||
initialize(&[String::from("http://localhost")], &config);
|
||||
initialize(
|
||||
&[String::from("http://localhost")],
|
||||
&config,
|
||||
VERSION,
|
||||
stderr(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test to show that a new version is available for download
|
||||
async fn banner_intialize_with_mismatched_version() {
|
||||
let config = Configuration::default();
|
||||
let file = NamedTempFile::new().unwrap();
|
||||
initialize(
|
||||
&[String::from("http://localhost")],
|
||||
&config,
|
||||
"mismatched-version",
|
||||
&file,
|
||||
)
|
||||
.await;
|
||||
let contents = read_to_string(file.path()).unwrap();
|
||||
println!("contents: {}", contents);
|
||||
assert!(contents.contains("New Version Available"));
|
||||
assert!(contents.contains("https://github.com/epi052/feroxbuster/releases/latest"));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test that
|
||||
async fn banner_needs_update_returns_unknown_with_bad_url() {
|
||||
let result = needs_update(&CONFIGURATION.client, &"", VERSION).await;
|
||||
assert!(matches!(result, UpdateStatus::Unknown));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test return value of good url to needs_update
|
||||
async fn banner_needs_update_returns_up_to_date() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/latest")
|
||||
.return_status(200)
|
||||
.return_body("{\"tag_name\":\"v1.1.0\"}")
|
||||
.create_on(&srv);
|
||||
|
||||
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.1.0").await;
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert!(matches!(result, UpdateStatus::UpToDate));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test return value of good url to needs_update that returns a newer version than current
|
||||
async fn banner_needs_update_returns_out_of_date() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/latest")
|
||||
.return_status(200)
|
||||
.return_body("{\"tag_name\":\"v1.1.0\"}")
|
||||
.create_on(&srv);
|
||||
|
||||
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert!(matches!(result, UpdateStatus::OutOfDate));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test return value of good url that times out
|
||||
async fn banner_needs_update_returns_unknown_on_timeout() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/latest")
|
||||
.return_status(200)
|
||||
.return_body("{\"tag_name\":\"v1.1.0\"}")
|
||||
.return_with_delay(Duration::from_secs(8))
|
||||
.create_on(&srv);
|
||||
|
||||
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert!(matches!(result, UpdateStatus::Unknown));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test return value of good url with bad json response
|
||||
async fn banner_needs_update_returns_unknown_on_bad_json_response() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/latest")
|
||||
.return_status(200)
|
||||
.return_body("not json")
|
||||
.create_on(&srv);
|
||||
|
||||
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert!(matches!(result, UpdateStatus::Unknown));
|
||||
}
|
||||
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// test return value of good url with json response that lacks the tag_name field
|
||||
async fn banner_needs_update_returns_unknown_on_json_without_correct_tag() {
|
||||
let srv = MockServer::start();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/latest")
|
||||
.return_status(200)
|
||||
.return_body("{\"no tag_name\": \"doesn't exist\"}")
|
||||
.create_on(&srv);
|
||||
|
||||
let result = needs_update(&CONFIGURATION.client, &srv.url("/latest"), "1.0.1").await;
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert!(matches!(result, UpdateStatus::Unknown));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::time::Duration;
|
||||
/// Create and return an instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
|
||||
pub fn initialize(
|
||||
timeout: u64,
|
||||
useragent: &str,
|
||||
user_agent: &str,
|
||||
redirects: bool,
|
||||
insecure: bool,
|
||||
headers: &HashMap<String, String>,
|
||||
@@ -27,7 +27,7 @@ pub fn initialize(
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::new(timeout, 0))
|
||||
.user_agent(useragent)
|
||||
.user_agent(user_agent)
|
||||
.danger_accept_invalid_certs(insecure)
|
||||
.default_headers(header_map)
|
||||
.redirect(policy);
|
||||
|
||||
198
src/config.rs
198
src/config.rs
@@ -51,9 +51,13 @@ pub struct Configuration {
|
||||
#[serde(default)]
|
||||
pub target_url: String,
|
||||
|
||||
/// Status Codes of interest (default: 200 204 301 302 307 308 401 403 405)
|
||||
#[serde(default = "statuscodes")]
|
||||
pub statuscodes: Vec<u16>,
|
||||
/// Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)
|
||||
#[serde(default = "status_codes")]
|
||||
pub status_codes: Vec<u16>,
|
||||
|
||||
/// Status Codes to filter out (deny list)
|
||||
#[serde(default)]
|
||||
pub filter_status: Vec<u16>,
|
||||
|
||||
/// Instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
|
||||
#[serde(skip)]
|
||||
@@ -80,8 +84,8 @@ pub struct Configuration {
|
||||
pub output: String,
|
||||
|
||||
/// Sets the User-Agent (default: feroxbuster/VERSION)
|
||||
#[serde(default = "useragent")]
|
||||
pub useragent: String,
|
||||
#[serde(default = "user_agent")]
|
||||
pub user_agent: String,
|
||||
|
||||
/// Follow redirects
|
||||
#[serde(default)]
|
||||
@@ -105,7 +109,7 @@ pub struct Configuration {
|
||||
|
||||
/// Do not scan recursively
|
||||
#[serde(default)]
|
||||
pub norecursion: bool,
|
||||
pub no_recursion: bool,
|
||||
|
||||
/// Extract links from html/javscript
|
||||
#[serde(default)]
|
||||
@@ -113,7 +117,7 @@ pub struct Configuration {
|
||||
|
||||
/// Append / to each request
|
||||
#[serde(default)]
|
||||
pub addslash: bool,
|
||||
pub add_slash: bool,
|
||||
|
||||
/// Read url(s) from STDIN
|
||||
#[serde(default)]
|
||||
@@ -123,16 +127,20 @@ pub struct Configuration {
|
||||
#[serde(default = "depth")]
|
||||
pub depth: usize,
|
||||
|
||||
/// Number of concurrent scans permitted; a limit of 0 means no limit is imposed
|
||||
#[serde(default)]
|
||||
pub scan_limit: usize,
|
||||
|
||||
/// Filter out messages of a particular size
|
||||
#[serde(default)]
|
||||
pub sizefilters: Vec<u64>,
|
||||
pub filter_size: Vec<u64>,
|
||||
|
||||
/// Don't auto-filter wildcard responses
|
||||
#[serde(default)]
|
||||
pub dontfilter: bool,
|
||||
pub dont_filter: bool,
|
||||
}
|
||||
|
||||
// functions timeout, threads, statuscodes, useragent, wordlist, and depth are used to provide
|
||||
// functions timeout, threads, status_codes, user_agent, wordlist, and depth are used to provide
|
||||
// defaults in the event that a ferox-config.toml is found but one or more of the values below
|
||||
// aren't listed in the config. This way, we get the correct defaults upon Deserialization
|
||||
|
||||
@@ -147,7 +155,7 @@ fn threads() -> usize {
|
||||
}
|
||||
|
||||
/// default status codes
|
||||
fn statuscodes() -> Vec<u16> {
|
||||
fn status_codes() -> Vec<u16> {
|
||||
DEFAULT_STATUS_CODES
|
||||
.iter()
|
||||
.map(|code| code.as_u16())
|
||||
@@ -159,8 +167,8 @@ fn wordlist() -> String {
|
||||
String::from(DEFAULT_WORDLIST)
|
||||
}
|
||||
|
||||
/// default useragent
|
||||
fn useragent() -> String {
|
||||
/// default user-agent
|
||||
fn user_agent() -> String {
|
||||
format!("feroxbuster/{}", VERSION)
|
||||
}
|
||||
|
||||
@@ -173,21 +181,22 @@ impl Default for Configuration {
|
||||
/// Builds the default Configuration for feroxbuster
|
||||
fn default() -> Self {
|
||||
let timeout = timeout();
|
||||
let useragent = useragent();
|
||||
let client = client::initialize(timeout, &useragent, false, false, &HashMap::new(), None);
|
||||
let user_agent = user_agent();
|
||||
let client = client::initialize(timeout, &user_agent, false, false, &HashMap::new(), None);
|
||||
|
||||
Configuration {
|
||||
client,
|
||||
timeout,
|
||||
useragent,
|
||||
dontfilter: false,
|
||||
user_agent,
|
||||
dont_filter: false,
|
||||
quiet: false,
|
||||
stdin: false,
|
||||
verbosity: 0,
|
||||
addslash: false,
|
||||
scan_limit: 0,
|
||||
add_slash: false,
|
||||
insecure: false,
|
||||
redirects: false,
|
||||
norecursion: false,
|
||||
no_recursion: false,
|
||||
extract_links: false,
|
||||
proxy: String::new(),
|
||||
config: String::new(),
|
||||
@@ -195,12 +204,13 @@ impl Default for Configuration {
|
||||
target_url: String::new(),
|
||||
queries: Vec::new(),
|
||||
extensions: Vec::new(),
|
||||
sizefilters: Vec::new(),
|
||||
filter_size: Vec::new(),
|
||||
filter_status: Vec::new(),
|
||||
headers: HashMap::new(),
|
||||
threads: threads(),
|
||||
depth: depth(),
|
||||
wordlist: wordlist(),
|
||||
statuscodes: statuscodes(),
|
||||
status_codes: status_codes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,20 +228,22 @@ impl Configuration {
|
||||
/// - **timeout**: `7` seconds
|
||||
/// - **verbosity**: `0` (no logging enabled)
|
||||
/// - **proxy**: `None`
|
||||
/// - **statuscodes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
||||
/// - **status_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
||||
/// - **filter_status**: `None`
|
||||
/// - **output**: `None` (print to stdout)
|
||||
/// - **quiet**: `false`
|
||||
/// - **useragent**: `feroxer/VERSION`
|
||||
/// - **user_agent**: `feroxer/VERSION`
|
||||
/// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs)
|
||||
/// - **extensions**: `None`
|
||||
/// - **sizefilters**: `None`
|
||||
/// - **filter_size**: `None`
|
||||
/// - **headers**: `None`
|
||||
/// - **queries**: `None`
|
||||
/// - **norecursion**: `false` (recursively scan enumerated sub-directories)
|
||||
/// - **addslash**: `false`
|
||||
/// - **no_recursion**: `false` (recursively scan enumerated sub-directories)
|
||||
/// - **add_slash**: `false`
|
||||
/// - **stdin**: `false`
|
||||
/// - **dontfilter**: `false` (auto filter wildcard responses)
|
||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||
///
|
||||
/// After which, any values defined in a
|
||||
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
|
||||
@@ -316,6 +328,12 @@ impl Configuration {
|
||||
config.depth = depth;
|
||||
}
|
||||
|
||||
if args.value_of("scan_limit").is_some() {
|
||||
let scan_limit =
|
||||
value_t!(args.value_of("scan_limit"), usize).unwrap_or_else(|e| e.exit());
|
||||
config.scan_limit = scan_limit;
|
||||
}
|
||||
|
||||
if args.value_of("wordlist").is_some() {
|
||||
config.wordlist = String::from(args.value_of("wordlist").unwrap());
|
||||
}
|
||||
@@ -324,9 +342,29 @@ impl Configuration {
|
||||
config.output = String::from(args.value_of("output").unwrap());
|
||||
}
|
||||
|
||||
if args.values_of("statuscodes").is_some() {
|
||||
config.statuscodes = args
|
||||
.values_of("statuscodes")
|
||||
if args.values_of("status_codes").is_some() {
|
||||
config.status_codes = args
|
||||
.values_of("status_codes")
|
||||
.unwrap() // already known good
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
.as_u16()
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
if args.values_of("filter_status").is_some() {
|
||||
config.filter_status = args
|
||||
.values_of("filter_status")
|
||||
.unwrap() // already known good
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
@@ -352,9 +390,9 @@ impl Configuration {
|
||||
.collect();
|
||||
}
|
||||
|
||||
if args.values_of("sizefilters").is_some() {
|
||||
config.sizefilters = args
|
||||
.values_of("sizefilters")
|
||||
if args.values_of("filter_size").is_some() {
|
||||
config.filter_size = args
|
||||
.values_of("filter_size")
|
||||
.unwrap() // already known good
|
||||
.map(|size| {
|
||||
size.parse::<u64>().unwrap_or_else(|e| {
|
||||
@@ -378,8 +416,8 @@ impl Configuration {
|
||||
config.quiet = args.is_present("quiet");
|
||||
}
|
||||
|
||||
if args.is_present("dontfilter") {
|
||||
config.dontfilter = args.is_present("dontfilter");
|
||||
if args.is_present("dont_filter") {
|
||||
config.dont_filter = args.is_present("dont_filter");
|
||||
}
|
||||
|
||||
if args.occurrences_of("verbosity") > 0 {
|
||||
@@ -388,12 +426,12 @@ impl Configuration {
|
||||
config.verbosity = args.occurrences_of("verbosity") as u8;
|
||||
}
|
||||
|
||||
if args.is_present("norecursion") {
|
||||
config.norecursion = args.is_present("norecursion");
|
||||
if args.is_present("no_recursion") {
|
||||
config.no_recursion = args.is_present("no_recursion");
|
||||
}
|
||||
|
||||
if args.is_present("addslash") {
|
||||
config.addslash = args.is_present("addslash");
|
||||
if args.is_present("add_slash") {
|
||||
config.add_slash = args.is_present("add_slash");
|
||||
}
|
||||
|
||||
if args.is_present("extract_links") {
|
||||
@@ -413,8 +451,8 @@ impl Configuration {
|
||||
config.proxy = String::from(args.value_of("proxy").unwrap());
|
||||
}
|
||||
|
||||
if args.value_of("useragent").is_some() {
|
||||
config.useragent = String::from(args.value_of("useragent").unwrap());
|
||||
if args.value_of("user_agent").is_some() {
|
||||
config.user_agent = String::from(args.value_of("user_agent").unwrap());
|
||||
}
|
||||
|
||||
if args.value_of("timeout").is_some() {
|
||||
@@ -462,7 +500,7 @@ impl Configuration {
|
||||
// the client and store it in the config struct
|
||||
if !config.proxy.is_empty()
|
||||
|| config.timeout != timeout()
|
||||
|| config.useragent != useragent()
|
||||
|| config.user_agent != user_agent()
|
||||
|| config.redirects
|
||||
|| config.insecure
|
||||
|| !config.headers.is_empty()
|
||||
@@ -470,7 +508,7 @@ impl Configuration {
|
||||
if config.proxy.is_empty() {
|
||||
config.client = client::initialize(
|
||||
config.timeout,
|
||||
&config.useragent,
|
||||
&config.user_agent,
|
||||
config.redirects,
|
||||
config.insecure,
|
||||
&config.headers,
|
||||
@@ -479,7 +517,7 @@ impl Configuration {
|
||||
} else {
|
||||
config.client = client::initialize(
|
||||
config.timeout,
|
||||
&config.useragent,
|
||||
&config.user_agent,
|
||||
config.redirects,
|
||||
config.insecure,
|
||||
&config.headers,
|
||||
@@ -515,25 +553,27 @@ impl Configuration {
|
||||
fn merge_config(settings: &mut Self, settings_to_merge: Self) {
|
||||
settings.threads = settings_to_merge.threads;
|
||||
settings.wordlist = settings_to_merge.wordlist;
|
||||
settings.statuscodes = settings_to_merge.statuscodes;
|
||||
settings.status_codes = settings_to_merge.status_codes;
|
||||
settings.proxy = settings_to_merge.proxy;
|
||||
settings.timeout = settings_to_merge.timeout;
|
||||
settings.verbosity = settings_to_merge.verbosity;
|
||||
settings.quiet = settings_to_merge.quiet;
|
||||
settings.output = settings_to_merge.output;
|
||||
settings.useragent = settings_to_merge.useragent;
|
||||
settings.user_agent = settings_to_merge.user_agent;
|
||||
settings.redirects = settings_to_merge.redirects;
|
||||
settings.insecure = settings_to_merge.insecure;
|
||||
settings.extract_links = settings_to_merge.extract_links;
|
||||
settings.extensions = settings_to_merge.extensions;
|
||||
settings.headers = settings_to_merge.headers;
|
||||
settings.queries = settings_to_merge.queries;
|
||||
settings.norecursion = settings_to_merge.norecursion;
|
||||
settings.addslash = settings_to_merge.addslash;
|
||||
settings.no_recursion = settings_to_merge.no_recursion;
|
||||
settings.add_slash = settings_to_merge.add_slash;
|
||||
settings.stdin = settings_to_merge.stdin;
|
||||
settings.depth = settings_to_merge.depth;
|
||||
settings.sizefilters = settings_to_merge.sizefilters;
|
||||
settings.dontfilter = settings_to_merge.dontfilter;
|
||||
settings.filter_size = settings_to_merge.filter_size;
|
||||
settings.filter_status = settings_to_merge.filter_status;
|
||||
settings.dont_filter = settings_to_merge.dont_filter;
|
||||
settings.scan_limit = settings_to_merge.scan_limit;
|
||||
}
|
||||
|
||||
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
||||
@@ -569,25 +609,27 @@ mod tests {
|
||||
fn setup_config_test() -> Configuration {
|
||||
let data = r#"
|
||||
wordlist = "/some/path"
|
||||
statuscodes = [201, 301, 401]
|
||||
status_codes = [201, 301, 401]
|
||||
threads = 40
|
||||
timeout = 5
|
||||
proxy = "http://127.0.0.1:8080"
|
||||
quiet = true
|
||||
verbosity = 1
|
||||
scan_limit = 6
|
||||
output = "/some/otherpath"
|
||||
redirects = true
|
||||
insecure = true
|
||||
extensions = ["html", "php", "js"]
|
||||
headers = {stuff = "things", mostuff = "mothings"}
|
||||
queries = [["name","value"], ["rick", "astley"]]
|
||||
norecursion = true
|
||||
addslash = true
|
||||
no_recursion = true
|
||||
add_slash = true
|
||||
stdin = true
|
||||
dontfilter = true
|
||||
dont_filter = true
|
||||
extract_links = true
|
||||
depth = 1
|
||||
sizefilters = [4120]
|
||||
filter_size = [4120]
|
||||
filter_status = [201]
|
||||
"#;
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME);
|
||||
@@ -603,22 +645,24 @@ mod tests {
|
||||
assert_eq!(config.proxy, String::new());
|
||||
assert_eq!(config.target_url, String::new());
|
||||
assert_eq!(config.config, String::new());
|
||||
assert_eq!(config.statuscodes, statuscodes());
|
||||
assert_eq!(config.status_codes, status_codes());
|
||||
assert_eq!(config.threads, threads());
|
||||
assert_eq!(config.depth, depth());
|
||||
assert_eq!(config.timeout, timeout());
|
||||
assert_eq!(config.verbosity, 0);
|
||||
assert_eq!(config.scan_limit, 0);
|
||||
assert_eq!(config.quiet, false);
|
||||
assert_eq!(config.dontfilter, false);
|
||||
assert_eq!(config.norecursion, false);
|
||||
assert_eq!(config.dont_filter, false);
|
||||
assert_eq!(config.no_recursion, false);
|
||||
assert_eq!(config.stdin, false);
|
||||
assert_eq!(config.addslash, false);
|
||||
assert_eq!(config.add_slash, false);
|
||||
assert_eq!(config.redirects, false);
|
||||
assert_eq!(config.extract_links, false);
|
||||
assert_eq!(config.insecure, false);
|
||||
assert_eq!(config.queries, Vec::new());
|
||||
assert_eq!(config.extensions, Vec::<String>::new());
|
||||
assert_eq!(config.sizefilters, Vec::<u64>::new());
|
||||
assert_eq!(config.filter_size, Vec::<u64>::new());
|
||||
assert_eq!(config.filter_status, Vec::<u16>::new());
|
||||
assert_eq!(config.headers, HashMap::new());
|
||||
}
|
||||
|
||||
@@ -631,9 +675,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_statuscodes() {
|
||||
fn config_reads_status_codes() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.statuscodes, vec![201, 301, 401]);
|
||||
assert_eq!(config.status_codes, vec![201, 301, 401]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -650,6 +694,13 @@ mod tests {
|
||||
assert_eq!(config.depth, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_scan_limit() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.scan_limit, 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_timeout() {
|
||||
@@ -701,9 +752,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_norecursion() {
|
||||
fn config_reads_no_recursion() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.norecursion, true);
|
||||
assert_eq!(config.no_recursion, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -715,16 +766,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_dontfilter() {
|
||||
fn config_reads_dont_filter() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.dontfilter, true);
|
||||
assert_eq!(config.dont_filter, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_addslash() {
|
||||
fn config_reads_add_slash() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.addslash, true);
|
||||
assert_eq!(config.add_slash, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -743,9 +794,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_sizefilters() {
|
||||
fn config_reads_filter_size() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.sizefilters, vec![4120]);
|
||||
assert_eq!(config.filter_size, vec![4120]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_filter_status() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.filter_status, vec![201]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
142
src/filters.rs
Normal file
142
src/filters.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use crate::config::CONFIGURATION;
|
||||
use crate::utils::get_url_path_length;
|
||||
use crate::FeroxResponse;
|
||||
use std::any::Any;
|
||||
use std::fmt::Debug;
|
||||
|
||||
// references:
|
||||
// https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5
|
||||
// https://stackoverflow.com/questions/25339603/how-to-test-for-equality-between-trait-objects
|
||||
|
||||
/// FeroxFilter trait; represents different types of possible filters that can be applied to
|
||||
/// responses
|
||||
pub trait FeroxFilter: Debug + Send + Sync {
|
||||
/// Determine whether or not this particular filter should be applied or not
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool;
|
||||
|
||||
/// delegates to the FeroxFilter-implementing type which gives us the actual type of self
|
||||
fn box_eq(&self, other: &dyn Any) -> bool;
|
||||
|
||||
/// gives us `other` as Any in box_eq
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
/// implementation of PartialEq, necessary long-form due to "trait cannot be made into an object"
|
||||
/// error when attempting to derive PartialEq on the trait itself
|
||||
impl PartialEq for Box<dyn FeroxFilter> {
|
||||
/// Perform a comparison of two implementors of the FeroxFilter trait
|
||||
fn eq(&self, other: &Box<dyn FeroxFilter>) -> bool {
|
||||
self.box_eq(other.as_any())
|
||||
}
|
||||
}
|
||||
|
||||
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
|
||||
///
|
||||
/// `dynamic` is the size of the response that will later be combined with the length
|
||||
/// of the path of the url requested and used to determine interesting pages from custom
|
||||
/// 404s where the requested url is reflected back in the response
|
||||
///
|
||||
/// `size` is size of the response that should be included with filters passed via runtime
|
||||
/// configuration and any static wildcard lengths.
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct WildcardFilter {
|
||||
/// size of the response that will later be combined with the length of the path of the url
|
||||
/// requested
|
||||
pub dynamic: u64,
|
||||
|
||||
/// size of the response that should be included with filters passed via runtime configuration
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for WildcardFilter
|
||||
impl FeroxFilter for WildcardFilter {
|
||||
/// Examine size, dynamic, and content_len to determine whether or not the response received
|
||||
/// is a wildcard response and therefore should be filtered out
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
||||
|
||||
// quick return if dont_filter is set
|
||||
if CONFIGURATION.dont_filter {
|
||||
// --dont-filter applies specifically to wildcard filters, it is not a 100% catch all
|
||||
// for not filtering anything. As such, it should live in the implementation of
|
||||
// a wildcard filter
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.size > 0 && self.size == response.content_length() {
|
||||
// static wildcard size found during testing
|
||||
// size isn't default, size equals response length, and auto-filter is on
|
||||
log::debug!("static wildcard: filtered out {}", response.url());
|
||||
log::trace!("exit: should_filter_response -> true");
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.dynamic > 0 {
|
||||
// dynamic wildcard offset found during testing
|
||||
|
||||
// I'm about to manually split this url path instead of using reqwest::Url's
|
||||
// builtin parsing. The reason is that they call .split() on the url path
|
||||
// except that I don't want an empty string taking up the last index in the
|
||||
// event that the url ends with a forward slash. It's ugly enough to be split
|
||||
// into its own function for readability.
|
||||
let url_len = get_url_path_length(&response.url());
|
||||
|
||||
if url_len + self.dynamic == response.content_length() {
|
||||
log::debug!("dynamic wildcard: filtered out {}", response.url());
|
||||
log::trace!("exit: should_filter_response -> true");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
log::trace!("exit: should_filter_response -> false");
|
||||
false
|
||||
}
|
||||
|
||||
/// Compare one WildcardFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple implementor of FeroxFilter; used to filter out status codes specified using
|
||||
/// -C|--filter-status
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
pub struct StatusCodeFilter {
|
||||
/// Status code that should not be displayed to the user
|
||||
pub filter_code: u16,
|
||||
}
|
||||
|
||||
/// implementation of FeroxFilter for StatusCodeFilter
|
||||
impl FeroxFilter for StatusCodeFilter {
|
||||
/// Check `filter_code` against what was passed in via -C|--filter-status
|
||||
fn should_filter_response(&self, response: &FeroxResponse) -> bool {
|
||||
log::trace!("enter: should_filter_response({:?} {:?})", self, response);
|
||||
|
||||
if response.status().as_u16() == self.filter_code {
|
||||
log::debug!(
|
||||
"filtered out {} based on --filter-status of {}",
|
||||
response.url(),
|
||||
self.filter_code
|
||||
);
|
||||
log::trace!("exit: should_filter_response -> true");
|
||||
return true;
|
||||
}
|
||||
|
||||
log::trace!("exit: should_filter_response -> false");
|
||||
false
|
||||
}
|
||||
|
||||
/// Compare one StatusCodeFilter to another
|
||||
fn box_eq(&self, other: &dyn Any) -> bool {
|
||||
other.downcast_ref::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,21 @@
|
||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use crate::scanner::should_filter_response;
|
||||
use crate::utils::{
|
||||
ferox_print, format_url, get_url_path_length, make_request, module_colorizer, status_colorizer,
|
||||
use crate::{
|
||||
config::{CONFIGURATION, PROGRESS_PRINTER},
|
||||
filters::WildcardFilter,
|
||||
scanner::should_filter_response,
|
||||
utils::{
|
||||
ferox_print, format_url, get_url_path_length, make_request, module_colorizer,
|
||||
status_colorizer,
|
||||
},
|
||||
FeroxResponse,
|
||||
};
|
||||
use console::style;
|
||||
use indicatif::ProgressBar;
|
||||
use reqwest::Response;
|
||||
use std::process;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// length of a standard UUID, used when determining wildcard responses
|
||||
const UUID_LENGTH: u64 = 32;
|
||||
|
||||
/// Data holder for two pieces of data needed when auto-filtering out wildcard responses
|
||||
///
|
||||
/// `dynamic` is the size of the response that will later be combined with the length
|
||||
/// of the path of the url requested and used to determine interesting pages from custom
|
||||
/// 404s where the requested url is reflected back in the response
|
||||
///
|
||||
/// `size` is size of the response that should be included with filters passed via runtime
|
||||
/// configuration and any static wildcard lengths.
|
||||
#[derive(Default, Debug, PartialEq, Copy, Clone)]
|
||||
pub struct WildcardFilter {
|
||||
/// size of the response that will later be combined with the length of the path of the url
|
||||
/// requested
|
||||
pub dynamic: u64,
|
||||
|
||||
/// size of the response that should be included with filters passed via runtime configuration
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
/// Simple helper to return a uuid, formatted as lowercase without hyphens
|
||||
///
|
||||
/// `length` determines the number of uuids to string together. Each uuid
|
||||
@@ -66,8 +51,8 @@ pub async fn wildcard_test(
|
||||
tx_file
|
||||
);
|
||||
|
||||
if CONFIGURATION.dontfilter {
|
||||
// early return, dontfilter scans don't need tested
|
||||
if CONFIGURATION.dont_filter {
|
||||
// early return, dont_filter scans don't need tested
|
||||
log::trace!("exit: wildcard_test -> None");
|
||||
return None;
|
||||
}
|
||||
@@ -75,13 +60,13 @@ pub async fn wildcard_test(
|
||||
let clone_req_one = tx_file.clone();
|
||||
let clone_req_two = tx_file.clone();
|
||||
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1, clone_req_one).await {
|
||||
if let Some(ferox_response) = make_wildcard_request(&target_url, 1, clone_req_one).await {
|
||||
bar.inc(1);
|
||||
|
||||
// found a wildcard response
|
||||
let mut wildcard = WildcardFilter::default();
|
||||
|
||||
let wc_length = resp_one.content_length().unwrap_or(0);
|
||||
let wc_length = ferox_response.content_length();
|
||||
|
||||
if wc_length == 0 {
|
||||
log::trace!("exit: wildcard_test -> Some({:?})", wildcard);
|
||||
@@ -93,25 +78,23 @@ pub async fn wildcard_test(
|
||||
if let Some(resp_two) = make_wildcard_request(&target_url, 3, clone_req_two).await {
|
||||
bar.inc(1);
|
||||
|
||||
let wc2_length = resp_two.content_length().unwrap_or(0);
|
||||
let wc2_length = resp_two.content_length();
|
||||
|
||||
if wc2_length == wc_length + (UUID_LENGTH * 2) {
|
||||
// second length is what we'd expect to see if the requested url is
|
||||
// reflected in the response along with some static content; aka custom 404
|
||||
let url_len = get_url_path_length(&resp_one.url());
|
||||
let url_len = get_url_path_length(&ferox_response.url());
|
||||
|
||||
wildcard.dynamic = wc_length - url_len;
|
||||
|
||||
if !CONFIGURATION.quiet
|
||||
&& !should_filter_response(&wildcard.dynamic, &resp_one.url())
|
||||
{
|
||||
if !CONFIGURATION.quiet {
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
wildcard.dynamic,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length - url_len).cyan(),
|
||||
style("--dontfilter").yellow()
|
||||
style("--dont-filter").yellow()
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
@@ -125,15 +108,14 @@ pub async fn wildcard_test(
|
||||
} else if wc_length == wc2_length {
|
||||
wildcard.size = wc_length;
|
||||
|
||||
if !CONFIGURATION.quiet && !should_filter_response(&wildcard.size, &resp_one.url())
|
||||
{
|
||||
if !CONFIGURATION.quiet {
|
||||
let msg = format!(
|
||||
"{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n",
|
||||
status_colorizer("WLD"),
|
||||
wc_length,
|
||||
style("auto-filtering").yellow(),
|
||||
style(wc_length).cyan(),
|
||||
style("--dontfilter").yellow()
|
||||
style("--dont-filter").yellow()
|
||||
);
|
||||
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
@@ -167,7 +149,7 @@ async fn make_wildcard_request(
|
||||
target_url: &str,
|
||||
length: usize,
|
||||
tx_file: UnboundedSender<String>,
|
||||
) -> Option<Response> {
|
||||
) -> Option<FeroxResponse> {
|
||||
log::trace!(
|
||||
"enter: make_wildcard_request({}, {}, {:?})",
|
||||
target_url,
|
||||
@@ -180,7 +162,7 @@ async fn make_wildcard_request(
|
||||
let nonexistent = match format_url(
|
||||
target_url,
|
||||
&unique_str,
|
||||
CONFIGURATION.addslash,
|
||||
CONFIGURATION.add_slash,
|
||||
&CONFIGURATION.queries,
|
||||
None,
|
||||
) {
|
||||
@@ -197,20 +179,21 @@ async fn make_wildcard_request(
|
||||
match make_request(&CONFIGURATION.client, &nonexistent.to_owned()).await {
|
||||
Ok(response) => {
|
||||
if CONFIGURATION
|
||||
.statuscodes
|
||||
.status_codes
|
||||
.contains(&response.status().as_u16())
|
||||
{
|
||||
// found a wildcard response
|
||||
let url_len = get_url_path_length(&response.url());
|
||||
let content_len = response.content_length().unwrap_or(0);
|
||||
let ferox_response = FeroxResponse::from(response, false).await;
|
||||
let url_len = get_url_path_length(&ferox_response.url());
|
||||
let content_len = ferox_response.content_length();
|
||||
|
||||
if !CONFIGURATION.quiet && !should_filter_response(&content_len, &response.url()) {
|
||||
if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) {
|
||||
let msg = format!(
|
||||
"{} {:>10} Got {} for {} (url length: {})\n",
|
||||
wildcard,
|
||||
content_len,
|
||||
status_colorizer(&response.status().as_str()),
|
||||
response.url(),
|
||||
status_colorizer(&ferox_response.status().as_str()),
|
||||
ferox_response.url(),
|
||||
url_len
|
||||
);
|
||||
|
||||
@@ -223,18 +206,16 @@ async fn make_wildcard_request(
|
||||
);
|
||||
}
|
||||
|
||||
if response.status().is_redirection() {
|
||||
if ferox_response.status().is_redirection() {
|
||||
// show where it goes, if possible
|
||||
if let Some(next_loc) = response.headers().get("Location") {
|
||||
if let Some(next_loc) = ferox_response.headers().get("Location") {
|
||||
let next_loc_str = next_loc.to_str().unwrap_or("Unknown");
|
||||
if !CONFIGURATION.quiet
|
||||
&& !should_filter_response(&content_len, &response.url())
|
||||
{
|
||||
if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) {
|
||||
let msg = format!(
|
||||
"{} {:>10} {} redirects to => {}\n",
|
||||
wildcard,
|
||||
content_len,
|
||||
response.url(),
|
||||
ferox_response.url(),
|
||||
next_loc_str
|
||||
);
|
||||
|
||||
@@ -248,8 +229,8 @@ async fn make_wildcard_request(
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: make_wildcard_request -> {:?}", response);
|
||||
return Some(response);
|
||||
log::trace!("exit: make_wildcard_request -> {:?}", ferox_response);
|
||||
return Some(ferox_response);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -276,7 +257,7 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
||||
let request = match format_url(
|
||||
target_url,
|
||||
"",
|
||||
CONFIGURATION.addslash,
|
||||
CONFIGURATION.add_slash,
|
||||
&CONFIGURATION.queries,
|
||||
None,
|
||||
) {
|
||||
@@ -305,14 +286,6 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
||||
|
||||
if good_urls.is_empty() {
|
||||
log::error!("Could not connect to any target provided, exiting.");
|
||||
log::trace!("exit: connectivity_test");
|
||||
eprintln!(
|
||||
"{} {} Could not connect to any target provided",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("heuristics::connectivity_test"),
|
||||
);
|
||||
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
log::trace!("exit: connectivity_test -> {:?}", good_urls);
|
||||
@@ -334,8 +307,7 @@ fn try_send_message_to_file(msg: &str, tx_file: UnboundedSender<String>, save_ou
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
"{} {}",
|
||||
module_colorizer("heuristics::try_send_message_to_file"),
|
||||
e
|
||||
);
|
||||
|
||||
31
src/lib.rs
31
src/lib.rs
@@ -2,6 +2,7 @@ pub mod banner;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod extractor;
|
||||
pub mod filters;
|
||||
pub mod heuristics;
|
||||
pub mod logger;
|
||||
pub mod parser;
|
||||
@@ -10,13 +11,30 @@ pub mod reporter;
|
||||
pub mod scanner;
|
||||
pub mod utils;
|
||||
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::{Response, StatusCode, Url};
|
||||
use reqwest::{
|
||||
header::HeaderMap,
|
||||
{Response, StatusCode, Url},
|
||||
};
|
||||
use std::{error, fmt};
|
||||
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||
|
||||
/// Generic Result type to ease error handling in async contexts
|
||||
pub type FeroxResult<T> =
|
||||
std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
|
||||
pub type FeroxResult<T> = std::result::Result<T, Box<dyn error::Error + Send + Sync + 'static>>;
|
||||
|
||||
/// Simple Error implementation to allow for custom error returns
|
||||
#[derive(Debug, Default)]
|
||||
pub struct FeroxError {
|
||||
/// fancy string that can be printed via Display
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl error::Error for FeroxError {}
|
||||
|
||||
impl fmt::Display for FeroxError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", &self.message)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic mpsc::unbounded_channel type to tidy up some code
|
||||
pub type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
||||
@@ -32,6 +50,9 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const DEFAULT_WORDLIST: &str =
|
||||
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
|
||||
|
||||
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
|
||||
pub static SLEEP_DURATION: u64 = 500;
|
||||
|
||||
/// Default list of status codes to report
|
||||
///
|
||||
/// * 200 Ok
|
||||
@@ -61,7 +82,7 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [
|
||||
pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml";
|
||||
|
||||
/// A `FeroxResponse`, derived from a `Response` to a submitted `Request`
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FeroxResponse {
|
||||
/// The final `Url` of this `FeroxResponse`
|
||||
url: Url,
|
||||
|
||||
179
src/main.rs
179
src/main.rs
@@ -1,17 +1,59 @@
|
||||
use feroxbuster::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||
use feroxbuster::scanner::scan_url;
|
||||
use feroxbuster::utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer};
|
||||
use feroxbuster::{banner, heuristics, logger, reporter, FeroxResponse, FeroxResult};
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use feroxbuster::{
|
||||
banner,
|
||||
config::{CONFIGURATION, PROGRESS_BAR, PROGRESS_PRINTER},
|
||||
heuristics, logger, reporter,
|
||||
scanner::{scan_url, PAUSE_SCAN},
|
||||
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
||||
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use tokio::io;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{stderr, BufRead, BufReader},
|
||||
process,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{io, sync::mpsc::UnboundedSender, task::JoinHandle};
|
||||
use tokio_util::codec::{FramedRead, LinesCodec};
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not the terminal input handler should exit
|
||||
pub static SCAN_COMPLETE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Handles specific key events triggered by the user over stdin
|
||||
fn terminal_input_handler() {
|
||||
log::trace!("enter: terminal_input_handler");
|
||||
|
||||
loop {
|
||||
if event::poll(Duration::from_millis(SLEEP_DURATION)).unwrap_or(false) {
|
||||
// It's guaranteed that the `read()` won't block when the `poll()`
|
||||
// function returns `true`
|
||||
|
||||
if let Ok(key_pressed) = event::read() {
|
||||
if key_pressed == Event::Key(KeyCode::Enter.into()) {
|
||||
// if the user presses Enter, toggle the value stored in PAUSE_SCAN
|
||||
// ignore any other keys
|
||||
let current = PAUSE_SCAN.load(Ordering::Acquire);
|
||||
|
||||
PAUSE_SCAN.store(!current, Ordering::Release);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout expired and no `Event` is available; use the timeout to check SCAN_COMPLETE
|
||||
if SCAN_COMPLETE.load(Ordering::Relaxed) {
|
||||
// scan has been marked complete by main, time to exit the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("exit: terminal_input_handler");
|
||||
}
|
||||
|
||||
/// Create a HashSet of Strings from the given wordlist then stores it inside an Arc
|
||||
fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>>> {
|
||||
log::trace!("enter: get_unique_words_from_wordlist({})", path);
|
||||
@@ -19,12 +61,6 @@ fn get_unique_words_from_wordlist(path: &str) -> FeroxResult<Arc<HashSet<String>
|
||||
let file = match File::open(&path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("main::get_unique_words_from_wordlist"),
|
||||
e
|
||||
);
|
||||
log::error!("Could not open wordlist: {}", e);
|
||||
log::trace!("exit: get_unique_words_from_wordlist -> {}", e);
|
||||
|
||||
@@ -69,13 +105,9 @@ async fn scan(
|
||||
.await??;
|
||||
|
||||
if words.len() == 0 {
|
||||
eprintln!(
|
||||
"{} {} Did not find any words in {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("main::scan"),
|
||||
CONFIGURATION.wordlist
|
||||
);
|
||||
process::exit(1);
|
||||
let mut err = FeroxError::default();
|
||||
err.message = format!("Did not find any words in {}", CONFIGURATION.wordlist);
|
||||
return Err(Box::new(err));
|
||||
}
|
||||
|
||||
let mut tasks = vec![];
|
||||
@@ -100,6 +132,7 @@ async fn scan(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get targets from either commandline or stdin, pass them back to the caller as a Result<Vec>
|
||||
async fn get_targets() -> FeroxResult<Vec<String>> {
|
||||
log::trace!("enter: get_targets");
|
||||
|
||||
@@ -123,15 +156,31 @@ async fn get_targets() -> FeroxResult<Vec<String>> {
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// setup logging based on the number of -v's used
|
||||
logger::initialize(CONFIGURATION.verbosity);
|
||||
/// async main called from real main, broken out in this way to allow for some synchronous code
|
||||
/// to be executed before bringing the tokio runtime online
|
||||
async fn wrapped_main() {
|
||||
// join can only be called once, otherwise it causes the thread to panic
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// ok, lazy_static! uses (unsurprisingly in retrospect) a lazy loading model where the
|
||||
// thing obtained through deref isn't actually created until it's used. This created a
|
||||
// problem when initializing the logger as it relied on PROGRESS_PRINTER which may or may
|
||||
// not have been created by the time it was needed for logging (really only occurred in
|
||||
// heuristics / banner / main). In order to initialize logging properly, we need to ensure
|
||||
// PROGRESS_PRINTER and PROGRESS_BAR have been used at least once. This call satisfies
|
||||
// that constraint
|
||||
PROGRESS_PRINTER.println("");
|
||||
PROGRESS_BAR.join().unwrap();
|
||||
});
|
||||
|
||||
// can't trace main until after logger is initialized
|
||||
// can't trace main until after logger is initialized and the above task is started
|
||||
log::trace!("enter: main");
|
||||
log::debug!("{:#?}", *CONFIGURATION);
|
||||
|
||||
// spawn a thread that listens for keyboard input on stdin, when a user presses enter
|
||||
// the input handler will toggle PAUSE_SCAN, which in turn is used to pause and resume
|
||||
// scans that are already running
|
||||
tokio::task::spawn_blocking(terminal_input_handler);
|
||||
|
||||
let save_output = !CONFIGURATION.output.is_empty(); // was -o used?
|
||||
|
||||
let (tx_term, tx_file, term_handle, file_handle) =
|
||||
@@ -142,37 +191,64 @@ async fn main() {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
// should only happen in the event that there was an error reading from stdin
|
||||
log::error!("{}", e);
|
||||
ferox_print(
|
||||
&format!(
|
||||
"{} {} {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("main::get_targets"),
|
||||
e
|
||||
),
|
||||
&PROGRESS_PRINTER,
|
||||
);
|
||||
process::exit(1);
|
||||
log::error!("{} {}", module_colorizer("main::get_targets"), e);
|
||||
clean_up(tx_term, term_handle, tx_file, file_handle, save_output).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !CONFIGURATION.quiet {
|
||||
// only print banner if -q isn't used
|
||||
banner::initialize(&targets, &CONFIGURATION);
|
||||
let std_stderr = stderr(); // std::io::stderr
|
||||
banner::initialize(&targets, &CONFIGURATION, &VERSION, std_stderr).await;
|
||||
}
|
||||
|
||||
// discard non-responsive targets
|
||||
let live_targets = heuristics::connectivity_test(&targets).await;
|
||||
|
||||
if live_targets.is_empty() {
|
||||
clean_up(tx_term, term_handle, tx_file, file_handle, save_output).await;
|
||||
return;
|
||||
}
|
||||
|
||||
// kick off a scan against any targets determined to be responsive
|
||||
match scan(live_targets, tx_term.clone(), tx_file.clone()).await {
|
||||
Ok(_) => {
|
||||
log::info!("All scans complete!");
|
||||
}
|
||||
Err(e) => log::error!("An error occurred: {}", e),
|
||||
Err(e) => {
|
||||
ferox_print(
|
||||
&format!("{} while scanning: {}", status_colorizer("Error"), e),
|
||||
&PROGRESS_PRINTER,
|
||||
);
|
||||
clean_up(tx_term, term_handle, tx_file, file_handle, save_output).await;
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// manually drop tx in order for the rx task's while loops to eval to false
|
||||
clean_up(tx_term, term_handle, tx_file, file_handle, save_output).await;
|
||||
|
||||
log::trace!("exit: main");
|
||||
}
|
||||
|
||||
/// Single cleanup function that handles all the necessary drops/finishes etc required to gracefully
|
||||
/// shutdown the program
|
||||
async fn clean_up(
|
||||
tx_term: UnboundedSender<FeroxResponse>,
|
||||
term_handle: JoinHandle<()>,
|
||||
tx_file: UnboundedSender<String>,
|
||||
file_handle: Option<JoinHandle<()>>,
|
||||
save_output: bool,
|
||||
) {
|
||||
log::trace!(
|
||||
"enter: clean_up({:?}, {:?}, {:?}, {:?}, {}",
|
||||
tx_term,
|
||||
term_handle,
|
||||
tx_file,
|
||||
file_handle,
|
||||
save_output
|
||||
);
|
||||
|
||||
drop(tx_term);
|
||||
log::trace!("dropped terminal output handler's transmitter");
|
||||
|
||||
@@ -204,9 +280,22 @@ async fn main() {
|
||||
log::trace!("done awaiting file output handler's receiver");
|
||||
}
|
||||
|
||||
log::trace!("exit: main");
|
||||
// mark all scans complete so the terminal input handler will exit cleanly
|
||||
SCAN_COMPLETE.store(true, Ordering::Relaxed);
|
||||
|
||||
// clean-up function for the MultiProgress bar; must be called last in order to still see
|
||||
// the final trace message above
|
||||
// the final trace messages above
|
||||
PROGRESS_PRINTER.finish();
|
||||
|
||||
log::trace!("exit: clean_up");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// setup logging based on the number of -v's used
|
||||
logger::initialize(CONFIGURATION.verbosity);
|
||||
|
||||
if let Ok(mut runtime) = tokio::runtime::Runtime::new() {
|
||||
let future = wrapped_main();
|
||||
runtime.block_on(future);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,15 +68,15 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("statuscodes")
|
||||
Arg::with_name("status_codes")
|
||||
.short("s")
|
||||
.long("statuscodes")
|
||||
.long("status-codes")
|
||||
.value_name("STATUS_CODE")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
.help(
|
||||
"Status Codes of interest (default: 200 204 301 302 307 308 401 403 405)",
|
||||
"Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
@@ -87,9 +87,9 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.help("Only print URLs; Don't print status codes, response size, running config, etc...")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("dontfilter")
|
||||
Arg::with_name("dont_filter")
|
||||
.short("D")
|
||||
.long("dontfilter")
|
||||
.long("dont-filter")
|
||||
.takes_value(false)
|
||||
.help("Don't auto-filter wildcard responses")
|
||||
)
|
||||
@@ -102,9 +102,9 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.takes_value(true),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("useragent")
|
||||
Arg::with_name("user_agent")
|
||||
.short("a")
|
||||
.long("useragent")
|
||||
.long("user-agent")
|
||||
.value_name("USER_AGENT")
|
||||
.takes_value(true)
|
||||
.help(
|
||||
@@ -162,16 +162,16 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("norecursion")
|
||||
Arg::with_name("no_recursion")
|
||||
.short("n")
|
||||
.long("norecursion")
|
||||
.long("no-recursion")
|
||||
.takes_value(false)
|
||||
.help("Do not scan recursively")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("addslash")
|
||||
Arg::with_name("add_slash")
|
||||
.short("f")
|
||||
.long("addslash")
|
||||
.long("add-slash")
|
||||
.takes_value(false)
|
||||
.conflicts_with("extensions")
|
||||
.help("Append / to each request")
|
||||
@@ -184,9 +184,9 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.conflicts_with("url")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("sizefilters")
|
||||
Arg::with_name("filter_size")
|
||||
.short("S")
|
||||
.long("sizefilter")
|
||||
.long("filter-size")
|
||||
.value_name("SIZE")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
@@ -195,6 +195,18 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
"Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("filter_status")
|
||||
.short("C")
|
||||
.long("filter-status")
|
||||
.value_name("STATUS_CODE")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
.help(
|
||||
"Filter out status codes (deny list) (ex: -C 200 -S 401)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("extract_links")
|
||||
.short("e")
|
||||
@@ -202,7 +214,14 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.takes_value(false)
|
||||
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false)")
|
||||
)
|
||||
|
||||
.arg(
|
||||
Arg::with_name("scan_limit")
|
||||
.short("L")
|
||||
.long("scan-limit")
|
||||
.value_name("SCAN_LIMIT")
|
||||
.takes_value(true)
|
||||
.help("Limit total number of concurrent scans (default: 0, i.e. no limit)")
|
||||
)
|
||||
.after_help(r#"NOTE:
|
||||
Options that take multiple values are very flexible. Consider the following ways of specifying
|
||||
extensions:
|
||||
@@ -218,7 +237,7 @@ EXAMPLES:
|
||||
./feroxbuster -u http://127.1 -H Accept:application/json "Authorization: Bearer {token}"
|
||||
|
||||
IPv6, non-recursive scan with INFO-level logging enabled:
|
||||
./feroxbuster -u http://[::1] --norecursion -vv
|
||||
./feroxbuster -u http://[::1] --no-recursion -vv
|
||||
|
||||
Read urls from STDIN; pipe only resulting urls out to another tool
|
||||
cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
|
||||
@@ -92,9 +92,9 @@ async fn spawn_terminal_reporter(
|
||||
);
|
||||
|
||||
while let Some(resp) = resp_chan.recv().await {
|
||||
log::debug!("received {} on reporting channel", resp.url());
|
||||
log::trace!("received {} on reporting channel", resp.url());
|
||||
|
||||
if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) {
|
||||
if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) {
|
||||
let report = if CONFIGURATION.quiet {
|
||||
// -q used, just need the url
|
||||
format!("{}\n", resp.url())
|
||||
@@ -126,7 +126,7 @@ async fn spawn_terminal_reporter(
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("report complete: {}", resp.url());
|
||||
log::trace!("report complete: {}", resp.url());
|
||||
}
|
||||
log::trace!("exit: spawn_terminal_reporter");
|
||||
}
|
||||
|
||||
298
src/scanner.rs
298
src/scanner.rs
@@ -1,29 +1,120 @@
|
||||
use crate::config::{CONFIGURATION, PROGRESS_BAR};
|
||||
use crate::extractor::get_links;
|
||||
use crate::heuristics::WildcardFilter;
|
||||
use crate::utils::{format_url, get_current_depth, get_url_path_length, make_request};
|
||||
use crate::{heuristics, progress, FeroxChannel, FeroxResponse};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use futures::{stream, StreamExt};
|
||||
use crate::{
|
||||
config::CONFIGURATION,
|
||||
extractor::get_links,
|
||||
filters::{FeroxFilter, StatusCodeFilter, WildcardFilter},
|
||||
heuristics, progress,
|
||||
utils::{format_url, get_current_depth, make_request},
|
||||
FeroxChannel, FeroxResponse, SLEEP_DURATION,
|
||||
};
|
||||
use console::style;
|
||||
use futures::{
|
||||
future::{BoxFuture, FutureExt},
|
||||
stream, StreamExt,
|
||||
};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::Url;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
|
||||
use tokio::task::JoinHandle;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
convert::TryInto,
|
||||
io::{stderr, Write},
|
||||
ops::Deref,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{
|
||||
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||
Semaphore,
|
||||
},
|
||||
task::JoinHandle,
|
||||
time,
|
||||
};
|
||||
|
||||
/// Single atomic number that gets incremented once, used to track first scan vs. all others
|
||||
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Atomic boolean flag, used to determine whether or not a scan should pause or resume
|
||||
pub static PAUSE_SCAN: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
lazy_static! {
|
||||
/// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication
|
||||
static ref SCANNED_URLS: RwLock<HashSet<String>> = RwLock::new(HashSet::new());
|
||||
|
||||
/// Vector of WildcardFilters that have been ID'd through heuristics
|
||||
static ref WILDCARD_FILTERS: Arc<RwLock<Vec<Arc<WildcardFilter>>>> = Arc::new(RwLock::new(Vec::<Arc<WildcardFilter>>::new()));
|
||||
/// A clock spinner protected with a RwLock to allow for a single thread to use at a time
|
||||
static ref SINGLE_SPINNER: RwLock<ProgressBar> = RwLock::new(get_single_spinner());
|
||||
|
||||
/// Vector of implementors of the FeroxFilter trait
|
||||
static ref FILTERS: Arc<RwLock<Vec<Box<dyn FeroxFilter>>>> = Arc::new(RwLock::new(Vec::<Box<dyn FeroxFilter>>::new()));
|
||||
|
||||
/// Bounded semaphore used as a barrier to limit concurrent scans
|
||||
static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit);
|
||||
}
|
||||
|
||||
/// Return a clock spinner, used when scans are paused
|
||||
fn get_single_spinner() -> ProgressBar {
|
||||
log::trace!("enter: get_single_spinner");
|
||||
|
||||
let spinner = ProgressBar::new_spinner().with_style(
|
||||
ProgressStyle::default_spinner()
|
||||
.tick_strings(&[
|
||||
"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚",
|
||||
])
|
||||
.template(&format!(
|
||||
"\t-= All Scans {{spinner}} {} =-",
|
||||
style("Paused").red()
|
||||
)),
|
||||
);
|
||||
|
||||
log::trace!("exit: get_single_spinner -> {:?}", spinner);
|
||||
spinner
|
||||
}
|
||||
|
||||
/// Forced the calling thread into a busy loop
|
||||
///
|
||||
/// Every `SLEEP_DURATION` milliseconds, the function examines the result stored in `PAUSE_SCAN`
|
||||
///
|
||||
/// When the value stored in `PAUSE_SCAN` becomes `false`, the function returns, exiting the busy
|
||||
/// loop
|
||||
async fn pause_scan() {
|
||||
log::trace!("enter: pause_scan");
|
||||
// function uses tokio::time, not std
|
||||
|
||||
// local testing showed a pretty slow increase (less than linear) in CPU usage as # of
|
||||
// concurrent scans rose when SLEEP_DURATION was set to 500, using that as the default for now
|
||||
let mut interval = time::interval(time::Duration::from_millis(SLEEP_DURATION));
|
||||
|
||||
// ignore any error returned
|
||||
let _ = stderr().flush();
|
||||
|
||||
if SINGLE_SPINNER.read().unwrap().is_finished() {
|
||||
// in order to not leave draw artifacts laying around in the terminal, we call
|
||||
// finish_and_clear on the progress bar when resuming scans. For this reason, we need to
|
||||
// check if the spinner is finished, and repopulate the RwLock with a new spinner if
|
||||
// necessary
|
||||
if let Ok(mut guard) = SINGLE_SPINNER.write() {
|
||||
*guard = get_single_spinner();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
spinner.enable_steady_tick(120);
|
||||
}
|
||||
|
||||
loop {
|
||||
// first tick happens immediately, all others wait the specified duration
|
||||
interval.tick().await;
|
||||
|
||||
if !PAUSE_SCAN.load(Ordering::Acquire) {
|
||||
// PAUSE_SCAN is false, so we can exit the busy loop
|
||||
if let Ok(spinner) = SINGLE_SPINNER.write() {
|
||||
spinner.finish_and_clear();
|
||||
}
|
||||
let _ = stderr().flush();
|
||||
log::trace!("exit: pause_scan");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given url to `SCANNED_URLS`
|
||||
@@ -63,37 +154,37 @@ fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock<HashSet<Str
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the given WildcardFilter to `WILDCARD_FILTERS`
|
||||
/// Adds the given FeroxFilter to the given list of FeroxFilter implementors
|
||||
///
|
||||
/// If `WILDCARD_FILTERS` did not already contain the filter, return true; otherwise return false
|
||||
fn add_filter_to_list_of_wildcard_filters(
|
||||
filter: Arc<WildcardFilter>,
|
||||
wildcard_filters: Arc<RwLock<Vec<Arc<WildcardFilter>>>>,
|
||||
/// If the given list did not already contain the filter, return true; otherwise return false
|
||||
fn add_filter_to_list_of_ferox_filters(
|
||||
filter: Box<dyn FeroxFilter>,
|
||||
ferox_filters: Arc<RwLock<Vec<Box<dyn FeroxFilter>>>>,
|
||||
) -> bool {
|
||||
log::trace!(
|
||||
"enter: add_filter_to_list_of_wildcard_filters({:?}, {:?})",
|
||||
"enter: add_filter_to_list_of_ferox_filters({:?}, {:?})",
|
||||
filter,
|
||||
wildcard_filters
|
||||
ferox_filters
|
||||
);
|
||||
|
||||
match wildcard_filters.write() {
|
||||
match ferox_filters.write() {
|
||||
Ok(mut filters) => {
|
||||
// If the set did not contain the assigned filter, true is returned.
|
||||
// If the set did contain the assigned filter, false is returned.
|
||||
if filters.contains(&filter) {
|
||||
log::trace!("exit: add_filter_to_list_of_wildcard_filters -> false");
|
||||
log::trace!("exit: add_filter_to_list_of_ferox_filters -> false");
|
||||
return false;
|
||||
}
|
||||
|
||||
filters.push(filter);
|
||||
|
||||
log::trace!("exit: add_filter_to_list_of_wildcard_filters -> true");
|
||||
log::trace!("exit: add_filter_to_list_of_ferox_filters -> true");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
// poisoned lock
|
||||
log::error!("Set of wildcard filters poisoned: {}", e);
|
||||
log::trace!("exit: add_filter_to_list_of_wildcard_filters -> false");
|
||||
log::trace!("exit: add_filter_to_list_of_ferox_filters -> false");
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -120,6 +211,7 @@ fn spawn_recursion_handler(
|
||||
|
||||
let boxed_future = async move {
|
||||
let mut scans = vec![];
|
||||
|
||||
while let Some(resp) = recursion_channel.recv().await {
|
||||
let unknown = add_url_to_list_of_scanned_urls(&resp, &SCANNED_URLS);
|
||||
|
||||
@@ -173,7 +265,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec<Url>
|
||||
if let Ok(url) = format_url(
|
||||
&target_url,
|
||||
&word,
|
||||
CONFIGURATION.addslash,
|
||||
CONFIGURATION.add_slash,
|
||||
&CONFIGURATION.queries,
|
||||
None,
|
||||
) {
|
||||
@@ -184,7 +276,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec<Url>
|
||||
if let Ok(url) = format_url(
|
||||
&target_url,
|
||||
&word,
|
||||
CONFIGURATION.addslash,
|
||||
CONFIGURATION.add_slash,
|
||||
&CONFIGURATION.queries,
|
||||
Some(ext),
|
||||
) {
|
||||
@@ -237,6 +329,7 @@ fn response_is_directory(response: &FeroxResponse) -> bool {
|
||||
}
|
||||
} else if response.status().is_success() {
|
||||
// status code is 2xx, need to check if it ends in /
|
||||
|
||||
if response.url().as_str().ends_with('/') {
|
||||
log::debug!("{} is directory suitable for recursion", response.url());
|
||||
log::trace!("exit: is_directory -> true");
|
||||
@@ -332,43 +425,24 @@ async fn try_recursion(
|
||||
|
||||
/// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported
|
||||
/// to the user or not.
|
||||
pub fn should_filter_response(content_len: &u64, url: &Url) -> bool {
|
||||
if CONFIGURATION.sizefilters.contains(content_len) {
|
||||
// filtered value from --sizefilters, move on to the next url
|
||||
log::debug!("size filter: filtered out {}", url);
|
||||
pub fn should_filter_response(response: &FeroxResponse) -> bool {
|
||||
if CONFIGURATION
|
||||
.filter_size
|
||||
.contains(&response.content_length())
|
||||
{
|
||||
// filtered value from --filter-size, size filters and wildcards are two separate filters
|
||||
// and are applied independently
|
||||
log::debug!("size filter: filtered out {}", response.url());
|
||||
return true;
|
||||
}
|
||||
|
||||
match WILDCARD_FILTERS.read() {
|
||||
match FILTERS.read() {
|
||||
Ok(filters) => {
|
||||
for filter in filters.iter() {
|
||||
if CONFIGURATION.dontfilter {
|
||||
// quick return if dontfilter is set
|
||||
return false;
|
||||
}
|
||||
|
||||
if filter.size > 0 && filter.size == *content_len {
|
||||
// static wildcard size found during testing
|
||||
// size isn't default, size equals response length, and auto-filter is on
|
||||
log::debug!("static wildcard: filtered out {}", url);
|
||||
// wildcard.should_filter goes here
|
||||
if filter.should_filter_response(&response) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if filter.dynamic > 0 {
|
||||
// dynamic wildcard offset found during testing
|
||||
|
||||
// I'm about to manually split this url path instead of using reqwest::Url's
|
||||
// builtin parsing. The reason is that they call .split() on the url path
|
||||
// except that I don't want an empty string taking up the last index in the
|
||||
// event that the url ends with a forward slash. It's ugly enough to be split
|
||||
// into its own function for readability.
|
||||
let url_len = get_url_path_length(&url);
|
||||
|
||||
if url_len + filter.dynamic == *content_len {
|
||||
log::debug!("dynamic wildcard: filtered out {}", url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -407,16 +481,14 @@ async fn make_requests(
|
||||
let ferox_response = FeroxResponse::from(response, CONFIGURATION.extract_links).await;
|
||||
|
||||
// do recursion if appropriate
|
||||
if !CONFIGURATION.norecursion {
|
||||
if !CONFIGURATION.no_recursion {
|
||||
try_recursion(&ferox_response, base_depth, dir_chan.clone()).await;
|
||||
}
|
||||
|
||||
// purposefully doing recursion before filtering. the thought process is that
|
||||
// even though this particular url is filtered, subsequent urls may not
|
||||
|
||||
let content_len = &ferox_response.content_length();
|
||||
|
||||
if should_filter_response(content_len, &ferox_response.url()) {
|
||||
if should_filter_response(&ferox_response) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -435,7 +507,7 @@ async fn make_requests(
|
||||
let new_url = match format_url(
|
||||
&new_link,
|
||||
&"",
|
||||
CONFIGURATION.addslash,
|
||||
CONFIGURATION.add_slash,
|
||||
&CONFIGURATION.queries,
|
||||
None,
|
||||
) {
|
||||
@@ -453,8 +525,7 @@ async fn make_requests(
|
||||
FeroxResponse::from(new_response, CONFIGURATION.extract_links).await;
|
||||
|
||||
// filter if necessary
|
||||
let new_content_len = &new_ferox_response.content_length();
|
||||
if should_filter_response(new_content_len, &new_ferox_response.url()) {
|
||||
if should_filter_response(&new_ferox_response) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -471,7 +542,7 @@ async fn make_requests(
|
||||
continue;
|
||||
}
|
||||
|
||||
if !CONFIGURATION.norecursion {
|
||||
if !CONFIGURATION.no_recursion {
|
||||
log::debug!(
|
||||
"Recursive extraction: {} ({})",
|
||||
new_ferox_response.url(),
|
||||
@@ -548,15 +619,26 @@ pub async fn scan_url(
|
||||
progress_bar.reset_elapsed();
|
||||
|
||||
if CALL_COUNT.load(Ordering::Relaxed) == 0 {
|
||||
// join can only be called once, otherwise it causes the thread to panic
|
||||
tokio::task::spawn_blocking(move || PROGRESS_BAR.join().unwrap());
|
||||
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
// this protection around join also allows us to add the first scanned url to SCANNED_URLS
|
||||
// this protection allows us to add the first scanned url to SCANNED_URLS
|
||||
// from within the scan_url function instead of the recursion handler
|
||||
add_url_to_list_of_scanned_urls(&target_url, &SCANNED_URLS);
|
||||
|
||||
if CONFIGURATION.scan_limit == 0 {
|
||||
// scan_limit == 0 means no limit should be imposed... however, scoping the Semaphore
|
||||
// permit is tricky, so as a workaround, we'll add a ridiculous number of permits to
|
||||
// the semaphore (1,152,921,504,606,846,975 to be exact) and call that 'unlimited'
|
||||
SCAN_LIMITER.add_permits(usize::MAX >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
// When acquire is called and the semaphore has remaining permits, the function immediately
|
||||
// returns a permit. However, if no remaining permits are available, acquire (asynchronously)
|
||||
// waits until an outstanding permit is dropped. At this point, the freed permit is assigned
|
||||
// to the caller.
|
||||
let permit = SCAN_LIMITER.acquire().await;
|
||||
|
||||
// Arc clones to be passed around to the various scans
|
||||
let wildcard_bar = progress_bar.clone();
|
||||
let heuristics_file_clone = tx_file.clone();
|
||||
@@ -576,13 +658,23 @@ pub async fn scan_url(
|
||||
.await
|
||||
});
|
||||
|
||||
// add any wildcard filters to `FILTERS`
|
||||
let filter =
|
||||
match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_file_clone).await {
|
||||
Some(f) => Arc::new(f),
|
||||
None => Arc::new(WildcardFilter::default()),
|
||||
Some(f) => Box::new(f),
|
||||
None => Box::new(WildcardFilter::default()),
|
||||
};
|
||||
|
||||
add_filter_to_list_of_wildcard_filters(filter.clone(), WILDCARD_FILTERS.clone());
|
||||
add_filter_to_list_of_ferox_filters(filter, FILTERS.clone());
|
||||
|
||||
// add any status code filters to `FILTERS`
|
||||
for code_filter in &CONFIGURATION.filter_status {
|
||||
let filter = StatusCodeFilter {
|
||||
filter_code: *code_filter,
|
||||
};
|
||||
let boxed_filter = Box::new(filter);
|
||||
add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone());
|
||||
}
|
||||
|
||||
// producer tasks (mp of mpsc); responsible for making requests
|
||||
let producers = stream::iter(looping_words.deref().to_owned())
|
||||
@@ -592,14 +684,22 @@ pub async fn scan_url(
|
||||
let pb = progress_bar.clone(); // progress bar is an Arc around internal state
|
||||
let tgt = target_url.to_string(); // done to satisfy 'static lifetime below
|
||||
(
|
||||
tokio::spawn(async move { make_requests(&tgt, &word, base_depth, txd, txr).await }),
|
||||
tokio::spawn(async move {
|
||||
if PAUSE_SCAN.load(Ordering::Acquire) {
|
||||
// for every word in the wordlist, check to see if PAUSE_SCAN is set to true
|
||||
// when true; enter a busy loop that only exits by setting PAUSE_SCAN back
|
||||
// to false
|
||||
pause_scan().await;
|
||||
}
|
||||
make_requests(&tgt, &word, base_depth, txd, txr).await
|
||||
}),
|
||||
pb,
|
||||
)
|
||||
})
|
||||
.for_each_concurrent(CONFIGURATION.threads, |(resp, bar)| async move {
|
||||
match resp.await {
|
||||
Ok(_) => {
|
||||
bar.inc(1);
|
||||
bar.inc((CONFIGURATION.extensions.len() + 1) as u64);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("error awaiting a response: {}", e);
|
||||
@@ -612,6 +712,9 @@ pub async fn scan_url(
|
||||
producers.await;
|
||||
log::trace!("done awaiting scan producers");
|
||||
|
||||
// drop the current permit so the semaphore will allow another scan to proceed
|
||||
drop(permit);
|
||||
|
||||
progress_bar.finish();
|
||||
|
||||
// manually drop tx in order for the rx task's while loops to eval to false
|
||||
@@ -760,28 +863,31 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a wildcard filter with the `size` attribute set to WILDCARD_FILTERS and ensure that
|
||||
/// should_filter_response correctly returns true
|
||||
fn should_filter_response_filters_wildcard_size() {
|
||||
let mut filter = WildcardFilter::default();
|
||||
let url = Url::parse("http://localhost").unwrap();
|
||||
filter.size = 18;
|
||||
let filter = Arc::new(filter);
|
||||
add_filter_to_list_of_wildcard_filters(filter, WILDCARD_FILTERS.clone());
|
||||
let result = should_filter_response(&18, &url);
|
||||
assert!(result);
|
||||
/// test that get_single_spinner returns the correct spinner
|
||||
fn scanner_get_single_spinner_returns_spinner() {
|
||||
let spinner = get_single_spinner();
|
||||
assert!(!spinner.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// add a wildcard filter with the `dynamic` attribute set to WILDCARD_FILTERS and ensure that
|
||||
/// should_filter_response correctly returns true
|
||||
fn should_filter_response_filters_wildcard_dynamic() {
|
||||
let mut filter = WildcardFilter::default();
|
||||
let url = Url::parse("http://localhost/some-path").unwrap();
|
||||
filter.dynamic = 9;
|
||||
let filter = Arc::new(filter);
|
||||
add_filter_to_list_of_wildcard_filters(filter, WILDCARD_FILTERS.clone());
|
||||
let result = should_filter_response(&18, &url);
|
||||
assert!(result);
|
||||
#[tokio::test(core_threads = 1)]
|
||||
/// tests that pause_scan pauses execution and releases execution when PAUSE_SCAN is toggled
|
||||
/// the spinner used during the test has had .finish_and_clear called on it, meaning that
|
||||
/// a new one will be created, taking the if branch within the function
|
||||
async fn scanner_pause_scan_with_finished_spinner() {
|
||||
let now = time::Instant::now();
|
||||
|
||||
PAUSE_SCAN.store(true, Ordering::Relaxed);
|
||||
SINGLE_SPINNER.write().unwrap().finish_and_clear();
|
||||
|
||||
let expected = time::Duration::from_secs(2);
|
||||
|
||||
tokio::spawn(async move {
|
||||
time::delay_for(expected).await;
|
||||
PAUSE_SCAN.store(false, Ordering::Relaxed);
|
||||
});
|
||||
|
||||
pause_scan().await;
|
||||
|
||||
assert!(now.elapsed() > expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) {
|
||||
pub fn format_url(
|
||||
url: &str,
|
||||
word: &str,
|
||||
addslash: bool,
|
||||
add_slash: bool,
|
||||
queries: &[(String, String)],
|
||||
extension: Option<&str>,
|
||||
) -> FeroxResult<Url> {
|
||||
@@ -148,7 +148,7 @@ pub fn format_url(
|
||||
"enter: format_url({}, {}, {}, {:?} {:?})",
|
||||
url,
|
||||
word,
|
||||
addslash,
|
||||
add_slash,
|
||||
queries,
|
||||
extension
|
||||
);
|
||||
@@ -175,7 +175,7 @@ pub fn format_url(
|
||||
// extensions and slashes are mutually exclusive cases
|
||||
let word = if extension.is_some() {
|
||||
format!("{}.{}", word, extension.unwrap())
|
||||
} else if addslash && !word.ends_with('/') {
|
||||
} else if add_slash && !word.ends_with('/') {
|
||||
// -f used, and word doesn't already end with a /
|
||||
format!("{}/", word)
|
||||
} else {
|
||||
|
||||
@@ -23,7 +23,7 @@ fn banner_prints_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.pipe_stdin(file)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -56,7 +56,7 @@ fn banner_prints_headers() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("-H")
|
||||
.arg("mostuff:mothings")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -77,17 +77,17 @@ fn banner_prints_headers() -> Result<(), Box<dyn std::error::Error>> {
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + multiple size filters
|
||||
fn banner_prints_size_filters() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn banner_prints_filter_sizes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("-S")
|
||||
.arg("789456123")
|
||||
.arg("--sizefilter")
|
||||
.arg("--filter-size")
|
||||
.arg("44444444")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -118,7 +118,7 @@ fn banner_prints_queries() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("--query")
|
||||
.arg("stuff=things")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -147,7 +147,7 @@ fn banner_prints_status_codes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("-s")
|
||||
.arg("201,301,401")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -174,7 +174,7 @@ fn banner_prints_output_file() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("--output")
|
||||
.arg("/super/cool/path")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -201,7 +201,7 @@ fn banner_prints_insecure() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-k")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -228,7 +228,7 @@ fn banner_prints_redirects() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-r")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -258,7 +258,7 @@ fn banner_prints_extensions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("--extensions")
|
||||
.arg("pdf")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -277,15 +277,15 @@ fn banner_prints_extensions() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + dontfilter
|
||||
fn banner_prints_dontfilter() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// expect to see all mandatory prints + dont_filter
|
||||
fn banner_prints_dont_filter() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--dontfilter")
|
||||
.arg("--dont-filter")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -312,7 +312,7 @@ fn banner_prints_verbosity_one() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-v")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -339,7 +339,7 @@ fn banner_prints_verbosity_two() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-vv")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -366,7 +366,7 @@ fn banner_prints_verbosity_three() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-vvv")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -393,7 +393,7 @@ fn banner_prints_verbosity_four() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-vvvv")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -420,7 +420,7 @@ fn banner_prints_add_slash() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-f")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -448,7 +448,7 @@ fn banner_prints_infinite_depth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("--depth")
|
||||
.arg("0")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -476,7 +476,7 @@ fn banner_prints_recursion_depth() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("--depth")
|
||||
.arg("343214")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -503,7 +503,7 @@ fn banner_prints_no_recursion() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-n")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -522,7 +522,7 @@ fn banner_prints_no_recursion() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see only the error of could not connect
|
||||
/// expect to see nothing
|
||||
fn banner_doesnt_print() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
@@ -530,10 +530,8 @@ fn banner_doesnt_print() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-q")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"ERROR heuristics::connectivity_test Could not connect to any target provided",
|
||||
));
|
||||
.success()
|
||||
.stderr(predicate::str::is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -547,7 +545,7 @@ fn banner_prints_extract_links() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("http://localhost")
|
||||
.arg("-e")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
@@ -563,3 +561,59 @@ fn banner_prints_extract_links() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + scan-limit
|
||||
fn banner_prints_scan_limit() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("-L")
|
||||
.arg("4")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Concurrent Scan Limit"))
|
||||
.and(predicate::str::contains("│ 4"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + filter-status
|
||||
fn banner_prints_filter_status() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("-C")
|
||||
.arg("200")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Status Code Filters"))
|
||||
.and(predicate::str::contains("│ [200]"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ fn read_in_config_file_for_settings() -> Result<(), Box<dyn std::error::Error>>
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(predicate::str::contains("│ 37"));
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
@@ -181,7 +181,7 @@ fn extractor_finds_same_relative_url_twice() -> Result<(), Box<dyn std::error::E
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a request to a page that contains an absolute link that leads to a page with a sizefilter
|
||||
/// send a request to a page that contains an absolute link that leads to a page with a filter_size
|
||||
/// that should filter it out, expect not to see the second response reported
|
||||
fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
@@ -209,7 +209,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box<dyn std::error::Error>>
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--extract-links")
|
||||
.arg("--sizefilter")
|
||||
.arg("--filter-size")
|
||||
.arg("18")
|
||||
.unwrap();
|
||||
|
||||
|
||||
57
tests/test_filters.rs
Normal file
57
tests/test_filters.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
mod utils;
|
||||
use assert_cmd::prelude::*;
|
||||
use httpmock::Method::GET;
|
||||
use httpmock::{Mock, MockServer};
|
||||
use predicates::prelude::*;
|
||||
use std::process::Command;
|
||||
use utils::{setup_tmp_directory, teardown_tmp_directory};
|
||||
|
||||
#[test]
|
||||
/// create a FeroxResponse that should elicit a true from
|
||||
/// StatusCodeFilter::should_filter_response
|
||||
fn filters_status_code_should_filter_response() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) =
|
||||
setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_status(302)
|
||||
.return_body("this is a test")
|
||||
.create_on(&srv);
|
||||
|
||||
let mock_two = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/file.js")
|
||||
.return_status(200)
|
||||
.return_body("this is also a test of some import")
|
||||
.create_on(&srv);
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-vvvv")
|
||||
.arg("--filter-status")
|
||||
.arg("302")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("/LICENSE")
|
||||
.not()
|
||||
.and(predicate::str::contains("302"))
|
||||
.not()
|
||||
.and(predicate::str::contains("14"))
|
||||
.not()
|
||||
.and(predicate::str::contains("/file.js"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("34")),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
assert_eq!(mock_two.times_called(), 1);
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
@@ -19,11 +19,9 @@ fn test_single_target_cannot_connect() -> Result<(), Box<dyn std::error::Error>>
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(
|
||||
predicate::str::contains("Could not connect to any target provided")
|
||||
.and(predicate::str::contains("ERROR"))
|
||||
.and(predicate::str::contains("heuristics::connectivity_test")),
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("Could not connect to http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk, skipping...", )
|
||||
);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -47,11 +45,9 @@ fn test_two_targets_cannot_connect() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.pipe_stdin(file)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(
|
||||
predicate::str::contains("Could not connect to any target provided")
|
||||
.and(predicate::str::contains("ERROR"))
|
||||
.and(predicate::str::contains("heuristics::connectivity_test")),
|
||||
.success()
|
||||
.stdout(
|
||||
predicate::str::contains("Could not connect to http://fjdksafjkdsajfkdsajkfdsajkfsdjkdsfdsafdsafdsajkr3l2ajfdskafdsjk, skipping...", )
|
||||
);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -115,7 +111,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -158,7 +154,7 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
@@ -198,8 +194,8 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// uses dontfilter, so the normal wildcard test should never happen
|
||||
fn heuristics_static_wildcard_request_with_dontfilter() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// uses dont_filter, so the normal wildcard test should never happen
|
||||
fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
@@ -216,7 +212,7 @@ fn heuristics_static_wildcard_request_with_dontfilter() -> Result<(), Box<dyn st
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--dontfilter")
|
||||
.arg("--dont-filter")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -251,7 +247,7 @@ fn heuristics_wildcard_test_with_two_static_wildcards() -> Result<(), Box<dyn st
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -299,7 +295,7 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_quiet_enabled(
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.arg("-q")
|
||||
.unwrap();
|
||||
|
||||
@@ -340,7 +336,7 @@ fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file(
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
@@ -406,7 +402,7 @@ fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--addslash")
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
@@ -25,10 +25,8 @@ fn main_use_root_owned_file_as_wordlist() -> Result<(), Box<dyn std::error::Erro
|
||||
.arg("/etc/shadow")
|
||||
.arg("-vvvv")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(predicate::str::contains(
|
||||
"ERROR main::get_unique_words_from_wordlist Permission denied (os error 13)",
|
||||
));
|
||||
.failure()
|
||||
.stdout(predicate::str::contains("Permission denied (os error 13)"));
|
||||
|
||||
// connectivity test hits it once
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
@@ -57,9 +55,7 @@ fn main_use_empty_wordlist() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.arg("-vvvv")
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"ERROR main::scan Did not find any words in",
|
||||
));
|
||||
.stdout(predicate::str::contains("Did not find any words in"));
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
|
||||
@@ -83,11 +79,9 @@ fn main_use_empty_stdin_targets() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.pipe_stdin(file)
|
||||
.unwrap()
|
||||
.assert()
|
||||
.failure()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("Could not connect to any target provided")
|
||||
.and(predicate::str::contains("ERROR"))
|
||||
.and(predicate::str::contains("heuristics::connectivity_test"))
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.not(), // no target url found
|
||||
);
|
||||
|
||||
@@ -327,7 +327,8 @@ fn scanner_single_request_quiet_scan() -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send single valid request, get back a 301 without a Location header, expect false
|
||||
/// send single valid request, get back a 301 without a Location header
|
||||
/// expect response_is_directory to return false when called
|
||||
fn scanner_single_request_returns_301_without_location_header(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
@@ -336,6 +337,7 @@ fn scanner_single_request_returns_301_without_location_header(
|
||||
let mock = Mock::new()
|
||||
.expect_method(GET)
|
||||
.expect_path("/LICENSE")
|
||||
.return_body("this is a test")
|
||||
.return_status(301)
|
||||
.create_on(&srv);
|
||||
|
||||
@@ -345,17 +347,16 @@ fn scanner_single_request_returns_301_without_location_header(
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("-T")
|
||||
.arg("--timeout")
|
||||
.arg("5")
|
||||
.arg("-a")
|
||||
.arg("--user-agent")
|
||||
.arg("some-user-agent-string")
|
||||
.unwrap();
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains(srv.url("/LICENSE"))
|
||||
.and(predicate::str::contains("301"))
|
||||
.and(predicate::str::contains("14"))
|
||||
.not(),
|
||||
.and(predicate::str::contains("14")),
|
||||
);
|
||||
|
||||
assert_eq!(mock.times_called(), 1);
|
||||
@@ -401,7 +402,7 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std:
|
||||
.and(predicate::str::contains("20"))
|
||||
.and(predicate::str::contains("ignored"))
|
||||
.not()
|
||||
.and(predicate::str::contains("14"))
|
||||
.and(predicate::str::contains(" 14 "))
|
||||
.not(),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user