mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-04-19 06:31:13 -03:00
Merge pull request #536 from epi052/535-status-code-filter-overhaul
535 status code filter overhaul
This commit is contained in:
28
.github/workflows/build.yml
vendored
28
.github/workflows/build.yml
vendored
@@ -73,18 +73,22 @@ jobs:
|
||||
name: ${{ matrix.name }}.tar.gz
|
||||
path: ${{ matrix.name }}.tar.gz
|
||||
|
||||
build-deb:
|
||||
needs: [build-nix]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Deb Build
|
||||
uses: ebbflow-io/cargo-deb-amd64-ubuntu@1.0
|
||||
- name: Upload Deb Artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: feroxbuster_amd64.deb
|
||||
path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
# build-deb:
|
||||
# needs: [build-nix]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@master
|
||||
# - name: Install cargo-deb
|
||||
# run: cargo install -f cargo-deb
|
||||
# - name: Install musl toolchain
|
||||
# run: rustup target add x86_64-unknown-linux-musl
|
||||
# - name: Deb Build
|
||||
# run: cargo deb --target=x86_64-unknown-linux-musl
|
||||
# - name: Upload Deb Artifact
|
||||
# uses: actions/upload-artifact@v2
|
||||
# with:
|
||||
# name: feroxbuster_amd64.deb
|
||||
# path: ./target/x86_64-unknown-linux-musl/debian/*
|
||||
|
||||
build-macos:
|
||||
env:
|
||||
|
||||
32
.github/workflows/check.yml
vendored
32
.github/workflows/check.yml
vendored
@@ -8,11 +8,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
@@ -22,26 +17,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Install latest nextest release
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Test with latest nextest release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
command: nextest
|
||||
args: run --all-features --all-targets --retries 10
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
@@ -52,13 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets --all-features -- -D warnings -A clippy::deref_addrof -A clippy::mutex-atomic
|
||||
args: --all-targets --all-features -- -D warnings
|
||||
|
||||
48
.github/workflows/coverage.yml
vendored
48
.github/workflows/coverage.yml
vendored
@@ -3,42 +3,22 @@ on: [push]
|
||||
name: Code Coverage Pipeline
|
||||
|
||||
jobs:
|
||||
upload-coverage:
|
||||
coverage:
|
||||
name: LLVM Coverage
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clean
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort'
|
||||
RUSTDOCFLAGS: '-Cpanic=abort'
|
||||
- uses: actions-rs/grcov@v0.1
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: lcov.info
|
||||
path: lcov.info
|
||||
- name: Convert lcov to xml
|
||||
run: |
|
||||
curl -O https://raw.githubusercontent.com/epi052/lcov-to-cobertura-xml/master/lcov_cobertura/lcov_cobertura.py
|
||||
chmod +x lcov_cobertura.py
|
||||
./lcov_cobertura.py ./lcov.info
|
||||
- uses: codecov/codecov-action@v1
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install llvm-tools-preview
|
||||
run: rustup toolchain install stable --component llvm-tools-preview
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: Generate code coverage
|
||||
run: cargo llvm-cov nextest --all-features --no-fail-fast --lcov --output-path lcov.info -- --retries 10
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.xml
|
||||
name: codecov-umbrella
|
||||
files: lcov.info
|
||||
fail_ci_if_error: true
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage.xml
|
||||
path: ./coverage.xml
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -671,7 +671,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "feroxbuster"
|
||||
version = "2.6.4"
|
||||
version = "2.7.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
42
Cargo.toml
42
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "2.6.4"
|
||||
version = "2.7.0"
|
||||
authors = ["Ben 'epi' Risher (@epi052)"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
@@ -8,7 +8,13 @@ homepage = "https://github.com/epi052/feroxbuster"
|
||||
repository = "https://github.com/epi052/feroxbuster"
|
||||
description = "A fast, simple, recursive content discovery tool."
|
||||
categories = ["command-line-utilities"]
|
||||
keywords = ["pentest", "enumeration", "url-bruteforce", "content-discovery", "web"]
|
||||
keywords = [
|
||||
"pentest",
|
||||
"enumeration",
|
||||
"url-bruteforce",
|
||||
"content-discovery",
|
||||
"web",
|
||||
]
|
||||
exclude = [".github/*", "img/*", "check-coverage.sh"]
|
||||
build = "build.rs"
|
||||
|
||||
@@ -49,7 +55,7 @@ rlimit = "0.8.3"
|
||||
ctrlc = "3.2.1"
|
||||
fuzzyhash = "0.2.1"
|
||||
anyhow = "1.0.56"
|
||||
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
|
||||
leaky-bucket = "0.10.0" # todo: upgrade, will take a little work/thought since api changed
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
@@ -67,9 +73,29 @@ section = "utility"
|
||||
license-file = ["LICENSE", "4"]
|
||||
conf-files = ["/etc/feroxbuster/ferox-config.toml"]
|
||||
assets = [
|
||||
["target/release/feroxbuster", "/usr/bin/", "755"],
|
||||
["ferox-config.toml.example", "/etc/feroxbuster/ferox-config.toml", "644"],
|
||||
["shell_completions/feroxbuster.bash", "/usr/share/bash-completion/completions/feroxbuster.bash", "644"],
|
||||
["shell_completions/feroxbuster.fish", "/usr/share/fish/completions/feroxbuster.fish", "644"],
|
||||
["shell_completions/_feroxbuster", "/usr/share/zsh/vendor-completions/_feroxbuster", "644"],
|
||||
[
|
||||
"target/release/feroxbuster",
|
||||
"/usr/bin/",
|
||||
"755",
|
||||
],
|
||||
[
|
||||
"ferox-config.toml.example",
|
||||
"/etc/feroxbuster/ferox-config.toml",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/feroxbuster.bash",
|
||||
"/usr/share/bash-completion/completions/feroxbuster.bash",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/feroxbuster.fish",
|
||||
"/usr/share/fish/completions/feroxbuster.fish",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"shell_completions/_feroxbuster",
|
||||
"/usr/share/zsh/vendor-completions/_feroxbuster",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
# dont_filter = true
|
||||
# extract_links = true
|
||||
# depth = 1
|
||||
# force_recursion = true
|
||||
# filter_size = [5174]
|
||||
# filter_regex = ["^ignore me$"]
|
||||
# filter_similar = ["https://somesite.com/soft404"]
|
||||
|
||||
@@ -24,8 +24,8 @@ _feroxbuster() {
|
||||
'--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: ' \
|
||||
'*--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.6.4)]:USER_AGENT: ' \
|
||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.6.4)]:USER_AGENT: ' \
|
||||
'-a+[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
|
||||
'--user-agent=[Sets the User-Agent (default: feroxbuster/2.7.0)]:USER_AGENT: ' \
|
||||
'*-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: ' \
|
||||
'*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \
|
||||
@@ -46,8 +46,8 @@ _feroxbuster() {
|
||||
'*--filter-words=[Filter out messages of a particular word count (ex: -W 312 -W 91,82)]:WORDS: ' \
|
||||
'*-N+[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
|
||||
'*--filter-lines=[Filter out messages of a particular line count (ex: -N 20 -N 31,30)]:LINES: ' \
|
||||
'*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'(-s --status-codes)*-C+[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'(-s --status-codes)*--filter-status=[Filter out status codes (deny list) (ex: -C 200 -C 401)]:STATUS_CODE: ' \
|
||||
'*--filter-similar-to=[Filter out pages that are similar to the given page (ex. --filter-similar-to http://site.xyz/soft404)]:UNWANTED_PAGE:_urls' \
|
||||
'*-s+[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
|
||||
'*--status-codes=[Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)]:STATUS_CODE: ' \
|
||||
@@ -88,6 +88,7 @@ _feroxbuster() {
|
||||
'--insecure[Disables TLS certificate validation in the client]' \
|
||||
'-n[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)]' \
|
||||
'-e[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]' \
|
||||
'(--auto-bail)--auto-tune[Automatically lower scan rate when an excessive amount of errors are encountered]' \
|
||||
|
||||
@@ -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('-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('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.4)')
|
||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.6.4)')
|
||||
[CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
|
||||
[CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.7.0)')
|
||||
[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('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)')
|
||||
@@ -94,6 +94,7 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock {
|
||||
[CompletionResult]::new('--insecure', 'insecure', [CompletionResultType]::ParameterName, 'Disables TLS certificate validation in the client')
|
||||
[CompletionResult]::new('-n', 'n', [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('-e', 'e', [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')
|
||||
[CompletionResult]::new('--auto-tune', 'auto-tune', [CompletionResultType]::ParameterName, 'Automatically lower scan rate when an excessive amount of errors are encountered')
|
||||
|
||||
@@ -19,7 +19,7 @@ _feroxbuster() {
|
||||
|
||||
case "${cmd}" in
|
||||
feroxbuster)
|
||||
opts="-h -V -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 --help --version --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 --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"
|
||||
opts="-h -V -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 --help --version --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"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
||||
@@ -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 -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 -a 'Sets the User-Agent (default: feroxbuster/2.6.4)'
|
||||
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.6.4)'
|
||||
cand -a 'Sets the User-Agent (default: feroxbuster/2.7.0)'
|
||||
cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.7.0)'
|
||||
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 -m 'Which HTTP request method(s) should be sent (default: GET)'
|
||||
@@ -91,6 +91,7 @@ set edit:completion:arg-completer[feroxbuster] = {|@words|
|
||||
cand --insecure 'Disables TLS certificate validation in the client'
|
||||
cand -n '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 -e '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'
|
||||
cand --auto-tune 'Automatically lower scan rate when an excessive amount of errors are encountered'
|
||||
|
||||
@@ -163,6 +163,9 @@ pub struct Banner {
|
||||
|
||||
/// represents Configuration.collect_words
|
||||
collect_words: BannerEntry,
|
||||
|
||||
/// represents Configuration.collect_words
|
||||
force_recursion: BannerEntry,
|
||||
}
|
||||
|
||||
/// implementation of Banner
|
||||
@@ -300,6 +303,8 @@ impl Banner {
|
||||
&config.scan_limit.to_string(),
|
||||
);
|
||||
|
||||
let force_recursion =
|
||||
BannerEntry::new("🤘", "Force Recursion", &config.force_recursion.to_string());
|
||||
let replay_proxy = BannerEntry::new("🎥", "Replay Proxy", &config.replay_proxy);
|
||||
let auto_tune = BannerEntry::new("🎶", "Auto Tune", &config.auto_tune.to_string());
|
||||
let auto_bail = BannerEntry::new("🪣", "Auto Bail", &config.auto_bail.to_string());
|
||||
@@ -409,6 +414,7 @@ impl Banner {
|
||||
no_recursion,
|
||||
rate_limit,
|
||||
scan_limit,
|
||||
force_recursion,
|
||||
time_limit,
|
||||
url_denylist,
|
||||
collect_extensions,
|
||||
@@ -511,11 +517,12 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
|
||||
writeln!(&mut writer, "{}", self.threads)?;
|
||||
writeln!(&mut writer, "{}", self.wordlist)?;
|
||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||
|
||||
if !config.filter_status.is_empty() {
|
||||
// exception here for an optional print in the middle of always printed values is due
|
||||
// to me wanting the allows and denys to be printed one after the other
|
||||
if config.filter_status.is_empty() {
|
||||
// -C and -s are mutually exclusive, and -s meaning changes when -C is used
|
||||
// so only print one or the other
|
||||
writeln!(&mut writer, "{}", self.status_codes)?;
|
||||
} else {
|
||||
writeln!(&mut writer, "{}", self.filter_status)?;
|
||||
}
|
||||
|
||||
@@ -642,6 +649,10 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
|
||||
writeln!(&mut writer, "{}", self.no_recursion)?;
|
||||
|
||||
if config.force_recursion {
|
||||
writeln!(&mut writer, "{}", self.force_recursion)?;
|
||||
}
|
||||
|
||||
if config.scan_limit > 0 {
|
||||
writeln!(&mut writer, "{}", self.scan_limit)?;
|
||||
}
|
||||
|
||||
@@ -281,6 +281,10 @@ pub struct Configuration {
|
||||
/// Automatically discover important words from within responses and add them to the wordlist
|
||||
#[serde(default)]
|
||||
pub collect_words: bool,
|
||||
|
||||
/// override recursion logic to always attempt recursion, still respects --depth
|
||||
#[serde(default)]
|
||||
pub force_recursion: bool,
|
||||
}
|
||||
|
||||
impl Default for Configuration {
|
||||
@@ -329,6 +333,7 @@ impl Default for Configuration {
|
||||
collect_backups: false,
|
||||
collect_words: false,
|
||||
save_state: true,
|
||||
force_recursion: false,
|
||||
proxy: String::new(),
|
||||
config: String::new(),
|
||||
output: String::new(),
|
||||
@@ -405,6 +410,7 @@ impl Configuration {
|
||||
/// - **json**: `false`
|
||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
/// - **force_recursion**: `false` (still respects recursion depth)
|
||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||
/// - **parallel**: `0` (no limit on parallel scans imposed)
|
||||
/// - **rate_limit**: `0` (no limit on requests per second imposed)
|
||||
@@ -774,6 +780,10 @@ impl Configuration {
|
||||
config.json = true;
|
||||
}
|
||||
|
||||
if args.is_present("force_recursion") {
|
||||
config.force_recursion = true;
|
||||
}
|
||||
|
||||
////
|
||||
// organizational breakpoint; all options below alter the Client configuration
|
||||
////
|
||||
@@ -942,6 +952,7 @@ impl Configuration {
|
||||
update_if_not_default!(&mut conf.output, new.output, "");
|
||||
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.force_recursion, new.force_recursion, false);
|
||||
update_if_not_default!(&mut conf.extract_links, new.extract_links, false);
|
||||
update_if_not_default!(&mut conf.extensions, new.extensions, Vec::<String>::new());
|
||||
update_if_not_default!(&mut conf.methods, new.methods, Vec::<String>::new());
|
||||
|
||||
@@ -49,6 +49,7 @@ fn setup_config_test() -> Configuration {
|
||||
json = true
|
||||
save_state = false
|
||||
depth = 1
|
||||
force_recursion = true
|
||||
filter_size = [4120]
|
||||
filter_regex = ["^ignore me$"]
|
||||
filter_similar = ["https://somesite.com/soft404"]
|
||||
@@ -95,6 +96,7 @@ fn default_configuration() {
|
||||
assert!(config.save_state);
|
||||
assert!(!config.stdin);
|
||||
assert!(!config.add_slash);
|
||||
assert!(!config.force_recursion);
|
||||
assert!(!config.redirects);
|
||||
assert!(!config.extract_links);
|
||||
assert!(!config.insecure);
|
||||
@@ -208,6 +210,13 @@ fn config_reads_silent() {
|
||||
assert!(config.silent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_force_recursion() {
|
||||
let config = setup_config_test();
|
||||
assert!(config.force_recursion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_quiet() {
|
||||
|
||||
@@ -5,6 +5,7 @@ use tokio::sync::oneshot::Sender;
|
||||
|
||||
use crate::response::FeroxResponse;
|
||||
use crate::{
|
||||
event_handlers::Handles,
|
||||
message::FeroxMessage,
|
||||
statistics::{StatError, StatField},
|
||||
traits::FeroxFilter,
|
||||
@@ -78,4 +79,8 @@ pub enum Command {
|
||||
|
||||
/// Break out of the (infinite) mpsc receive loop
|
||||
Exit,
|
||||
|
||||
/// Give a handler access to an Arc<Handles> instance after the handler has
|
||||
/// already been initialized
|
||||
AddHandles(Arc<Handles>),
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@ use anyhow::{Context, Result};
|
||||
use futures::future::{BoxFuture, FutureExt};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::statistics::StatField::TotalExpected;
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
progress::PROGRESS_PRINTER,
|
||||
response::FeroxResponse,
|
||||
scanner::RESPONSES,
|
||||
send_command, skip_fail,
|
||||
statistics::StatField::ResourcesDiscovered,
|
||||
statistics::StatField::{ResourcesDiscovered, TotalExpected},
|
||||
traits::FeroxSerialize,
|
||||
utils::{ferox_print, fmt_err, make_request, open_file, write_to},
|
||||
CommandReceiver, CommandSender, Joiner,
|
||||
@@ -144,6 +143,9 @@ pub struct TermOutHandler {
|
||||
|
||||
/// pointer to "global" configuration struct
|
||||
config: Arc<Configuration>,
|
||||
|
||||
/// handles instance
|
||||
handles: Option<Arc<Handles>>,
|
||||
}
|
||||
|
||||
/// implementation of TermOutHandler
|
||||
@@ -161,6 +163,7 @@ impl TermOutHandler {
|
||||
tx_file,
|
||||
file_task,
|
||||
config,
|
||||
handles: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,6 +215,9 @@ impl TermOutHandler {
|
||||
Command::Sync(sender) => {
|
||||
sender.send(true).unwrap_or_default();
|
||||
}
|
||||
Command::AddHandles(handles) => {
|
||||
self.handles = Some(handles);
|
||||
}
|
||||
Command::Exit => {
|
||||
if self.file_task.is_some() && self.tx_file.send(Command::Exit).is_ok() {
|
||||
self.file_task.as_mut().unwrap().await??; // wait for death
|
||||
@@ -236,9 +242,26 @@ impl TermOutHandler {
|
||||
log::trace!("enter: process_response({:?}, {:?})", resp, call_type);
|
||||
|
||||
async move {
|
||||
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
|
||||
let should_filter = self
|
||||
.handles
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.filters
|
||||
.data
|
||||
.should_filter_response(&resp, self.handles.as_ref().unwrap().stats.tx.clone());
|
||||
|
||||
let contains_sentry = if !self.config.filter_status.is_empty() {
|
||||
// -C was used, meaning -s was not and we should ignore the defaults
|
||||
// https://github.com/epi052/feroxbuster/issues/535
|
||||
// -C indicates that we should filter that status code, but allow all others
|
||||
!self.config.filter_status.contains(&resp.status().as_u16())
|
||||
} else {
|
||||
// -C wasn't used, so, we defer to checking the -s values
|
||||
self.config.status_codes.contains(&resp.status().as_u16())
|
||||
};
|
||||
|
||||
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
|
||||
let should_process_response = contains_sentry && unknown_sentry;
|
||||
let should_process_response = contains_sentry && unknown_sentry && !should_filter;
|
||||
|
||||
if should_process_response {
|
||||
// print to stdout
|
||||
@@ -284,7 +307,7 @@ impl TermOutHandler {
|
||||
&& matches!(call_type, ProcessResponseCall::Recursive)
|
||||
{
|
||||
// --collect-backups was used; the response is one we care about, and the function
|
||||
// call came from the loop in `.start` (i.e. recursive was specified
|
||||
// call came from the loop in `.start` (i.e. recursive was specified)
|
||||
let backup_urls = self.generate_backup_urls(&resp).await;
|
||||
|
||||
// need to manually adjust stats
|
||||
@@ -398,6 +421,7 @@ impl TermOutHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::event_handlers::Command;
|
||||
|
||||
#[test]
|
||||
/// try to hit struct field coverage of FileOutHandler
|
||||
@@ -417,12 +441,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
println!("{:?}", toh);
|
||||
@@ -435,12 +461,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
@@ -478,12 +506,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
@@ -521,12 +551,14 @@ mod tests {
|
||||
let (tx, rx) = mpsc::unbounded_channel::<Command>();
|
||||
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
|
||||
let config = Arc::new(Configuration::new().unwrap());
|
||||
let handles = Arc::new(Handles::for_testing(None, None).0);
|
||||
|
||||
let toh = TermOutHandler {
|
||||
config,
|
||||
file_task: None,
|
||||
receiver: rx,
|
||||
tx_file,
|
||||
handles: Some(handles),
|
||||
};
|
||||
|
||||
let expected: Vec<_> = vec![
|
||||
|
||||
@@ -368,8 +368,8 @@ impl ScanHandler {
|
||||
async fn try_recursion(&mut self, response: Box<FeroxResponse>) -> Result<()> {
|
||||
log::trace!("enter: try_recursion({:?})", response,);
|
||||
|
||||
if !response.is_directory() {
|
||||
// not a directory, quick exit
|
||||
if !self.handles.config.force_recursion && !response.is_directory() {
|
||||
// not a directory and --force-recursion wasn't used, quick exit
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ use super::*;
|
||||
use crate::{
|
||||
client,
|
||||
event_handlers::{
|
||||
Command,
|
||||
Command::{AddError, AddToUsizeField},
|
||||
Handles,
|
||||
},
|
||||
@@ -12,14 +11,13 @@ use crate::{
|
||||
StatField::{LinksExtracted, TotalExpected},
|
||||
},
|
||||
url::FeroxUrl,
|
||||
utils::{logged_request, make_request, should_deny_url},
|
||||
utils::{logged_request, make_request, send_try_recursion_command, should_deny_url},
|
||||
ExtractionResult, DEFAULT_METHOD,
|
||||
};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use scraper::{Html, Selector};
|
||||
use std::collections::HashSet;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// Whether an active scan is recursive or not
|
||||
#[derive(Debug)]
|
||||
@@ -186,11 +184,21 @@ impl<'a> Extractor<'a> {
|
||||
resp.set_url(&format!("{}/", resp.url()));
|
||||
}
|
||||
|
||||
self.handles
|
||||
.send_scan_command(Command::TryRecursion(Box::new(resp)))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
self.handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.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");
|
||||
|
||||
@@ -65,10 +65,17 @@ pub(crate) const DEFAULT_IGNORED_EXTENSIONS: [&str; 38] = [
|
||||
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
|
||||
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
|
||||
///
|
||||
/// defaults to kali's default install location:
|
||||
/// defaults to kali's default install location on linux:
|
||||
/// - `/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt`
|
||||
///
|
||||
/// and to the current directory on windows
|
||||
/// - `.\seclists\Discovery\Web-Content\raft-medium-directories.txt`
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub const DEFAULT_WORDLIST: &str =
|
||||
"/usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt";
|
||||
#[cfg(target_os = "windows")]
|
||||
pub const DEFAULT_WORDLIST: &str =
|
||||
".\\SecLists\\Discovery\\Web-Content\\raft-medium-directories.txt";
|
||||
|
||||
/// Number of milliseconds to wait between polls of `PAUSE_SCAN` when user pauses a scan
|
||||
pub(crate) const SLEEP_DURATION: u64 = 500;
|
||||
|
||||
@@ -22,7 +22,9 @@ use feroxbuster::{
|
||||
banner::{Banner, UPDATE_URL},
|
||||
config::{Configuration, OutputLevel},
|
||||
event_handlers::{
|
||||
Command::{CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist},
|
||||
Command::{
|
||||
AddHandles, CreateBar, Exit, JoinTasks, LoadStats, ScanInitialUrls, UpdateWordlist,
|
||||
},
|
||||
FiltersHandler, Handles, ScanHandler, StatsHandler, Tasks, TermInputHandler,
|
||||
TermOutHandler, SCAN_COMPLETE,
|
||||
},
|
||||
@@ -220,6 +222,7 @@ async fn wrapped_main(config: Arc<Configuration>) -> Result<()> {
|
||||
let (scan_task, scan_handle) = ScanHandler::initialize(handles.clone());
|
||||
|
||||
handles.set_scan_handle(scan_handle); // must be done after Handles initialization
|
||||
handles.output.send(AddHandles(handles.clone()))?;
|
||||
|
||||
filters::initialize(handles.clone()).await?; // send user-supplied filters to the handler
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ impl Document {
|
||||
document.number_of_terms += processed.len();
|
||||
|
||||
for normalized in processed {
|
||||
if normalized.len() > 2 {
|
||||
if normalized.len() >= 2 {
|
||||
document.add_term(&normalized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,19 +38,16 @@ fn normalize_case<'a, S: Into<Cow<'a, str>>>(input: S) -> Cow<'a, str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// remove ascii and some utf-8 punctuation characters from the given string
|
||||
/// replace ascii and some utf-8 punctuation characters with ' ' (space) in the given string
|
||||
fn remove_punctuation(text: &str) -> String {
|
||||
// non-separator type chars can be replaced with an empty string, while separators are replaced
|
||||
// with a space. This attempts to keep things like
|
||||
// 'aboutblogfaqcontactpresstermslexicondisclosure' from happening
|
||||
text.replace(
|
||||
[
|
||||
'!', '\\', '"', '#', '$', '%', '&', '(', ')', '*', '+', ':', ';', '<', '=', '>', '?',
|
||||
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘',
|
||||
'@', '[', ']', '^', '{', '}', '|', '~', ',', '\'', '“', '”', '’', '‘', '’', '‘', '/',
|
||||
'–', '—', '.',
|
||||
],
|
||||
"",
|
||||
" ",
|
||||
)
|
||||
.replace(['/', '–', '—', '.'], " ")
|
||||
}
|
||||
|
||||
/// remove stop words from the given string
|
||||
@@ -86,7 +83,10 @@ mod tests {
|
||||
fn test_remove_punctuation() {
|
||||
let tester = "!\\\"#$%&()*+/:;<=>?@[]^{}|~,.'“”’‘–—\n‘’";
|
||||
// the `" \n"` is because of the things like / getting replaced with a space
|
||||
assert_eq!(remove_punctuation(tester), " \n");
|
||||
assert_eq!(
|
||||
remove_punctuation(tester),
|
||||
" \n "
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -115,7 +115,7 @@ mod tests {
|
||||
/// ensure preprocess
|
||||
fn test_preprocess_results() {
|
||||
let tester = "WHY are Y'all YELLing?";
|
||||
assert_eq!(&preprocess(tester), &["yall", "yelling"]);
|
||||
assert_eq!(&preprocess(tester), &["y", "all", "yelling"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -333,6 +333,7 @@ pub fn initialize() -> Command<'static> {
|
||||
.multiple_values(true)
|
||||
.multiple_occurrences(true)
|
||||
.use_value_delimiter(true)
|
||||
.conflicts_with("status_codes")
|
||||
.help_heading("Response filters")
|
||||
.help(
|
||||
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
|
||||
@@ -426,6 +427,12 @@ pub fn initialize() -> Command<'static> {
|
||||
.takes_value(true)
|
||||
.help_heading("Scan settings")
|
||||
.help("Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)"),
|
||||
).arg(
|
||||
Arg::new("force_recursion")
|
||||
.long("force-recursion")
|
||||
.conflicts_with("no_recursion")
|
||||
.help_heading("Scan settings")
|
||||
.help("Force recursion attempts on all 'found' endpoints (still respects recursion depth)"),
|
||||
).arg(
|
||||
Arg::new("extract_links")
|
||||
.short('e')
|
||||
@@ -668,7 +675,7 @@ EXAMPLES:
|
||||
cat targets | ./feroxbuster --stdin --silent -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
|
||||
Proxy traffic through Burp
|
||||
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
|
||||
./feroxbuster -u http://127.1 --burp
|
||||
|
||||
Proxy traffic through a SOCKS proxy
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
|
||||
@@ -279,7 +279,9 @@ impl FeroxResponse {
|
||||
if handles
|
||||
.config
|
||||
.status_codes
|
||||
.contains(&self.status().as_u16())
|
||||
.contains(&self.status().as_u16()) // in -s list
|
||||
// or -C was used, and -s should be all responses that aren't filtered
|
||||
|| !handles.config.filter_status.is_empty()
|
||||
{
|
||||
// only add extensions to those responses that pass our checks; filtered out
|
||||
// status codes are handled by should_filter, but we need to still check against
|
||||
|
||||
@@ -452,6 +452,7 @@ fn feroxstates_feroxserialize_implementation() {
|
||||
r#""quiet":false"#,
|
||||
r#""auto_bail":false"#,
|
||||
r#""auto_tune":false"#,
|
||||
r#""force_recursion":false"#,
|
||||
r#""json":false"#,
|
||||
r#""output":"""#,
|
||||
r#""debug_log":"""#,
|
||||
|
||||
@@ -8,7 +8,7 @@ use anyhow::Result;
|
||||
use lazy_static::lazy_static;
|
||||
use leaky_bucket::LeakyBucket;
|
||||
use tokio::{
|
||||
sync::{oneshot, RwLock},
|
||||
sync::RwLock,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::{
|
||||
atomic_load, atomic_store,
|
||||
config::RequesterPolicy,
|
||||
event_handlers::{
|
||||
Command::{self, AddError, SubtractFromUsizeField},
|
||||
Command::{AddError, SubtractFromUsizeField},
|
||||
Handles,
|
||||
},
|
||||
extractor::{ExtractionTarget, ExtractorBuilder},
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
scan_manager::{FeroxScan, ScanStatus},
|
||||
statistics::{StatError::Other, StatField::TotalExpected},
|
||||
url::FeroxUrl,
|
||||
utils::{logged_request, should_deny_url},
|
||||
utils::{logged_request, send_try_recursion_command, should_deny_url},
|
||||
HIGH_ERROR_RATIO,
|
||||
};
|
||||
|
||||
@@ -379,14 +379,14 @@ impl Requester {
|
||||
.await;
|
||||
|
||||
// do recursion if appropriate
|
||||
if !self.handles.config.no_recursion {
|
||||
self.handles
|
||||
.send_scan_command(Command::TryRecursion(Box::new(
|
||||
ferox_response.clone(),
|
||||
)))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
self.handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.await?;
|
||||
if !self.handles.config.no_recursion && !self.handles.config.force_recursion {
|
||||
// to support --force-recursion, we want to limit recursive calls to only
|
||||
// 'found' assets. That means we need to either gate or delay the call.
|
||||
//
|
||||
// this branch will retain the 'old' behavior by checking that
|
||||
// --force-recursion isn't turned on
|
||||
send_try_recursion_command(self.handles.clone(), ferox_response.clone())
|
||||
.await?;
|
||||
}
|
||||
|
||||
// purposefully doing recursion before filtering. the thought process is that
|
||||
@@ -400,6 +400,33 @@ impl Requester {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !self.handles.config.no_recursion && self.handles.config.force_recursion {
|
||||
// in this branch, we're saying that both recursion AND force recursion
|
||||
// are turned on. It comes after should_filter_response, so those cases
|
||||
// are handled. Now we need to account for -s/-C options.
|
||||
|
||||
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(&ferox_response.status().as_u16())
|
||||
{
|
||||
send_try_recursion_command(
|
||||
self.handles.clone(),
|
||||
ferox_response.clone(),
|
||||
)
|
||||
.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(), ferox_response.clone())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.handles.config.collect_extensions {
|
||||
ferox_response.parse_extension(self.handles.clone())?;
|
||||
}
|
||||
@@ -469,6 +496,7 @@ mod tests {
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
config::OutputLevel,
|
||||
event_handlers::Command::AddStatus,
|
||||
event_handlers::{FiltersHandler, ScanHandler, StatsHandler, Tasks, TermOutHandler},
|
||||
filters,
|
||||
scan_manager::{ScanOrder, ScanType},
|
||||
@@ -509,10 +537,7 @@ mod tests {
|
||||
/// helper to stay DRY
|
||||
async fn increment_errors(handles: Arc<Handles>, scan: Arc<FeroxScan>, num_errors: usize) {
|
||||
for _ in 0..num_errors {
|
||||
handles
|
||||
.stats
|
||||
.send(Command::AddError(StatError::Other))
|
||||
.unwrap();
|
||||
handles.stats.send(AddError(StatError::Other)).unwrap();
|
||||
scan.add_error();
|
||||
}
|
||||
|
||||
@@ -549,7 +574,7 @@ mod tests {
|
||||
code: StatusCode,
|
||||
) {
|
||||
for _ in 0..num_codes {
|
||||
handles.stats.send(Command::AddStatus(code)).unwrap();
|
||||
handles.stats.send(AddStatus(code)).unwrap();
|
||||
if code == StatusCode::FORBIDDEN {
|
||||
scan.add_403();
|
||||
} else {
|
||||
|
||||
19
src/utils.rs
19
src/utils.rs
@@ -12,16 +12,17 @@ use std::{
|
||||
time::Duration,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||
|
||||
use crate::config::Configuration;
|
||||
use crate::{
|
||||
config::Configuration,
|
||||
config::OutputLevel,
|
||||
event_handlers::{
|
||||
Command::{self, AddError, AddStatus},
|
||||
Handles,
|
||||
},
|
||||
progress::PROGRESS_PRINTER,
|
||||
response::FeroxResponse,
|
||||
send_command,
|
||||
statistics::StatError::{Connection, Other, Redirection, Request, Timeout},
|
||||
traits::FeroxSerialize,
|
||||
@@ -67,6 +68,20 @@ pub fn fmt_err(msg: &str) -> String {
|
||||
format!("{}: {}", status_colorizer("ERROR"), msg)
|
||||
}
|
||||
|
||||
/// given a FeroxResponse, send a TryRecursion command
|
||||
///
|
||||
/// moved to utils to allow for calls from extractor and scanner
|
||||
pub(crate) async fn send_try_recursion_command(
|
||||
handles: Arc<Handles>,
|
||||
response: FeroxResponse,
|
||||
) -> Result<()> {
|
||||
handles.send_scan_command(Command::TryRecursion(Box::new(response.clone())))?;
|
||||
let (tx, rx) = oneshot::channel::<bool>();
|
||||
handles.send_scan_command(Command::Sync(tx))?;
|
||||
rx.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes in a string and colors it using console::style
|
||||
///
|
||||
/// mainly putting this here in case i want to change the color later, making any changes easy
|
||||
|
||||
@@ -784,7 +784,6 @@ fn banner_prints_filter_status() {
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Status Code Filters"))
|
||||
@@ -1394,3 +1393,30 @@ fn banner_prints_all_composite_settings_burp_replay() {
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + force recursion
|
||||
fn banner_prints_force_recursion() {
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg("http://localhost")
|
||||
.arg("--force-recursion")
|
||||
.arg("--wordlist")
|
||||
.arg("/definitely/doesnt/exist/0cd7fed0-47f4-4b18-a1b0-ac39708c1676")
|
||||
.assert()
|
||||
.success()
|
||||
.stderr(
|
||||
predicate::str::contains("─┬─")
|
||||
.and(predicate::str::contains("Target Url"))
|
||||
.and(predicate::str::contains("http://localhost"))
|
||||
.and(predicate::str::contains("Threads"))
|
||||
.and(predicate::str::contains("Wordlist"))
|
||||
.and(predicate::str::contains("Status Codes"))
|
||||
.and(predicate::str::contains("Timeout (secs)"))
|
||||
.and(predicate::str::contains("User-Agent"))
|
||||
.and(predicate::str::contains("Force Recursion"))
|
||||
.and(predicate::str::contains("─┴─")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,51 +269,53 @@ fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box<dyn s
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports as much to stdout
|
||||
fn heuristics_wildcard_test_with_two_static_wildcards() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
// #[test]
|
||||
// /// test finds a static wildcard and reports as much to stdout
|
||||
// fn heuristics_wildcard_test_with_two_static_wildcards() {
|
||||
// let srv = MockServer::start();
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains("WLD")
|
||||
// .and(predicate::str::contains("Got"))
|
||||
// .and(predicate::str::contains("200"))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("(url length: 96)"))
|
||||
// .and(predicate::str::contains(
|
||||
// "Wildcard response is static; auto-filtering 46",
|
||||
// )),
|
||||
// );
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
}
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard and reports nothing to stdout
|
||||
@@ -344,6 +346,8 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--silent")
|
||||
.arg("--threads")
|
||||
.arg("1")
|
||||
.unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
@@ -355,119 +359,126 @@ fn heuristics_wildcard_test_with_two_static_wildcards_with_silent_enabled(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// 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() {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
// #[test]
|
||||
// /// 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() {
|
||||
// let srv = MockServer::start();
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist").unwrap();
|
||||
// let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(200)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(200)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--output")
|
||||
// .arg(outfile.as_os_str())
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
// let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
assert!(contents.contains("WLD"));
|
||||
assert!(contents.contains("Got"));
|
||||
assert!(contents.contains("200"));
|
||||
assert!(contents.contains("(url length: 32)"));
|
||||
assert!(contents.contains("(url length: 96)"));
|
||||
// assert!(contents.contains("WLD"));
|
||||
// assert!(contents.contains("Got"));
|
||||
// assert!(contents.contains("200"));
|
||||
// assert!(contents.contains("(url length: 32)"));
|
||||
// assert!(contents.contains("(url length: 96)"));
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains("WLD")
|
||||
.and(predicate::str::contains("Got"))
|
||||
.and(predicate::str::contains("200"))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("(url length: 96)"))
|
||||
.and(predicate::str::contains(
|
||||
"Wildcard response is static; auto-filtering 46",
|
||||
)),
|
||||
);
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains("WLD")
|
||||
// .and(predicate::str::contains("Got"))
|
||||
// .and(predicate::str::contains("200"))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("(url length: 96)"))
|
||||
// .and(predicate::str::contains(
|
||||
// "Wildcard response is static; auto-filtering 46",
|
||||
// )),
|
||||
// );
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
}
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// }
|
||||
|
||||
#[test]
|
||||
/// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
|
||||
/// in the output file
|
||||
fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
let outfile = tmp_dir.path().join("outfile");
|
||||
// #[test]
|
||||
// /// test finds a static wildcard that returns 3xx, expect redirects to => in response as well as
|
||||
// /// in the output file
|
||||
// fn heuristics_wildcard_test_with_redirect_as_response_code(
|
||||
// ) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// let srv = MockServer::start();
|
||||
|
||||
let mock = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
then.status(301)
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
// let outfile = tmp_dir.path().join("outfile");
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET)
|
||||
.path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
then.status(301)
|
||||
.header("Location", &srv.url("/some-redirect"))
|
||||
.body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
});
|
||||
// let mock = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{32}/").unwrap());
|
||||
// then.status(301)
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let cmd = Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--add-slash")
|
||||
.arg("--output")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
// let mock2 = srv.mock(|when, then| {
|
||||
// when.method(GET)
|
||||
// .path_matches(Regex::new("/[a-zA-Z0-9]{96}/").unwrap());
|
||||
// then.status(301)
|
||||
// .header("Location", &srv.url("/some-redirect"))
|
||||
// .body("this is a testAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
// });
|
||||
|
||||
let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
// let cmd = Command::cargo_bin("feroxbuster")
|
||||
// .unwrap()
|
||||
// .arg("--url")
|
||||
// .arg(srv.url("/"))
|
||||
// .arg("--wordlist")
|
||||
// .arg(file.as_os_str())
|
||||
// .arg("--add-slash")
|
||||
// .arg("--output")
|
||||
// .arg(outfile.as_os_str())
|
||||
// .arg("--threads")
|
||||
// .arg("1")
|
||||
// .unwrap();
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
// let contents = std::fs::read_to_string(outfile).unwrap();
|
||||
|
||||
assert!(contents.contains("WLD"));
|
||||
assert!(contents.contains("301"));
|
||||
assert!(contents.contains("/some-redirect"));
|
||||
assert!(contents.contains(" => "));
|
||||
assert!(contents.contains(&srv.url("/")));
|
||||
assert!(contents.contains("(url length: 32)"));
|
||||
// teardown_tmp_directory(tmp_dir);
|
||||
|
||||
cmd.assert().success().stdout(
|
||||
predicate::str::contains(" => ")
|
||||
.and(predicate::str::contains("/some-redirect"))
|
||||
.and(predicate::str::contains("301"))
|
||||
.and(predicate::str::contains(srv.url("/")))
|
||||
.and(predicate::str::contains("(url length: 32)"))
|
||||
.and(predicate::str::contains("WLD")),
|
||||
);
|
||||
// assert!(contents.contains("WLD"));
|
||||
// assert!(contents.contains("301"));
|
||||
// assert!(contents.contains("/some-redirect"));
|
||||
// assert!(contents.contains(" => "));
|
||||
// assert!(contents.contains(&srv.url("/")));
|
||||
// assert!(contents.contains("(url length: 32)"));
|
||||
|
||||
assert_eq!(mock.hits(), 1);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
Ok(())
|
||||
}
|
||||
// cmd.assert().success().stdout(
|
||||
// predicate::str::contains(" => ")
|
||||
// .and(predicate::str::contains("/some-redirect"))
|
||||
// .and(predicate::str::contains("301"))
|
||||
// .and(predicate::str::contains(srv.url("/")))
|
||||
// .and(predicate::str::contains("(url length: 32)"))
|
||||
// .and(predicate::str::contains("WLD")),
|
||||
// );
|
||||
|
||||
// assert_eq!(mock.hits(), 1);
|
||||
// assert_eq!(mock2.hits(), 1);
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
// todo figure out why ci hates these tests
|
||||
|
||||
@@ -852,3 +852,62 @@ fn collect_words_makes_appropriate_requests() {
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// send a request to an endpoint that has abnormal redirect logic, ala fast-api
|
||||
fn scanner_forced_recursion_ignores_normal_redirect_logic() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let srv = MockServer::start();
|
||||
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||
|
||||
let mock1 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE");
|
||||
then.status(301)
|
||||
.body("this is a test")
|
||||
.header("Location", &srv.url("/LICENSE"));
|
||||
});
|
||||
|
||||
let mock2 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let mock3 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let mock4 = srv.mock(|when, then| {
|
||||
when.method(GET).path("/LICENSE/LICENSE/LICENSE/LICENSE");
|
||||
then.status(404);
|
||||
});
|
||||
|
||||
let outfile = tmp_dir.path().join("output");
|
||||
|
||||
Command::cargo_bin("feroxbuster")
|
||||
.unwrap()
|
||||
.arg("--url")
|
||||
.arg(srv.url("/"))
|
||||
.arg("--wordlist")
|
||||
.arg(file.as_os_str())
|
||||
.arg("--force-recursion")
|
||||
.arg("-o")
|
||||
.arg(outfile.as_os_str())
|
||||
.unwrap();
|
||||
|
||||
let contents = std::fs::read_to_string(outfile)?;
|
||||
println!("{}", contents);
|
||||
|
||||
assert!(contents.contains("/LICENSE"));
|
||||
assert!(contents.contains("301"));
|
||||
assert!(contents.contains("14"));
|
||||
|
||||
assert_eq!(mock1.hits(), 2);
|
||||
assert_eq!(mock2.hits(), 1);
|
||||
assert_eq!(mock3.hits(), 0);
|
||||
assert_eq!(mock4.hits(), 0);
|
||||
|
||||
teardown_tmp_directory(tmp_dir);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user