Compare commits

...

44 Commits

Author SHA1 Message Date
epi
ec78ec3049 added ability to specify install directory for install-nix.sh 2023-04-19 17:15:50 -05:00
epi
960536e918 Merge pull request #879 from epi052/all-contributors/add-DrorDvash
docs: add DrorDvash as a contributor for bug
2023-04-19 08:05:15 -05:00
allcontributors[bot]
fdae9aa9d6 docs: update .all-contributorsrc [skip ci] 2023-04-19 13:03:50 +00:00
allcontributors[bot]
5c73c3fb23 docs: update README.md [skip ci] 2023-04-19 13:03:49 +00:00
epi
02ef6d7e3f Merge pull request #877 from epi052/update-indicatif-finally
Random improvements
2023-04-19 07:59:47 -05:00
epi
3378246820 updated arm release names for --update fix 2023-04-19 07:46:43 -05:00
epi
692db93048 clippy/tests and added logic to wait for link extraction if done 2023-04-19 06:57:36 -05:00
epi
233cf99907 made link extraction req/resp async 2023-04-19 06:56:52 -05:00
epi
8cd9918b76 upgraded deps 2023-04-19 06:55:23 -05:00
epi
66bcbfc2f2 bumped version to 2.9.4 2023-04-19 06:51:35 -05:00
epi
8b127c0093 made 404-like req/resp async 2023-04-17 06:37:28 -05:00
epi
94de58d855 removed response body from mpsc traversal 2023-04-17 06:36:47 -05:00
epi
2b95b7be69 updated indicatif to 0.17.3 2023-04-17 06:26:59 -05:00
epi
e77c1314b1 Merge pull request #869 from epi052/auto-filtering-account-for-extensions
added extensions and status codes into auto filtering decision calculus
2023-04-11 19:07:53 -05:00
epi
1ced3b5d77 modified msg when dir listing is found with dont-extract 2023-04-11 18:48:18 -05:00
epi
b5472f5341 updated deps 2023-04-11 18:39:28 -05:00
epi
ea81600850 clippy 2023-04-11 18:36:37 -05:00
epi
4f679592b8 bumped version to 2.9.3 2023-04-11 18:34:02 -05:00
epi
b375893461 nitpickery 2023-04-11 18:32:56 -05:00
epi
e110f86f39 added extensions and status codes into auto filtering decision calculus 2023-04-11 18:29:12 -05:00
epi
c7498a7695 Merge pull request #839 from epi052/all-contributors/add-acut3
docs: add acut3 as a contributor for bug
2023-03-18 12:23:34 -05:00
allcontributors[bot]
f973baaba8 docs: update .all-contributorsrc [skip ci] 2023-03-18 17:23:25 +00:00
allcontributors[bot]
148982cdc4 docs: update README.md [skip ci] 2023-03-18 17:23:24 +00:00
epi
5d96658c79 Merge pull request #834 from epi052/827-load-wordlist-from-url
load wordlist from url; change some defaults/fix some bugs
2023-03-18 11:59:23 -05:00
epi
46d00507b0 removed cruft 2023-03-18 11:52:58 -05:00
epi
d561e59ec9 added test 2023-03-18 11:44:45 -05:00
epi
b786578c03 Merge pull request #824 from aancw/docs-package
Update alternative installation method for brew and chocolatey
2023-03-18 07:13:38 -05:00
epi
bd54ad0087 Merge branch 'main' into 827-load-wordlist-from-url 2023-03-18 07:09:14 -05:00
epi
d98c6a7457 bumped deps 2023-03-18 07:07:40 -05:00
epi
c493d001b5 fmt clippy etc 2023-03-18 07:02:45 -05:00
epi
bd4566fa7b updated parser text 2023-03-18 07:01:07 -05:00
epi
8fbf9d0274 -w accepts http/https urls 2023-03-18 06:59:19 -05:00
epi
d6b10c6476 reverted collect-backups change 2023-03-18 06:07:38 -05:00
epi
a5e845864c Merge branch 'main' of github.com:epi052/feroxbuster 2023-03-17 06:47:19 -05:00
epi
b02358678b added check for force-recursion to dirlisting check 2023-03-17 06:47:13 -05:00
epi
1b8fdcec17 hid old false defaults; added dont-* flags 2023-03-17 06:32:28 -05:00
epi
92cc2ab448 fixed test 2023-03-17 06:31:23 -05:00
epi
0b0e08ae4f updated extract-links and collect-backups default to true 2023-03-17 05:45:19 -05:00
epi
25762395b1 Merge pull request #833 from epi052/all-contributors/add-imBigo
docs: add imBigo as a contributor for bug
2023-03-16 21:30:12 -05:00
allcontributors[bot]
55b4034bd0 docs: update .all-contributorsrc [skip ci] 2023-03-17 02:29:52 +00:00
allcontributors[bot]
ffa409ca3d docs: update README.md [skip ci] 2023-03-17 02:29:51 +00:00
epi
bb4a335299 fixed divide by zero error 2023-03-16 21:23:39 -05:00
epi
1e0ec5c833 fixed divide by zero error 2023-03-16 21:21:05 -05:00
Aan
b5fa6b149e Update alternative installation method for brew and chocolatey 2023-03-12 22:05:05 +07:00
32 changed files with 1278 additions and 756 deletions

View File

@@ -553,6 +553,33 @@
"infra", "infra",
"ideas" "ideas"
] ]
},
{
"login": "imBigo",
"name": "Simon",
"avatar_url": "https://avatars.githubusercontent.com/u/54672433?v=4",
"profile": "https://github.com/imBigo",
"contributions": [
"bug"
]
},
{
"login": "acut3",
"name": "Nicolas Christin",
"avatar_url": "https://avatars.githubusercontent.com/u/17295243?v=4",
"profile": "https://acut3.github.io/",
"contributions": [
"bug"
]
},
{
"login": "DrorDvash",
"name": "DrDv",
"avatar_url": "https://avatars.githubusercontent.com/u/8413651?v=4",
"profile": "https://github.com/DrorDvash",
"contributions": [
"bug"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -27,13 +27,13 @@ jobs:
- type: armv7 - type: armv7
os: ubuntu-latest os: ubuntu-latest
target: armv7-unknown-linux-gnueabihf target: armv7-unknown-linux-gnueabihf
name: armv7-feroxbuster name: armv7-linux-feroxbuster
path: target/armv7-unknown-linux-gnueabihf/release/feroxbuster path: target/armv7-unknown-linux-gnueabihf/release/feroxbuster
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
- type: aarch64 - type: aarch64
os: ubuntu-latest os: ubuntu-latest
target: aarch64-unknown-linux-gnu target: aarch64-unknown-linux-gnu
name: aarch64-feroxbuster name: aarch64-linux-feroxbuster
path: target/aarch64-unknown-linux-gnu/release/feroxbuster path: target/aarch64-unknown-linux-gnu/release/feroxbuster
pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig pkg_config_path: /usr/lib/x86_64-linux-gnu/pkgconfig
steps: steps:

696
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "feroxbuster" name = "feroxbuster"
version = "2.9.1" version = "2.9.4"
authors = ["Ben 'epi' Risher (@epi052)"] authors = ["Ben 'epi' Risher (@epi052)"]
license = "MIT" license = "MIT"
edition = "2021" edition = "2021"
@@ -22,47 +22,52 @@ build = "build.rs"
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[build-dependencies] [build-dependencies]
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] } clap = { version = "4.2", features = ["wrap_help", "cargo"] }
clap_complete = "4.1.4" clap_complete = "4.1"
regex = "1.5.5" regex = "1.5"
lazy_static = "1.4.0" lazy_static = "1.4"
dirs = "4.0.0" dirs = "5.0"
[dependencies] [dependencies]
scraper = "0.15.0" scraper = "0.16"
futures = "0.3.26" futures = "0.3"
tokio = { version = "1.26.0", features = ["full"] } tokio = { version = "1.26", features = ["full"] }
tokio-util = { version = "0.7.7", features = ["codec"] } tokio-util = { version = "0.7", features = ["codec"] }
log = "0.4.17" log = "0.4"
env_logger = "0.10.0" env_logger = "0.10"
reqwest = { version = "0.11.10", features = ["socks"] } reqwest = { version = "0.11", features = ["socks"] }
# uses feature unification to add 'serde' to reqwest::Url # uses feature unification to add 'serde' to reqwest::Url
url = { version = "2.2.2", features = ["serde"] } url = { version = "2.2", features = ["serde"] }
serde_regex = "1.1.0" serde_regex = "1.1"
clap = { version = "4.1.8", features = ["wrap_help", "cargo"] } clap = { version = "4.2", features = ["wrap_help", "cargo"] }
lazy_static = "1.4.0" lazy_static = "1.4"
toml = "0.7.2" toml = "0.7"
serde = { version = "1.0.137", features = ["derive", "rc"] } serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0.94" serde_json = "1.0"
uuid = { version = "1.3.0", features = ["v4"] } uuid = { version = "1.3", features = ["v4"] }
indicatif = "0.15" indicatif = "0.17"
console = "0.15.2" console = "0.15"
openssl = { version = "0.10", features = ["vendored"] } openssl = { version = "0.10", features = ["vendored"] }
dirs = "4.0.0" dirs = "5.0"
regex = "1.5.5" regex = "1.5"
crossterm = "0.26.0" crossterm = "0.26"
rlimit = "0.9.1" rlimit = "0.9"
ctrlc = "3.2.2" ctrlc = "3.2"
anyhow = "1.0.69" anyhow = "1.0"
leaky-bucket = "0.12.1" leaky-bucket = "0.12"
gaoya = "0.1.2" gaoya = "0.1"
self_update = {version = "0.36.0", features = ["archive-tar", "compression-flate2", "archive-zip", "compression-zip-deflate"]} self_update = { version = "0.36", features = [
"archive-tar",
"compression-flate2",
"archive-zip",
"compression-zip-deflate",
] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3"
httpmock = "0.6.6" httpmock = "0.6"
assert_cmd = "2.0.4" assert_cmd = "2.0"
predicates = "2.1.1" predicates = "3.0"
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -11,7 +11,7 @@ rm ferox-*.state
# dependency management # dependency management
[tasks.upgrade-deps] [tasks.upgrade-deps]
command = "cargo" command = "cargo"
args = ["upgrade", "--exclude", "indicatif"] args = ["upgrade"]
[tasks.update] [tasks.update]
command = "cargo" command = "cargo"

View File

@@ -97,8 +97,20 @@ sudo apt update && sudo apt install -y feroxbuster
#### Linux (32 and 64-bit) & MacOS #### Linux (32 and 64-bit) & MacOS
Install to a particular directory
``` ```
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/master/install-nix.sh | bash curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/main/install-nix.sh | bash -s $HOME/.local/bin
```
Install to current working directory
```
curl -sL https://raw.githubusercontent.com/epi052/feroxbuster/main/install-nix.sh | bash
```
#### MacOS via Homebrew
```
brew install feroxbuster
``` ```
#### Windows x86_64 #### Windows x86_64
@@ -109,6 +121,12 @@ Expand-Archive .\feroxbuster.zip
.\feroxbuster\feroxbuster.exe -V .\feroxbuster\feroxbuster.exe -V
``` ```
#### Windows via Chocolatey
```
choco install feroxbuster
```
#### All others #### All others
Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/). Please refer the the [documentation](https://epi052.github.io/feroxbuster-docs/docs/).
@@ -265,6 +283,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xaeroborg"><img src="https://avatars.githubusercontent.com/u/33274680?v=4?s=100" width="100px;" alt="xaeroborg"/><br /><sub><b>xaeroborg</b></sub></a><br /><a href="#ideas-xaeroborg" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/xaeroborg"><img src="https://avatars.githubusercontent.com/u/33274680?v=4?s=100" width="100px;" alt="xaeroborg"/><br /><sub><b>xaeroborg</b></sub></a><br /><a href="#ideas-xaeroborg" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Luoooio"><img src="https://avatars.githubusercontent.com/u/26653157?v=4?s=100" width="100px;" alt="Luoooio"/><br /><sub><b>Luoooio</b></sub></a><br /><a href="#ideas-Luoooio" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/Luoooio"><img src="https://avatars.githubusercontent.com/u/26653157?v=4?s=100" width="100px;" alt="Luoooio"/><br /><sub><b>Luoooio</b></sub></a><br /><a href="#ideas-Luoooio" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://petruknisme.com"><img src="https://avatars.githubusercontent.com/u/6284204?v=4?s=100" width="100px;" alt="Aan"/><br /><sub><b>Aan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aancw" title="Code">💻</a> <a href="#infra-aancw" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-aancw" title="Ideas, Planning, & Feedback">🤔</a></td> <td align="center" valign="top" width="14.28%"><a href="https://petruknisme.com"><img src="https://avatars.githubusercontent.com/u/6284204?v=4?s=100" width="100px;" alt="Aan"/><br /><sub><b>Aan</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/commits?author=aancw" title="Code">💻</a> <a href="#infra-aancw" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-aancw" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/imBigo"><img src="https://avatars.githubusercontent.com/u/54672433?v=4?s=100" width="100px;" alt="Simon"/><br /><sub><b>Simon</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3AimBigo" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://acut3.github.io/"><img src="https://avatars.githubusercontent.com/u/17295243?v=4?s=100" width="100px;" alt="Nicolas Christin"/><br /><sub><b>Nicolas Christin</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3Aacut3" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DrorDvash"><img src="https://avatars.githubusercontent.com/u/8413651?v=4?s=100" width="100px;" alt="DrDv"/><br /><sub><b>DrDv</b></sub></a><br /><a href="https://github.com/epi052/feroxbuster/issues?q=author%3ADrorDvash" title="Bug reports">🐛</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@@ -13,13 +13,13 @@ LIN64_URL="$BASE_URL/$LIN64_ZIP"
EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf EMOJI_URL=https://gist.github.com/epi052/8196b550ea51d0907ad4b93751b1b57d/raw/6112c9f32ae07922983fdc549c54fd3fb9a38e4c/NotoColorEmoji.ttf
echo "[+] Installing feroxbuster!" INSTALL_DIR="${1:-$(pwd)}"
echo "[+] Installing feroxbuster to ${INSTALL_DIR}!"
which unzip &>/dev/null which unzip &>/dev/null
if [ "$?" = "0" ]; then if [ "$?" != "0" ]; then
echo "[+] unzip found" echo "[!] unzip not found, exiting. "
else
echo "[ ] unzip not found, exiting. "
exit -1 exit -1
fi fi
@@ -27,20 +27,20 @@ if [[ "$(uname)" == "Darwin" ]]; then
echo "[=] Found MacOS, downloading from $MAC_URL" echo "[=] Found MacOS, downloading from $MAC_URL"
curl -sLO "$MAC_URL" curl -sLO "$MAC_URL"
unzip -o "$MAC_ZIP" >/dev/null unzip -o "$MAC_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$MAC_ZIP" rm "$MAC_ZIP"
elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
if [[ $(getconf LONG_BIT) == 32 ]]; then if [[ $(getconf LONG_BIT) == 32 ]]; then
echo "[=] Found 32-bit Linux, downloading from $LIN32_URL" echo "[=] Found 32-bit Linux, downloading from $LIN32_URL"
curl -sLO "$LIN32_URL" curl -sLO "$LIN32_URL"
unzip -o "$LIN32_ZIP" >/dev/null unzip -o "$LIN32_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$LIN32_ZIP" rm "$LIN32_ZIP"
else else
echo "[=] Found 64-bit Linux, downloading from $LIN64_URL" echo "[=] Found 64-bit Linux, downloading from $LIN64_URL"
curl -sLO "$LIN64_URL" curl -sLO "$LIN64_URL"
unzip -o "$LIN64_ZIP" >/dev/null unzip -o "$LIN64_ZIP" -d "${INSTALL_DIR}" >/dev/null
rm "$LIN64_ZIP" rm "$LIN64_ZIP"
fi fi
@@ -60,6 +60,8 @@ elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then
fi fi
fi fi
chmod +x ./feroxbuster chmod +x "${INSTALL_DIR}/feroxbuster"
echo "[+] Installed feroxbuster version $(./feroxbuster -V)" echo "[+] Installed feroxbuster"
echo " [-] path: ${INSTALL_DIR}/feroxbuster"
echo " [-] version: $(${INSTALL_DIR}/feroxbuster -V | awk '{print $2}')"

View File

@@ -24,8 +24,8 @@ _feroxbuster() {
'--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \ '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \
'*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \
'-a+[Sets the User-Agent (default: feroxbuster/2.9.1)]:USER_AGENT: ' \ '-a+[Sets the User-Agent (default: feroxbuster/2.9.4)]:USER_AGENT: ' \
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.9.1)]:USER_AGENT: ' \ '--user-agent=[Sets the User-Agent (default: feroxbuster/2.9.4)]:USER_AGENT: ' \
'*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \ '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
@@ -62,8 +62,8 @@ _feroxbuster() {
'--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]:PARALLEL_SCANS: ' \ '--parallel=[Run parallel feroxbuster instances (one child process per url passed via stdin)]:PARALLEL_SCANS: ' \
'(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]:RATE_LIMIT: ' \ '(--auto-tune)--rate-limit=[Limit number of requests per second (per directory) (default: 0, i.e. no limit)]:RATE_LIMIT: ' \
'--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]:TIME_SPEC: ' \ '--time-limit=[Limit total run time of all scans (ex: --time-limit 10m)]:TIME_SPEC: ' \
'-w+[Path to the wordlist]:FILE:_files' \ '-w+[Path or URL of the wordlist]:FILE:_files' \
'--wordlist=[Path to the wordlist]:FILE:_files' \ '--wordlist=[Path or URL of the wordlist]:FILE:_files' \
'*-I+[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \ '*-I+[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \
'*--dont-collect=[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \ '*--dont-collect=[File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)]:FILE_EXTENSION: ' \
'-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \ '-o+[Output file to write results to (use w/ --json for JSON entries)]:FILE:_files' \
@@ -72,7 +72,7 @@ _feroxbuster() {
'(-u --url)--stdin[Read url(s) from STDIN]' \ '(-u --url)--stdin[Read url(s) from STDIN]' \
'(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \ '(-p --proxy -k --insecure --burp-replay)--burp[Set --proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \ '(-P --replay-proxy -k --insecure)--burp-replay[Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true]' \
'(--rate-limit --auto-bail)--smart[Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true]' \ '(--rate-limit --auto-bail)--smart[Set --auto-tune, --collect-words, and --collect-backups to true]' \
'(--rate-limit --auto-bail)--thorough[Use the same settings as --smart and set --collect-extensions to true]' \ '(--rate-limit --auto-bail)--thorough[Use the same settings as --smart and set --collect-extensions to true]' \
'-A[Use a random User-Agent]' \ '-A[Use a random User-Agent]' \
'--random-agent[Use a random User-Agent]' \ '--random-agent[Use a random User-Agent]' \
@@ -85,8 +85,9 @@ _feroxbuster() {
'-n[Do not scan recursively]' \ '-n[Do not scan recursively]' \
'--no-recursion[Do not scan recursively]' \ '--no-recursion[Do not scan recursively]' \
'(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \ '(-n --no-recursion)--force-recursion[Force recursion attempts on all '\''found'\'' endpoints (still respects recursion depth)]' \
'-e[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ '-e[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)]' \
'--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings]' \ '--extract-links[Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)]' \
'--dont-extract-links[Don'\''t extract links from response body (html, javascript, etc...)]' \
'(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \ '(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \
'--auto-bail[Automatically stop scanning when an excessive amount of errors are encountered]' \ '--auto-bail[Automatically stop scanning when an excessive amount of errors are encountered]' \
'-D[Don'\''t auto-filter wildcard responses]' \ '-D[Don'\''t auto-filter wildcard responses]' \

View File

@@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests') [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests')
[CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)')
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.1)') [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.4)')
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.1)') [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.9.4)')
[CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)')
[CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
@@ -68,8 +68,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)') [CompletionResult]::new('--parallel', 'parallel', [CompletionResultType]::ParameterName, 'Run parallel feroxbuster instances (one child process per url passed via stdin)')
[CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)') [CompletionResult]::new('--rate-limit', 'rate-limit', [CompletionResultType]::ParameterName, 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)')
[CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)') [CompletionResult]::new('--time-limit', 'time-limit', [CompletionResultType]::ParameterName, 'Limit total run time of all scans (ex: --time-limit 10m)')
[CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Path to the wordlist') [CompletionResult]::new('-w', 'w', [CompletionResultType]::ParameterName, 'Path or URL of the wordlist')
[CompletionResult]::new('--wordlist', 'wordlist', [CompletionResultType]::ParameterName, 'Path to the wordlist') [CompletionResult]::new('--wordlist', 'wordlist', [CompletionResultType]::ParameterName, 'Path or URL of the wordlist')
[CompletionResult]::new('-I', 'I', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)') [CompletionResult]::new('-I', 'I', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)')
[CompletionResult]::new('--dont-collect', 'dont-collect', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)') [CompletionResult]::new('--dont-collect', 'dont-collect', [CompletionResultType]::ParameterName, 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)')
[CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)') [CompletionResult]::new('-o', 'o', [CompletionResultType]::ParameterName, 'Output file to write results to (use w/ --json for JSON entries)')
@@ -78,7 +78,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN') [CompletionResult]::new('--stdin', 'stdin', [CompletionResultType]::ParameterName, 'Read url(s) from STDIN')
[CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true') [CompletionResult]::new('--burp', 'burp', [CompletionResultType]::ParameterName, 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true') [CompletionResult]::new('--burp-replay', 'burp-replay', [CompletionResultType]::ParameterName, 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true')
[CompletionResult]::new('--smart', 'smart', [CompletionResultType]::ParameterName, 'Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true') [CompletionResult]::new('--smart', 'smart', [CompletionResultType]::ParameterName, 'Set --auto-tune, --collect-words, and --collect-backups to true')
[CompletionResult]::new('--thorough', 'thorough', [CompletionResultType]::ParameterName, 'Use the same settings as --smart and set --collect-extensions to true') [CompletionResult]::new('--thorough', 'thorough', [CompletionResultType]::ParameterName, 'Use the same settings as --smart and set --collect-extensions to true')
[CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Use a random User-Agent') [CompletionResult]::new('-A', 'A', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
[CompletionResult]::new('--random-agent', 'random-agent', [CompletionResultType]::ParameterName, 'Use a random User-Agent') [CompletionResult]::new('--random-agent', 'random-agent', [CompletionResultType]::ParameterName, 'Use a random User-Agent')
@@ -91,8 +91,9 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
[CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively') [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively') [CompletionResult]::new('--no-recursion', 'no-recursion', [CompletionResultType]::ParameterName, 'Do not scan recursively')
[CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)') [CompletionResult]::new('--force-recursion', 'force-recursion', [CompletionResultType]::ParameterName, 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') [CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)')
[CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings') [CompletionResult]::new('--extract-links', 'extract-links', [CompletionResultType]::ParameterName, 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)')
[CompletionResult]::new('--dont-extract-links', 'dont-extract-links', [CompletionResultType]::ParameterName, 'Don''t extract links from response body (html, javascript, etc...)')
[CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered') [CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')
[CompletionResult]::new('--auto-bail', 'auto-bail', [CompletionResultType]::ParameterName, 'Automatically stop scanning when an excessive amount of errors are encountered') [CompletionResult]::new('--auto-bail', 'auto-bail', [CompletionResultType]::ParameterName, 'Automatically stop scanning when an excessive amount of errors are encountered')
[CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses') [CompletionResult]::new('-D', 'D', [CompletionResultType]::ParameterName, 'Don''t auto-filter wildcard responses')

View File

@@ -19,7 +19,7 @@ _feroxbuster() {
case "${cmd}" in case "${cmd}" in
feroxbuster) feroxbuster)
opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --force-recursion --extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state --update --help --version" opts="-u -p -P -R -a -A -x -m -H -b -Q -f -S -X -W -N -C -s -T -r -k -t -n -d -e -L -w -D -E -B -g -I -v -q -o -U -h -V --url --stdin --resume-from --burp --burp-replay --smart --thorough --proxy --replay-proxy --replay-codes --user-agent --random-agent --extensions --methods --data --headers --cookies --query --add-slash --dont-scan --filter-size --filter-regex --filter-words --filter-lines --filter-status --filter-similar-to --status-codes --timeout --redirects --insecure --threads --no-recursion --depth --force-recursion --extract-links --dont-extract-links --scan-limit --parallel --rate-limit --time-limit --wordlist --auto-tune --auto-bail --dont-filter --collect-extensions --collect-backups --collect-words --dont-collect --verbosity --silent --quiet --json --output --debug-log --no-state --update --help --version"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests' cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests'
cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)'
cand -a 'Sets the User-Agent (default: feroxbuster/2.9.1)' cand -a 'Sets the User-Agent (default: feroxbuster/2.9.4)'
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.9.1)' cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.9.4)'
cand -x 'File extension(s) to search for (ex: -x php -x pdf js)' cand -x 'File extension(s) to search for (ex: -x php -x pdf js)'
cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)' cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)'
cand -m 'Which HTTP request method(s) should be sent (default: GET)' cand -m 'Which HTTP request method(s) should be sent (default: GET)'
@@ -65,8 +65,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --parallel 'Run parallel feroxbuster instances (one child process per url passed via stdin)' cand --parallel 'Run parallel feroxbuster instances (one child process per url passed via stdin)'
cand --rate-limit 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)' cand --rate-limit 'Limit number of requests per second (per directory) (default: 0, i.e. no limit)'
cand --time-limit 'Limit total run time of all scans (ex: --time-limit 10m)' cand --time-limit 'Limit total run time of all scans (ex: --time-limit 10m)'
cand -w 'Path to the wordlist' cand -w 'Path or URL of the wordlist'
cand --wordlist 'Path to the wordlist' cand --wordlist 'Path or URL of the wordlist'
cand -I 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)' cand -I 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)'
cand --dont-collect 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)' cand --dont-collect 'File extension(s) to Ignore while collecting extensions (only used with --collect-extensions)'
cand -o 'Output file to write results to (use w/ --json for JSON entries)' cand -o 'Output file to write results to (use w/ --json for JSON entries)'
@@ -75,7 +75,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand --stdin 'Read url(s) from STDIN' cand --stdin 'Read url(s) from STDIN'
cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true' cand --burp 'Set --proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true' cand --burp-replay 'Set --replay-proxy to http://127.0.0.1:8080 and set --insecure to true'
cand --smart 'Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true' cand --smart 'Set --auto-tune, --collect-words, and --collect-backups to true'
cand --thorough 'Use the same settings as --smart and set --collect-extensions to true' cand --thorough 'Use the same settings as --smart and set --collect-extensions to true'
cand -A 'Use a random User-Agent' cand -A 'Use a random User-Agent'
cand --random-agent 'Use a random User-Agent' cand --random-agent 'Use a random User-Agent'
@@ -88,8 +88,9 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
cand -n 'Do not scan recursively' cand -n 'Do not scan recursively'
cand --no-recursion 'Do not scan recursively' cand --no-recursion 'Do not scan recursively'
cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)' cand --force-recursion 'Force recursion attempts on all ''found'' endpoints (still respects recursion depth)'
cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings' cand -e 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)'
cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings' cand --extract-links 'Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)'
cand --dont-extract-links 'Don''t extract links from response body (html, javascript, etc...)'
cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered' cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'
cand --auto-bail 'Automatically stop scanning when an excessive amount of errors are encountered' cand --auto-bail 'Automatically stop scanning when an excessive amount of errors are encountered'
cand -D 'Don''t auto-filter wildcard responses' cand -D 'Don''t auto-filter wildcard responses'

View File

@@ -1,6 +1,7 @@
use super::utils::{ use super::utils::{
depth, ignored_extensions, methods, report_and_exit, save_state, serialized_type, status_codes, depth, extract_links, ignored_extensions, methods, report_and_exit, save_state,
threads, timeout, user_agent, wordlist, OutputLevel, RequesterPolicy, serialized_type, status_codes, threads, timeout, user_agent, wordlist, OutputLevel,
RequesterPolicy,
}; };
use crate::config::determine_output_level; use crate::config::determine_output_level;
use crate::config::utils::determine_requester_policy; use crate::config::utils::determine_requester_policy;
@@ -214,7 +215,7 @@ pub struct Configuration {
pub no_recursion: bool, pub no_recursion: bool,
/// Extract links from html/javscript /// Extract links from html/javscript
#[serde(default)] #[serde(default = "extract_links")]
pub extract_links: bool, pub extract_links: bool,
/// Append / to each request /// Append / to each request
@@ -328,6 +329,7 @@ impl Default for Configuration {
let kind = serialized_type(); let kind = serialized_type();
let output_level = OutputLevel::Default; let output_level = OutputLevel::Default;
let requester_policy = RequesterPolicy::Default; let requester_policy = RequesterPolicy::Default;
let extract_links = extract_links();
Configuration { Configuration {
kind, kind,
@@ -336,6 +338,7 @@ impl Default for Configuration {
user_agent, user_agent,
replay_codes, replay_codes,
status_codes, status_codes,
extract_links,
replay_client, replay_client,
requester_policy, requester_policy,
dont_filter: false, dont_filter: false,
@@ -355,7 +358,6 @@ impl Default for Configuration {
insecure: false, insecure: false,
redirects: false, redirects: false,
no_recursion: false, no_recursion: false,
extract_links: false,
random_agent: false, random_agent: false,
collect_extensions: false, collect_extensions: false,
collect_backups: false, collect_backups: false,
@@ -398,7 +400,7 @@ impl Configuration {
/// ///
/// - **timeout**: `5` seconds /// - **timeout**: `5` seconds
/// - **redirects**: `false` /// - **redirects**: `false`
/// - **extract-links**: `false` /// - **extract_links**: `true`
/// - **wordlist**: [`DEFAULT_WORDLIST`](constant.DEFAULT_WORDLIST.html) /// - **wordlist**: [`DEFAULT_WORDLIST`](constant.DEFAULT_WORDLIST.html)
/// - **config**: `None` /// - **config**: `None`
/// - **threads**: `50` /// - **threads**: `50`
@@ -807,11 +809,8 @@ impl Configuration {
config.add_slash = true; config.add_slash = true;
} }
if came_from_cli!(args, "extract_links") if came_from_cli!(args, "dont_extract_links") {
|| came_from_cli!(args, "smart") config.extract_links = false;
|| came_from_cli!(args, "thorough")
{
config.extract_links = true;
} }
if came_from_cli!(args, "json") { if came_from_cli!(args, "json") {
@@ -997,7 +996,7 @@ impl Configuration {
update_if_not_default!(&mut conf.redirects, new.redirects, false); update_if_not_default!(&mut conf.redirects, new.redirects, false);
update_if_not_default!(&mut conf.insecure, new.insecure, false); update_if_not_default!(&mut conf.insecure, new.insecure, false);
update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false); update_if_not_default!(&mut conf.force_recursion, new.force_recursion, false);
update_if_not_default!(&mut conf.extract_links, new.extract_links, false); update_if_not_default!(&mut conf.extract_links, new.extract_links, extract_links());
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new()); update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
update_if_not_default!(&mut conf.methods, new.methods, methods()); update_if_not_default!(&mut conf.methods, new.methods, methods());
update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new()); update_if_not_default!(&mut conf.data, new.data, Vec::<u8>::new());

View File

@@ -45,7 +45,7 @@ fn setup_config_test() -> Configuration {
add_slash = true add_slash = true
stdin = true stdin = true
dont_filter = true dont_filter = true
extract_links = true extract_links = false
json = true json = true
save_state = false save_state = false
depth = 1 depth = 1
@@ -98,7 +98,7 @@ fn default_configuration() {
assert!(!config.add_slash); assert!(!config.add_slash);
assert!(!config.force_recursion); assert!(!config.force_recursion);
assert!(!config.redirects); assert!(!config.redirects);
assert!(!config.extract_links); assert!(config.extract_links);
assert!(!config.insecure); assert!(!config.insecure);
assert!(!config.collect_extensions); assert!(!config.collect_extensions);
assert!(!config.collect_backups); assert!(!config.collect_backups);
@@ -305,7 +305,7 @@ fn config_reads_add_slash() {
/// parse the test config and see that the value parsed is correct /// parse the test config and see that the value parsed is correct
fn config_reads_extract_links() { fn config_reads_extract_links() {
let config = setup_config_test(); let config = setup_config_test();
assert!(config.extract_links); assert!(!config.extract_links);
} }
#[test] #[test]

View File

@@ -84,6 +84,11 @@ pub(super) fn depth() -> usize {
4 4
} }
/// default extract links
pub(super) fn extract_links() -> bool {
true
}
/// enum representing the three possible states for informational output (not logging verbosity) /// enum representing the three possible states for informational output (not logging verbosity)
#[derive(Debug, Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OutputLevel { pub enum OutputLevel {

View File

@@ -242,14 +242,6 @@ impl TermOutHandler {
log::trace!("enter: process_response({:?}, {:?})", resp, call_type); log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
async move { async move {
let should_filter = self
.handles
.as_ref()
.unwrap()
.filters
.data
.should_filter_response(&resp, tx_stats.clone());
let contains_sentry = if !self.config.filter_status.is_empty() { let contains_sentry = if !self.config.filter_status.is_empty() {
// -C was used, meaning -s was not and we should ignore the defaults // -C was used, meaning -s was not and we should ignore the defaults
// https://github.com/epi052/feroxbuster/issues/535 // https://github.com/epi052/feroxbuster/issues/535
@@ -261,7 +253,7 @@ impl TermOutHandler {
}; };
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
let should_process_response = contains_sentry && unknown_sentry && !should_filter; let should_process_response = contains_sentry && unknown_sentry;
if should_process_response { if should_process_response {
// print to stdout // print to stdout

View File

@@ -222,7 +222,7 @@ impl ScanHandler {
let current_expectation = self.handles.expected_num_requests_per_dir() as u64; let current_expectation = self.handles.expected_num_requests_per_dir() as u64;
// used in the calculation of bar width down below, see explanation there // used in the calculation of bar width down below, see explanation there
let divisor = self.handles.expected_num_requests_multiplier() as u64 - 1; let divisor = (self.handles.expected_num_requests_multiplier() as u64 - 1).max(1);
// add another `wordlist.len` to the expected per scan tracker in the statistics handler // add another `wordlist.len` to the expected per scan tracker in the statistics handler
self.handles self.handles
@@ -266,7 +266,7 @@ impl ScanHandler {
let bar = scan.progress_bar(); let bar = scan.progress_bar();
// (4000 - 3000) / 2 => 500 words left to send // (4000 - 3000) / 2 => 500 words left to send
let length = bar.length(); let length = bar.length().unwrap_or(1);
let num_words_left = (length - bar.position()) / divisor; let num_words_left = (length - bar.position()) / divisor;
// accumulate each bar's increment value for incrementing the total bar // accumulate each bar's increment value for incrementing the total bar
@@ -339,7 +339,7 @@ impl ScanHandler {
// number of requests have already been sent, we need to adjust the offset into the // number of requests have already been sent, we need to adjust the offset into the
// wordlist to ensure we don't index out of bounds // wordlist to ensure we don't index out of bounds
let adjusted = scan.requests_made_so_far() as f64 / divisor as f64 - 1.0; let adjusted = scan.requests_made_so_far() as f64 / (divisor as f64 - 1.0).max(1.0);
self.get_wordlist(adjusted as usize)? self.get_wordlist(adjusted as usize)?
} else { } else {
self.get_wordlist(scan.requests_made_so_far() as usize)? self.get_wordlist(scan.requests_made_so_far() as usize)?

View File

@@ -147,7 +147,7 @@ impl StatsHandler {
self.stats.errors(), self.stats.errors(),
); );
self.bar.set_message(&msg); self.bar.set_message(msg);
if self.bar.position() < self.stats.total_expected() as u64 { if self.bar.position() < self.stats.total_expected() as u64 {
// don't run off the end when we're a few requests over the expected total // don't run off the end when we're a few requests over the expected total

View File

@@ -15,12 +15,53 @@ use crate::{
ExtractionResult, DEFAULT_METHOD, ExtractionResult, DEFAULT_METHOD,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use reqwest::{Client, StatusCode, Url}; use futures::StreamExt;
use reqwest::{Client, Response, StatusCode, Url};
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use std::{borrow::Cow, collections::HashSet}; use std::{borrow::Cow, collections::HashSet};
/// Wrapper around link extraction logic
/// - create a new Url object based on cli options/args
/// - check if the new Url has already been seen/scanned -> None
/// - make a request to the new Url ? -> Some(response) : None
pub(super) async fn request_link(url: &str, handles: Arc<Handles>) -> Result<Response> {
log::trace!("enter: request_link({})", url);
let ferox_url = FeroxUrl::from_string(url, handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format("", None)?;
let scanned_urls = handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None");
bail!("previously seen url");
}
if (!handles.config.url_denylist.is_empty() || !handles.config.regex_denylist.is_empty())
&& should_deny_url(&new_url, handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?} || {:?}",
url,
handles.config.url_denylist,
handles.config.regex_denylist,
);
}
// make the request and store the response
let new_response = logged_request(&new_url, DEFAULT_METHOD, None, handles.clone()).await?;
log::trace!("exit: request_link -> {:?}", new_response);
Ok(new_response)
}
/// Whether an active scan is recursive or not /// Whether an active scan is recursive or not
#[derive(Debug)] #[derive(Debug, Copy, Clone)]
enum RecursionStatus { enum RecursionStatus {
/// Scan is recursive /// Scan is recursive
Recursive, Recursive,
@@ -121,91 +162,140 @@ impl<'a> Extractor<'a> {
/// given a set of links from a normal http body response, task the request handler to make /// given a set of links from a normal http body response, task the request handler to make
/// the requests /// the requests
pub async fn request_links(&mut self, links: HashSet<String>) -> Result<()> { pub async fn request_links(
&mut self,
links: HashSet<String>,
) -> Result<Option<tokio::task::JoinHandle<()>>> {
log::trace!("enter: request_links({:?})", links); log::trace!("enter: request_links({:?})", links);
if links.is_empty() { if links.is_empty() {
return Ok(()); return Ok(None);
} }
self.update_stats(links.len())?;
// create clones/remove use of self of/from everything the async move block will need to function
let cloned_scanned_urls = self.handles.ferox_scans()?;
let cloned_handles = self.handles.clone();
let cloned_url = self.url.clone();
let threads = self.handles.config.threads;
let recursive = if self.handles.config.no_recursion { let recursive = if self.handles.config.no_recursion {
RecursionStatus::NotRecursive RecursionStatus::NotRecursive
} else { } else {
RecursionStatus::Recursive RecursionStatus::Recursive
}; };
let scanned_urls = self.handles.ferox_scans()?; let link_request_task = tokio::spawn(async move {
self.update_stats(links.len())?; let producers = futures::stream::iter(links.into_iter())
.map(|link| {
// another clone to satisfy the async move block
let inner_clone = cloned_handles.clone();
for link in links { (
let mut resp = match self.request_link(&link).await { tokio::spawn(async move { request_link(&link, inner_clone).await }),
Ok(resp) => resp, cloned_handles.clone(),
Err(_) => continue, cloned_scanned_urls.clone(),
}; recursive,
cloned_url.clone(),
)
})
.for_each_concurrent(
threads,
|(join_handle, c_handles, c_scanned_urls, c_recursive, og_url)| async move {
match join_handle.await {
Ok(Ok(reqwest_response)) => {
let mut resp = FeroxResponse::from(
reqwest_response,
&og_url,
DEFAULT_METHOD,
c_handles.config.output_level,
)
.await;
// filter if necessary // filter if necessary
if self if c_handles
.handles .filters
.filters .data
.data .should_filter_response(&resp, c_handles.stats.tx.clone())
.should_filter_response(&resp, self.handles.stats.tx.clone()) {
{ return;
continue; }
}
// request and report assumed file // request and report assumed file
if resp.is_file() || !resp.is_directory() { if resp.is_file() || !resp.is_directory() {
log::debug!("Extracted File: {}", resp); log::debug!("Extracted File: {}", resp);
scanned_urls.add_file_scan(resp.url().as_str(), ScanOrder::Latest); c_scanned_urls
.add_file_scan(resp.url().as_str(), ScanOrder::Latest);
if self.handles.config.collect_extensions { if c_handles.config.collect_extensions {
resp.parse_extension(self.handles.clone())?; // no real reason this should fail
} resp.parse_extension(c_handles.clone()).unwrap();
}
if let Err(e) = resp.send_report(self.handles.output.tx.clone()) { if let Err(e) = resp.send_report(c_handles.output.tx.clone()) {
log::warn!("Could not send FeroxResponse to output handler: {}", e); log::warn!(
} "Could not send FeroxResponse to output handler: {}",
e
);
}
continue; return;
} }
if matches!(recursive, RecursionStatus::Recursive) { if matches!(c_recursive, RecursionStatus::Recursive) {
log::debug!("Extracted Directory: {}", resp); log::debug!("Extracted Directory: {}", resp);
if !resp.url().as_str().ends_with('/') if !resp.url().as_str().ends_with('/')
&& (resp.status().is_success() && (resp.status().is_success()
|| matches!(resp.status(), &StatusCode::FORBIDDEN)) || matches!(resp.status(), &StatusCode::FORBIDDEN))
{ {
// if the url doesn't end with a / // if the url doesn't end with a /
// and the response code is either a 2xx or 403 // and the response code is either a 2xx or 403
// since all of these are 2xx or 403, recursion is only attempted if the // since all of these are 2xx or 403, recursion is only attempted if the
// url ends in a /. I am actually ok with adding the slash and not // url ends in a /. I am actually ok with adding the slash and not
// adding it, as both have merit. Leaving it in for now to see how // adding it, as both have merit. Leaving it in for now to see how
// things turn out (current as of: v1.1.0) // things turn out (current as of: v1.1.0)
resp.set_url(&format!("{}/", resp.url())); resp.set_url(&format!("{}/", resp.url()));
} }
if c_handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if c_handles
.config
.status_codes
.contains(&resp.status().as_u16())
{
send_try_recursion_command(c_handles.clone(), resp)
.await
.unwrap_or_default();
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(c_handles.clone(), resp)
.await
.unwrap_or_default();
}
}
}
Ok(Err(err)) => {
log::warn!("Error during link extraction: {}", err);
}
Err(err) => {
log::warn!("JoinError during link extraction: {}", err);
}
}
},
);
// wait for the requests to finish
producers.await;
});
if self.handles.config.filter_status.is_empty() {
// -C wasn't used, so -s is the only 'filter' left to account for
if self
.handles
.config
.status_codes
.contains(&resp.status().as_u16())
{
send_try_recursion_command(self.handles.clone(), resp).await?;
}
} else {
// -C was used, that means the filters above would have removed
// those responses, and anything else should be let through
send_try_recursion_command(self.handles.clone(), resp).await?;
}
}
}
log::trace!("exit: request_links"); log::trace!("exit: request_links");
Ok(()) Ok(Some(link_request_task))
} }
/// wrapper around link extraction via html attributes /// wrapper around link extraction via html attributes
@@ -415,56 +505,6 @@ impl<'a> Extractor<'a> {
Ok(()) Ok(())
} }
/// Wrapper around link extraction logic
/// - create a new Url object based on cli options/args
/// - check if the new Url has already been seen/scanned -> None
/// - make a request to the new Url ? -> Some(response) : None
pub(super) async fn request_link(&self, url: &str) -> Result<FeroxResponse> {
log::trace!("enter: request_link({})", url);
let ferox_url = FeroxUrl::from_string(url, self.handles.clone());
// create a url based on the given command line options
let new_url = ferox_url.format("", None)?;
let scanned_urls = self.handles.ferox_scans()?;
if scanned_urls.get_scan_by_url(new_url.as_ref()).is_some() {
//we've seen the url before and don't need to scan again
log::trace!("exit: request_link -> None");
bail!("previously seen url");
}
if (!self.handles.config.url_denylist.is_empty()
|| !self.handles.config.regex_denylist.is_empty())
&& should_deny_url(&new_url, self.handles.clone())?
{
// can't allow a denied url to be requested
bail!(
"prevented request to {} due to {:?} || {:?}",
url,
self.handles.config.url_denylist,
self.handles.config.regex_denylist,
);
}
// make the request and store the response
let new_response =
logged_request(&new_url, DEFAULT_METHOD, None, self.handles.clone()).await?;
let new_ferox_response = FeroxResponse::from(
new_response,
url,
DEFAULT_METHOD,
self.handles.config.output_level,
)
.await;
log::trace!("exit: request_link -> {:?}", new_ferox_response);
Ok(new_ferox_response)
}
/// Entry point to perform link extraction from robots.txt /// Entry point to perform link extraction from robots.txt
/// ///
/// `base_url` can have paths and subpaths, however robots.txt will be requested from the /// `base_url` can have paths and subpaths, however robots.txt will be requested from the

View File

@@ -1,4 +1,5 @@
use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX}; use super::builder::{LINKFINDER_REGEX, ROBOTS_TXT_REGEX, URL_CHARS_REGEX};
use super::container::request_link;
use super::*; use super::*;
use crate::config::{Configuration, OutputLevel}; use crate::config::{Configuration, OutputLevel};
use crate::scan_manager::ScanOrder; use crate::scan_manager::ScanOrder;
@@ -360,13 +361,13 @@ async fn request_link_happy_path() -> Result<()> {
then.status(200).body("this is a test"); then.status(200).body("this is a test");
}); });
let r_resp = ROBOTS_EXT.request_link(&srv.url("/login.php")).await?; let r_resp = request_link(&srv.url("/login.php"), ROBOTS_EXT.handles.clone()).await?;
let b_resp = BODY_EXT.request_link(&srv.url("/login.php")).await?; let b_resp = request_link(&srv.url("/login.php"), BODY_EXT.handles.clone()).await?;
assert!(matches!(r_resp.status(), &StatusCode::OK)); assert!(matches!(r_resp.status(), StatusCode::OK));
assert!(matches!(b_resp.status(), &StatusCode::OK)); assert!(matches!(b_resp.status(), StatusCode::OK));
assert_eq!(r_resp.content_length(), 14); assert_eq!(r_resp.content_length().unwrap(), 14);
assert_eq!(b_resp.content_length(), 14); assert_eq!(b_resp.content_length().unwrap(), 14);
assert_eq!(mock.hits(), 2); assert_eq!(mock.hits(), 2);
Ok(()) Ok(())
} }
@@ -390,8 +391,8 @@ async fn request_link_bails_on_seen_url() -> Result<()> {
let robots = setup_extractor(ExtractionTarget::RobotsTxt, scans.clone()); let robots = setup_extractor(ExtractionTarget::RobotsTxt, scans.clone());
let body = setup_extractor(ExtractionTarget::ResponseBody, scans); let body = setup_extractor(ExtractionTarget::ResponseBody, scans);
let r_resp = robots.request_link(&served).await; let r_resp = request_link(&served, robots.handles.clone()).await;
let b_resp = body.request_link(&served).await; let b_resp = request_link(&served, body.handles.clone()).await;
assert!(r_resp.is_err()); assert!(r_resp.is_err());
assert!(b_resp.is_err()); assert!(b_resp.is_err());

View File

@@ -1,6 +1,8 @@
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use futures::future;
use scraper::{Html, Selector}; use scraper::{Html, Selector};
use uuid::Uuid; use uuid::Uuid;
@@ -276,138 +278,185 @@ impl HeuristicTests {
None None
}; };
// 4 is due to the array in the nested for loop below // no matter what, we want an empty extension for the base case
let mut responses = Vec::with_capacity(4); let mut extensions = vec!["".to_string()];
// and then we want to add any extensions that was specified
// or has since been added to the running config
for ext in &self.handles.config.extensions {
extensions.push(format!(".{}", ext));
}
// for every method, attempt to id its 404 response // for every method, attempt to id its 404 response
// //
// a good example of one where the GET/POST differ is on hackthebox: // a good example of one where the GET/POST differ is on hackthebox:
// - http://prd.m.rendering-api.interface.htb/api // - http://prd.m.rendering-api.interface.htb/api
//
// a good example of one where the heuristics return a 403 and a 404 (apache)
// as well as return two different types of 404s based on the file extension
// - http://10.10.11.198 (Encoding box in normal labs)
//
// both methods and extensions can elicit different responses from a given
// server, so both are considered when building auto-filter rules
for method in self.handles.config.methods.iter() { for method in self.handles.config.methods.iter() {
for (prefix, length) in [("", 1), ("", 3), (".htaccess", 1), ("admin", 1)] { for extension in extensions.iter() {
let path = format!("{prefix}{}", self.unique_string(length)); // build out the 6 paths we'll use
let paths = [
("", 1),
("", 3),
(".htaccess", 1),
(".htaccess", 3),
("admin", 1),
("admin", 3),
]
.map(|(prefix, length)| {
format!("{prefix}{}{extension}", self.unique_string(length))
});
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone()); // allow all 6 requests to fly asynchronously
let responses = future::join_all(paths.into_iter().map(|path| async move {
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
let nonexistent_url = ferox_url.format(&path, slash)?; let Ok(nonexistent_url) = ferox_url.format(&path, slash) else {
return None;
};
// example requests: // example requests:
// - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea // - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea
// - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f // - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
// - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09 // - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09
// - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280 // - http://localhost/.htaccess92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
let response = // - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280
logged_request(&nonexistent_url, method, data, self.handles.clone()).await; // - http://localhost/admin92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
let Ok(response) =
logged_request(&nonexistent_url, method, data, self.handles.clone())
.await else {
return None;
};
req_counter += 1; if !self
.handles
.config
.status_codes
.contains(&response.status().as_u16())
{
// if the response code isn't one that's accepted via -s values, then skip to the next
//
// the default value for -s is all status codes, so unless the user says otherwise
// this won't fire
return None;
}
// continue to next on error Some(
let response = skip_fail!(response); FeroxResponse::from(
response,
&ferox_url.target,
method,
self.handles.config.output_level,
)
.await,
)
}))
.await // await gives vector of options containing feroxresponses
.into_iter()
.flatten() // strip out the none values
.collect::<Vec<_>>();
if !self if responses.len() < 2 {
.handles // don't have enough responses to make a determination, continue to next method
.config log::debug!("not enough responses to make a determination");
.status_codes
.contains(&response.status().as_u16())
{
// if the response code isn't one that's accepted via -s values, then skip to the next
//
// the default value for -s is all status codes, so unless the user says otherwise
// this won't fire
continue; continue;
} }
let ferox_response = FeroxResponse::from( // check the responses for similarities on which we can filter, multiple may be returned
response, let Some((wildcard_filters, wildcard_responses)) = self.examine_404_like_responses(&responses) else {
&ferox_url.target, // no match was found during analysis of responses
method, log::warn!("no match found for 404 responses");
continue;
};
// report to the user, if appropriate
if matches!(
self.handles.config.output_level, self.handles.config.output_level,
) OutputLevel::Default | OutputLevel::Quiet
.await; ) {
// sentry value to control whether or not to print the filter
// used because we only want to print the same filter once
let mut print_sentry;
responses.push(ferox_response); if let Ok(filters) = self.handles.filters.data.filters.read() {
} for new_wildcard in &wildcard_filters {
// reset the sentry for every new wildcard produced by examine_404_like_responses
print_sentry = true;
if responses.len() < 2 { for other in filters.iter() {
// don't have enough responses to make a determination, continue to next method if let Some(other_wildcard) =
responses.clear(); other.as_any().downcast_ref::<WildcardFilter>()
continue; {
} // check the new wildcard against all existing wildcards, if it was added
// on the cli or by a previous directory, don't print it
if new_wildcard.as_ref() == other_wildcard {
print_sentry = false;
break;
}
}
}
// Command::AddFilter, &str (bytes/words/lines), usize (i.e. length associated with the type) // if we're here, we've found a new wildcard that we didn't previously display, print it
let Some(filter) = self.examine_404_like_responses(&responses) else { if print_sentry {
// no match was found during analysis of responses ferox_print(&format!("{}", new_wildcard), &PROGRESS_PRINTER);
responses.clear();
continue;
};
// report to the user, if appropriate
if matches!(
self.handles.config.output_level,
OutputLevel::Default | OutputLevel::Quiet
) {
// sentry value to control whether or not to print the filter
// used because we only want to print the same filter once
let mut print_sentry = true;
if let Ok(filters) = self.handles.filters.data.filters.read() {
for other in filters.iter() {
if let Some(other_wildcard) =
other.as_any().downcast_ref::<WildcardFilter>()
{
if &*filter == other_wildcard {
print_sentry = false;
break;
} }
} }
} }
} }
if print_sentry { // create the new filter
ferox_print(&format!("{}", filter), &PROGRESS_PRINTER); for wildcard in wildcard_filters {
self.handles.filters.send(Command::AddFilter(wildcard))?;
}
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
//
// in addition, we'll create a similarity filter as a fallback
for resp in wildcard_responses {
let hash = SIM_HASHER.create_signature(preprocess(resp.text()).iter());
let sim_filter = SimilarityFilter {
hash,
original_url: resp.url().to_string(),
};
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
if resp.is_directory() {
// response is either a 3XX with a Location header that matches url + '/'
// or it's a 2XX that ends with a '/'
// or it's a 403 that ends with a '/'
// set the wildcard flag to true, so we can check it when preventing
// recursion in event_handlers/scans.rs
// we'd need to clone the response to give ownership to the global list anyway
// so we'll also use that clone to set the wildcard flag
let mut cloned_resp = resp.clone();
cloned_resp.set_wildcard(true);
// add the response to the global list of responses
RESPONSES.insert(cloned_resp);
// function-internal magic number, indicates that we've detected a wildcard directory
req_counter += 100;
}
} }
} }
// create the new filter
self.handles.filters.send(Command::AddFilter(filter))?;
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
//
// in addition, we'll create a similarity filter as a fallback
let hash = SIM_HASHER.create_signature(preprocess(responses[0].text()).iter());
let sim_filter = SimilarityFilter {
hash,
original_url: responses[0].url().to_string(),
};
self.handles
.filters
.send(Command::AddFilter(Box::new(sim_filter)))?;
if responses[0].is_directory() {
// response is either a 3XX with a Location header that matches url + '/'
// or it's a 2XX that ends with a '/'
// or it's a 403 that ends with a '/'
// set the wildcard flag to true, so we can check it when preventing
// recursion in event_handlers/scans.rs
responses[0].set_wildcard(true);
// add the response to the global list of responses
RESPONSES.insert(responses[0].clone());
// function-internal magic number, indicates that we've detected a wildcard directory
req_counter += 100;
}
// reset the responses for the next method, if it exists
responses.clear();
} }
log::trace!("exit: detect_404_like_responses"); log::trace!("exit: detect_404_like_responses");
let retval = if req_counter > 100 { let retval = if req_counter >= 100 {
WildcardResult::WildcardDirectory(req_counter) WildcardResult::WildcardDirectory(req_counter)
} else { } else {
WildcardResult::FourOhFourLike(req_counter) WildcardResult::FourOhFourLike(req_counter)
@@ -416,96 +465,138 @@ impl HeuristicTests {
Ok(Some(retval)) Ok(Some(retval))
} }
/// for all responses, examine chars/words/lines /// for all responses, group them by status code, then examine chars/words/lines.
/// if all responses respective lengths match each other, we can assume /// if all responses' respective lengths within a status code grouping match
/// that will remain true for subsequent non-existent urls /// each other, we can assume that will remain true for subsequent non-existent urls
/// ///
/// values are examined from most to least specific (content length, word count, line count) /// within a status code grouping, values are examined from most to
fn examine_404_like_responses( /// least specific (content length, word count, line count)
#[allow(clippy::vec_box)] // the box is needed in the caller and i dont feel like changing it
fn examine_404_like_responses<'a>(
&self, &self,
responses: &[FeroxResponse], responses: &'a [FeroxResponse],
) -> Option<Box<WildcardFilter>> { ) -> Option<(Vec<Box<WildcardFilter>>, Vec<&'a FeroxResponse>)> {
// aside from word/line/byte counts, additional discriminators are status code
// extension, and request method. The request method and extension are handled by
// the caller, since they're part of the request and make up the nested for loops
// in detect_404_like_responses.
//
// The status code is handled here, since it's part of the response to catch cases
// where we have something like a 403 and a 404
let mut size_sentry = true; let mut size_sentry = true;
let mut word_sentry = true; let mut word_sentry = true;
let mut line_sentry = true; let mut line_sentry = true;
let method = responses[0].method(); // returned vec of boxed wildcard filters
let status_code = responses[0].status(); let mut wildcards = Vec::new();
let content_length = responses[0].content_length();
let word_count = responses[0].word_count();
let line_count = responses[0].line_count();
for response in &responses[1..] { // returned vec of ferox responses that are needed for additional
// if any of the responses differ in length, that particular // analysis
// response length type is no longer a candidate for filtering let mut wild_responses = Vec::new();
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count { // mapping of grouped responses to status code
word_sentry = false; let mut grouped_responses = HashMap::new();
}
if response.line_count() != line_count { // iterate over all responses and add each response to its
line_sentry = false; // corresponding status code group
} for response in responses {
grouped_responses
.entry(response.status())
.or_insert_with(Vec::new)
.push(response);
} }
if !size_sentry && !word_sentry && !line_sentry { // iterate over each grouped response and determine the most specific
// none of the response lengths match, so we can't filter on any of them // filter that can be applied to all responses in the group, i.e.
return None; // start from byte count and work 'out' to line count
for response_group in grouped_responses.values() {
if response_group.len() < 2 {
// not enough responses to make a determination
continue;
}
let method = response_group[0].method();
let status_code = response_group[0].status();
let content_length = response_group[0].content_length();
let word_count = response_group[0].word_count();
let line_count = response_group[0].line_count();
for response in &response_group[1..] {
// if any of the responses differ in length, that particular
// response length type is no longer a candidate for filtering
if response.content_length() != content_length {
size_sentry = false;
}
if response.word_count() != word_count {
word_sentry = false;
}
if response.line_count() != line_count {
line_sentry = false;
}
}
if !size_sentry && !word_sentry && !line_sentry {
// none of the response lengths match, so we can't filter on any of them
continue;
}
let mut wildcard = WildcardFilter {
content_length: None,
line_count: None,
word_count: None,
method: method.to_string(),
status_code: status_code.as_u16(),
dont_filter: self.handles.config.dont_filter,
};
match (size_sentry, word_sentry, line_sentry) {
(true, true, true) => {
// all three types of length match, so we can't filter on any of them
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, true, false) => {
// content length and word count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
}
(true, false, true) => {
// content length and line count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.line_count = Some(line_count);
}
(false, true, true) => {
// word count and line count match, so we can filter on either
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, false, false) => {
// content length matches, so we can filter on that
wildcard.content_length = Some(content_length);
}
(false, true, false) => {
// word count matches, so we can filter on that
wildcard.word_count = Some(word_count);
}
(false, false, true) => {
// line count matches, so we can filter on that
wildcard.line_count = Some(line_count);
}
(false, false, false) => {
// none of the length types match, so we can't filter on any of them
unreachable!("no wildcard size matches; handled by the if statement above");
}
};
wild_responses.push(response_group[0]);
wildcards.push(Box::new(wildcard));
} }
let mut wildcard = WildcardFilter { Some((wildcards, wild_responses))
content_length: None,
line_count: None,
word_count: None,
method: method.to_string(),
status_code: status_code.as_u16(),
dont_filter: self.handles.config.dont_filter,
};
match (size_sentry, word_sentry, line_sentry) {
(true, true, true) => {
// all three types of length match, so we can't filter on any of them
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, true, false) => {
// content length and word count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.word_count = Some(word_count);
}
(true, false, true) => {
// content length and line count match, so we can filter on either
wildcard.content_length = Some(content_length);
wildcard.line_count = Some(line_count);
}
(false, true, true) => {
// word count and line count match, so we can filter on either
wildcard.word_count = Some(word_count);
wildcard.line_count = Some(line_count);
}
(true, false, false) => {
// content length matches, so we can filter on that
wildcard.content_length = Some(content_length);
}
(false, true, false) => {
// word count matches, so we can filter on that
wildcard.word_count = Some(word_count);
}
(false, false, true) => {
// line count matches, so we can filter on that
wildcard.line_count = Some(line_count);
}
(false, false, false) => {
// none of the length types match, so we can't filter on any of them
unreachable!("no wildcard size matches; handled by the if statement above");
}
};
Some(Box::new(wildcard))
} }
} }

View File

@@ -31,7 +31,7 @@ use feroxbuster::{
TermOutHandler, SCAN_COMPLETE, TermOutHandler, SCAN_COMPLETE,
}, },
filters, heuristics, logger, filters, heuristics, logger,
progress::{PROGRESS_BAR, PROGRESS_PRINTER}, progress::PROGRESS_PRINTER,
scan_manager::{self, ScanType}, scan_manager::{self, ScanType},
scanner, scanner,
utils::{fmt_err, slugify_filename}, utils::{fmt_err, slugify_filename},
@@ -220,7 +220,6 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
// PROGRESS_PRINTER and PROGRESS_BAR have been used at least once. This call satisfies // PROGRESS_PRINTER and PROGRESS_BAR have been used at least once. This call satisfies
// that constraint // that constraint
PROGRESS_PRINTER.println(""); PROGRESS_PRINTER.println("");
PROGRESS_BAR.join().unwrap();
}); });
// check if update_app is true // check if update_app is true
@@ -237,19 +236,50 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
exit(0); exit(0);
} }
// cloning an Arc is cheap (it's basically a pointer into the heap) let words = if config.wordlist.starts_with("http") {
// so that will allow for cheap/safe sharing of a single wordlist across multi-target scans // found a url scheme, attempt to download the wordlist
// as well as additional directories found as part of recursion let response = config.client.get(&config.wordlist).send().await?;
let words = match get_unique_words_from_wordlist(&config.wordlist) {
Ok(w) => w,
Err(err) => {
let secondary = Path::new(SECONDARY_WORDLIST);
if secondary.exists() { if !response.status().is_success() {
eprintln!("Found wordlist in secondary location"); // status code isn't a 200, bail
get_unique_words_from_wordlist(SECONDARY_WORDLIST)? bail!(
} else { "[{}] Unable to download wordlist from url: {}",
return Err(err); response.status().as_str(),
config.wordlist
);
}
// attempt to get the filename from the url's path
let Some(path_segments) = response
.url()
.path_segments() else {
bail!("Unable to parse path from url: {}", response.url());
};
let Some(filename) = path_segments.last() else {
bail!("Unable to parse filename from url's path: {}", response.url().path());
};
let filename = filename.to_string();
// read the body and write it to disk, then use existing code to read the wordlist
let body = response.text().await?;
std::fs::write(&filename, body)?;
get_unique_words_from_wordlist(&filename)?
} else {
match get_unique_words_from_wordlist(&config.wordlist) {
Ok(w) => w,
Err(err) => {
let secondary = Path::new(SECONDARY_WORDLIST);
if secondary.exists() {
eprintln!("Found wordlist in secondary location");
get_unique_words_from_wordlist(SECONDARY_WORDLIST)?
} else {
return Err(err);
}
} }
} }
}; };

View File

@@ -92,8 +92,9 @@ pub fn initialize() -> Command {
.num_args(0) .num_args(0)
.help_heading("Composite settings") .help_heading("Composite settings")
.conflicts_with_all(["rate_limit", "auto_bail"]) .conflicts_with_all(["rate_limit", "auto_bail"])
.help("Set --extract-links, --auto-tune, --collect-words, and --collect-backups to true"), .help("Set --auto-tune, --collect-words, and --collect-backups to true"),
).arg( )
.arg(
Arg::new("thorough") Arg::new("thorough")
.long("thorough") .long("thorough")
.num_args(0) .num_args(0)
@@ -433,7 +434,15 @@ pub fn initialize() -> Command {
.long("extract-links") .long("extract-links")
.num_args(0) .num_args(0)
.help_heading("Scan settings") .help_heading("Scan settings")
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings") .hide(true)
.help("Extract links from response body (html, javascript, etc...); make new requests based on findings (default: true)")
)
.arg(
Arg::new("dont_extract_links")
.long("dont-extract-links")
.num_args(0)
.help_heading("Scan settings")
.help("Don't extract links from response body (html, javascript, etc...)")
) )
.arg( .arg(
Arg::new("scan_limit") Arg::new("scan_limit")
@@ -477,7 +486,7 @@ pub fn initialize() -> Command {
.long("wordlist") .long("wordlist")
.value_hint(ValueHint::FilePath) .value_hint(ValueHint::FilePath)
.value_name("FILE") .value_name("FILE")
.help("Path to the wordlist") .help("Path or URL of the wordlist")
.help_heading("Scan settings") .help_heading("Scan settings")
.num_args(1), .num_args(1),
).arg( ).arg(
@@ -515,7 +524,8 @@ pub fn initialize() -> Command {
.num_args(0) .num_args(0)
.help_heading("Dynamic collection settings") .help_heading("Dynamic collection settings")
.help("Automatically request likely backup extensions for \"found\" urls") .help("Automatically request likely backup extensions for \"found\" urls")
).arg( )
.arg(
Arg::new("collect_words") Arg::new("collect_words")
.short('g') .short('g')
.long("collect-words") .long("collect-words")
@@ -684,9 +694,6 @@ EXAMPLES:
Pass auth token via query parameter Pass auth token via query parameter
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF ./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
Find links in javascript/html and make additional requests based on results
./feroxbuster -u http://127.1 --extract-links
Ludicrous speed... go! Ludicrous speed... go!
./feroxbuster -u http://127.1 --threads 200 ./feroxbuster -u http://127.1 --threads 200

View File

@@ -1,4 +1,6 @@
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle}; use std::time::Duration;
use indicatif::{HumanDuration, MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use lazy_static::lazy_static; use lazy_static::lazy_static;
lazy_static! { lazy_static! {
@@ -31,30 +33,68 @@ pub enum BarType {
/// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html) /// Add an [indicatif::ProgressBar](https://docs.rs/indicatif/latest/indicatif/struct.ProgressBar.html)
/// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html) /// to the global [PROGRESS_BAR](../config/struct.PROGRESS_BAR.html)
pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar { pub fn add_bar(prefix: &str, length: u64, bar_type: BarType) -> ProgressBar {
let mut style = ProgressStyle::default_bar().progress_chars("#>-"); let mut style = ProgressStyle::default_bar()
.progress_chars("#>-")
.with_key(
"smoothed_per_sec",
|state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write| match (
state.pos(),
state.elapsed().as_millis(),
) {
// https://github.com/console-rs/indicatif/issues/394#issuecomment-1309971049
//
// indicatif released a change to how they reported eta/per_sec
// and the results looked really weird based on how we use the progress
// bars. this fixes that
(pos, elapsed_ms) if elapsed_ms > 0 => {
write!(w, "{:.0}/s", pos as f64 * 1000_f64 / elapsed_ms as f64).unwrap()
}
_ => write!(w, "-").unwrap(),
},
)
.with_key(
"smoothed_eta",
|state: &indicatif::ProgressState, w: &mut dyn std::fmt::Write| match (
state.pos(),
state.len(),
) {
(pos, Some(len)) => write!(
w,
"{:#}",
HumanDuration(Duration::from_millis(
(state.elapsed().as_millis()
* ((len as u128).checked_sub(pos as u128).unwrap_or(1))
.checked_div(pos as u128)
.unwrap_or(1)) as u64
))
)
.unwrap(),
_ => write!(w, "-").unwrap(),
},
);
style = match bar_type { style = match bar_type {
BarType::Hidden => style.template(""), BarType::Hidden => style.template("").unwrap(),
BarType::Default => style.template( BarType::Default => style
"[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {per_sec:7} {prefix} {msg}", .template("[{bar:.cyan/blue}] - {elapsed:<4} {pos:>7}/{len:7} {smoothed_per_sec:7} {prefix} {msg}")
), .unwrap(),
BarType::Message => style.template(&format!( BarType::Message => style
.template(&format!(
"[{{bar:.cyan/blue}}] - {{elapsed:<4}} {{pos:>7}}/{{len:7}} {:7} {{prefix}} {{msg}}", "[{{bar:.cyan/blue}}] - {{elapsed:<4}} {{pos:>7}}/{{len:7}} {:7} {{prefix}} {{msg}}",
"-" "-"
)), ))
BarType::Total => { .unwrap(),
style.template("[{bar:.yellow/blue}] - {elapsed:<4} {pos:>7}/{len:7} {eta:7} {msg}") BarType::Total => style
} .template("[{bar:.yellow/blue}] - {elapsed:<4} {pos:>7}/{len:7} {smoothed_eta:7} {msg}")
BarType::Quiet => style.template("Scanning: {prefix}"), .unwrap(),
BarType::Quiet => style.template("Scanning: {prefix}").unwrap(),
}; };
let progress_bar = PROGRESS_BAR.add(ProgressBar::new(length)); PROGRESS_BAR.add(
ProgressBar::new(length)
progress_bar.set_style(style); .with_style(style)
.with_prefix(prefix.to_string()),
progress_bar.set_prefix(prefix); )
progress_bar
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -170,7 +170,8 @@ impl FeroxResponse {
/// free the `text` data, reducing memory usage /// free the `text` data, reducing memory usage
pub fn drop_text(&mut self) { pub fn drop_text(&mut self) {
self.text = String::new(); self.text.clear(); // length is set to 0
self.text.shrink_to_fit(); // allocated capacity shrinks to reflect the new size
} }
/// Make a reasonable guess at whether the response is a file or not /// Make a reasonable guess at whether the response is a file or not
@@ -394,7 +395,14 @@ impl FeroxResponse {
pub fn send_report(self, report_sender: CommandSender) -> Result<()> { pub fn send_report(self, report_sender: CommandSender) -> Result<()> {
log::trace!("enter: send_report({:?}", report_sender); log::trace!("enter: send_report({:?}", report_sender);
report_sender.send(Command::Report(Box::new(self)))?; // there's no reason to send the response body across the mpsc
//
// the only possible reason is for filtering on the body, but both `send_report`
// calls are gated behind checks for `should_filter_response`
let mut me = self;
me.drop_text();
report_sender.send(Command::Report(Box::new(me)))?;
log::trace!("exit: send_report"); log::trace!("exit: send_report");
Ok(()) Ok(())

View File

@@ -159,7 +159,7 @@ impl FeroxScan {
if pb.position() > self.num_requests { if pb.position() > self.num_requests {
pb.finish() pb.finish()
} else { } else {
pb.finish_at_current_pos() pb.abandon()
} }
} }
} }

View File

@@ -379,7 +379,7 @@ impl FeroxScans {
.unwrap_or_else(|e| log::warn!("Could not cancel task: {}", e)); .unwrap_or_else(|e| log::warn!("Could not cancel task: {}", e));
let pb = selected.progress_bar(); let pb = selected.progress_bar();
num_cancelled += pb.length() as usize - pb.position() as usize; num_cancelled += pb.length().unwrap_or(0) as usize - pb.position() as usize;
} else { } else {
self.menu.println("Ok, doing nothing..."); self.menu.println("Ok, doing nothing...");
} }

View File

@@ -72,7 +72,7 @@ fn add_url_to_list_of_scanned_urls_with_known_url() {
url, url,
ScanType::Directory, ScanType::Directory,
ScanOrder::Latest, ScanOrder::Latest,
pb.length(), pb.length().unwrap(),
OutputLevel::Default, OutputLevel::Default,
Some(pb), Some(pb),
); );
@@ -94,7 +94,7 @@ fn stop_progress_bar_stops_bar() {
url, url,
ScanType::Directory, ScanType::Directory,
ScanOrder::Latest, ScanOrder::Latest,
pb.length(), pb.length().unwrap(),
OutputLevel::Default, OutputLevel::Default,
Some(pb), Some(pb),
); );
@@ -152,7 +152,7 @@ async fn call_display_scans() {
url, url,
ScanType::Directory, ScanType::Directory,
ScanOrder::Latest, ScanOrder::Latest,
pb.length(), pb.length().unwrap(),
OutputLevel::Default, OutputLevel::Default,
Some(pb), Some(pb),
); );
@@ -160,7 +160,7 @@ async fn call_display_scans() {
url_two, url_two,
ScanType::Directory, ScanType::Directory,
ScanOrder::Latest, ScanOrder::Latest,
pb_two.length(), pb_two.length().unwrap(),
OutputLevel::Default, OutputLevel::Default,
Some(pb_two), Some(pb_two),
); );
@@ -469,7 +469,7 @@ fn feroxstates_feroxserialize_implementation() {
r#""headers""#, r#""headers""#,
r#""queries":[]"#, r#""queries":[]"#,
r#""no_recursion":false"#, r#""no_recursion":false"#,
r#""extract_links":false"#, r#""extract_links":true"#,
r#""add_slash":false"#, r#""add_slash":false"#,
r#""stdin":false"#, r#""stdin":false"#,
r#""depth":4"#, r#""depth":4"#,

View File

@@ -203,6 +203,9 @@ impl FeroxScanner {
log::info!("Starting scan against: {}", self.target_url); log::info!("Starting scan against: {}", self.target_url);
let mut scan_timer = Instant::now(); let mut scan_timer = Instant::now();
// every time we extract links we'll need to await the task to make sure
// it completes before the scan ends
let mut extraction_tasks = Vec::new();
if self.handles.config.extract_links && matches!(self.order, ScanOrder::Initial) { if self.handles.config.extract_links && matches!(self.order, ScanOrder::Initial) {
// check for robots.txt (cannot be in sub-directories, so limited to Initial) // check for robots.txt (cannot be in sub-directories, so limited to Initial)
@@ -213,7 +216,7 @@ impl FeroxScanner {
.build()?; .build()?;
let result = extractor.extract().await?; let result = extractor.extract().await?;
extractor.request_links(result).await?; extraction_tasks.push(extractor.request_links(result).await?)
} }
let scanned_urls = self.handles.ferox_scans()?; let scanned_urls = self.handles.ferox_scans()?;
@@ -265,7 +268,7 @@ impl FeroxScanner {
let result = extractor.extract_from_dir_listing().await?; let result = extractor.extract_from_dir_listing().await?;
extractor.request_links(result).await?; extraction_tasks.push(extractor.request_links(result).await?);
log::trace!("exit: scan_url -> Directory listing heuristic"); log::trace!("exit: scan_url -> Directory listing heuristic");
@@ -276,22 +279,32 @@ impl FeroxScanner {
self.handles.stats.send(SubtractFromUsizeField( self.handles.stats.send(SubtractFromUsizeField(
TotalExpected, TotalExpected,
progress_bar.length() as usize, progress_bar.length().unwrap_or(0) as usize,
))?; ))?;
} }
let mut message = format!("=> {}", style("Directory listing").blue().bright()); let mut message = format!("=> {}", style("Directory listing").blue().bright());
if !self.handles.config.extract_links { if !self.handles.config.extract_links {
write!(message, " (add {} to scan)", style("-e").bright().yellow())?; write!(
message,
" (remove {} to scan)",
style("--dont-extract-links").bright().yellow()
)?;
} }
progress_bar.reset_eta(); if !self.handles.config.force_recursion {
progress_bar.finish_with_message(&message); for handle in extraction_tasks.into_iter().flatten() {
_ = handle.await;
}
ferox_scan.finish()?; progress_bar.reset_eta();
progress_bar.finish_with_message(message);
return Ok(()); // nothing left to do if we found a dir listing ferox_scan.finish()?;
return Ok(()); // nothing left to do if we found a dir listing
}
} }
} }
@@ -311,7 +324,7 @@ impl FeroxScanner {
style("Wildcard").blue().bright(), style("Wildcard").blue().bright(),
style("stopped").red() style("stopped").red()
); );
progress_bar.set_message(&message); progress_bar.set_message(message);
progress_bar.inc(num_reqs as u64); progress_bar.inc(num_reqs as u64);
} }
Some(WildcardResult::FourOhFourLike(num_reqs)) => { Some(WildcardResult::FourOhFourLike(num_reqs)) => {
@@ -338,7 +351,7 @@ impl FeroxScanner {
let new_words = TF_IDF.read().unwrap().all_words(); let new_words = TF_IDF.read().unwrap().all_words();
let new_words_len = new_words.len(); let new_words_len = new_words.len();
let cur_length = progress_bar.length(); let cur_length = progress_bar.length().unwrap_or(0);
let new_length = cur_length + new_words_len as u64; let new_length = cur_length + new_words_len as u64;
progress_bar.set_length(new_length); progress_bar.set_length(new_length);
@@ -368,6 +381,10 @@ impl FeroxScanner {
scan_timer.elapsed().as_secs_f64(), scan_timer.elapsed().as_secs_f64(),
))?; ))?;
for handle in extraction_tasks.into_iter().flatten() {
_ = handle.await;
}
ferox_scan.finish()?; ferox_scan.finish()?;
log::trace!("exit: scan_url"); log::trace!("exit: scan_url");

View File

@@ -217,7 +217,7 @@ impl Requester {
self.ferox_scan self.ferox_scan
.progress_bar() .progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",)); .set_message(format!("=> 🚦 {styled_direction} scan speed",));
} }
self.policy_data.set_errors(scan_errors); self.policy_data.set_errors(scan_errors);
} else { } else {
@@ -230,7 +230,7 @@ impl Requester {
self.ferox_scan self.ferox_scan
.progress_bar() .progress_bar()
.set_message(&format!("=> 🚦 {styled_direction} scan speed",)); .set_message(format!("=> 🚦 {styled_direction} scan speed",));
} }
} }
@@ -286,7 +286,7 @@ impl Requester {
self.set_rate_limiter(Some(new_limit)).await?; self.set_rate_limiter(Some(new_limit)).await?;
self.ferox_scan self.ferox_scan
.progress_bar() .progress_bar()
.set_message(&format!("=> 🚦 set rate limit ({new_limit}/s)")); .set_message(format!("=> 🚦 set rate limit ({new_limit}/s)"));
} }
self.adjust_limit(trigger, true).await?; self.adjust_limit(trigger, true).await?;
@@ -321,11 +321,11 @@ impl Requester {
// figure out how many requests are skipped as a result // figure out how many requests are skipped as a result
let pb = self.ferox_scan.progress_bar(); let pb = self.ferox_scan.progress_bar();
let num_skipped = pb.length().saturating_sub(pb.position()) as usize; let num_skipped = pb.length().unwrap_or(0).saturating_sub(pb.position()) as usize;
let styled_trigger = style(format!("{trigger:?}")).red(); let styled_trigger = style(format!("{trigger:?}")).red();
pb.set_message(&format!( pb.set_message(format!(
"=> 💀 too many {} ({}) 💀 bailing", "=> 💀 too many {} ({}) 💀 bailing",
styled_trigger, styled_trigger,
self.ferox_scan.num_errors(trigger), self.ferox_scan.num_errors(trigger),
@@ -490,6 +490,7 @@ impl Requester {
.target(ExtractionTarget::ResponseBody) .target(ExtractionTarget::ResponseBody)
.response(&ferox_response) .response(&ferox_response)
.handles(self.handles.clone()) .handles(self.handles.clone())
.url(self.ferox_scan.url())
.build()?; .build()?;
let new_links: HashSet<_>; let new_links: HashSet<_>;
@@ -513,7 +514,11 @@ impl Requester {
} }
if !new_links.is_empty() { if !new_links.is_empty() {
extractor.request_links(new_links).await?; let extraction_task = extractor.request_links(new_links).await?;
if let Some(task) = extraction_task {
_ = task.await;
}
} }
} }

View File

@@ -75,7 +75,12 @@ pub(crate) async fn send_try_recursion_command(
handles: Arc<Handles>, handles: Arc<Handles>,
response: FeroxResponse, response: FeroxResponse,
) -> Result<()> { ) -> Result<()> {
handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?; // make the response mutable so we can drop the body before
// sending it over the mpsc
let mut response = response;
response.drop_text();
handles.send_scan_command(Command::TryRecursion(Box::new(response)))?;
let (tx, rx) = oneshot::channel::<bool>(); let (tx, rx) = oneshot::channel::<bool>();
handles.send_scan_command(Command::Sync(tx))?; handles.send_scan_command(Command::Sync(tx))?;
rx.await?; rx.await?;

View File

@@ -164,7 +164,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
let mock = srv.mock(|when, then| { let mock = srv.mock(|when, then| {
when.method(GET) when.method(GET)
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap()); .path_matches(Regex::new("/[.a-zA-Z0-9]{32,}/").unwrap());
then.status(200).body("this is a test"); then.status(200).body("this is a test");
}); });
@@ -188,7 +188,8 @@ fn test_static_wildcard_request_found() -> Result<(), Box<dyn std::error::Error>
.and(predicate::str::contains("1l")), .and(predicate::str::contains("1l")),
); );
assert_eq!(mock.hits(), 1); assert_eq!(mock.hits(), 6);
Ok(()) Ok(())
} }
@@ -305,11 +306,67 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
.success() .success()
.stdout(predicate::str::contains(srv.url("/"))); .stdout(predicate::str::contains(srv.url("/")));
assert_eq!(mock.hits(), 4); assert_eq!(mock.hits(), 6);
assert_eq!(mock2.hits(), 1); assert_eq!(mock2.hits(), 1);
Ok(()) Ok(())
} }
#[test]
/// test finds a 404-like response that returns a 403 and a 403 directory should still be allowed
/// to be tested for recrusion
fn heuristics_wildcard_test_that_auto_filtering_403s_still_allows_for_recursion_into_403_directories(
) -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let super_long = String::from("92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f09d471004154b83bcc1c6ec49f6f");
let (tmp_dir, file) =
setup_tmp_directory(&["LICENSE".to_string(), super_long.clone()], "wordlist")?;
srv.mock(|when, then| {
when.method(GET)
.path_matches(Regex::new("/.?[a-zA-Z0-9]{32,103}").unwrap());
then.status(403)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
srv.mock(|when, then| {
when.method(GET).path("/LICENSE/");
then.status(403)
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
});
srv.mock(|when, then| {
when.method(GET).path(format!("/LICENSE/{}", super_long));
then.status(200);
});
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.arg("--add-slash")
.unwrap();
teardown_tmp_directory(tmp_dir);
cmd.assert().success().stdout(
predicate::str::contains("GET")
.and(predicate::str::contains(
"Auto-filtering found 404-like response and created new filter",
))
.and(predicate::str::contains("403"))
.and(predicate::str::contains("1l"))
.and(predicate::str::contains("4w"))
.and(predicate::str::contains("46c"))
.and(predicate::str::contains(srv.url("/LICENSE/LICENSE/"))),
);
Ok(())
}
// #[test] // #[test]
// /// test finds a static wildcard and reports as much to stdout and a file // /// test finds a static wildcard and reports as much to stdout and a file
// fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() { // fn heuristics_wildcard_test_with_two_static_wildcards_and_output_to_file() {

View File

@@ -218,3 +218,46 @@ fn main_parallel_creates_output_directory() -> Result<(), Box<dyn std::error::Er
Ok(()) Ok(())
} }
#[test]
/// download a wordlist from a url
fn main_download_wordlist_from_url() -> Result<(), Box<dyn std::error::Error>> {
let srv = MockServer::start();
let (tmp_dir, _) = setup_tmp_directory(&["a".to_string()], "wordlist")?;
let mock1 = srv.mock(|when, then| {
when.method(GET).path("/derp");
then.status(200).body("stuff\nthings");
});
// serve endpoints stuff and things
let mock2 = srv.mock(|when, then| {
when.method(GET).path("/stuff");
then.status(200);
});
let mock3 = srv.mock(|when, then| {
when.method(GET).path("/things");
then.status(200);
});
Command::cargo_bin("feroxbuster")
.unwrap()
.current_dir(&tmp_dir)
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(srv.url("/derp"))
.assert()
.success()
.stderr(predicate::str::contains(srv.url("/derp")));
teardown_tmp_directory(tmp_dir);
assert_eq!(mock1.hits(), 1); // downloaded wordlist
assert_eq!(mock2.hits(), 1); // found stuff from wordlist
assert_eq!(mock3.hits(), 1); // found things from wordlist
Ok(())
}