mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-06-07 01:51:12 -03:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20938dd544 | ||
|
|
d63d7dc078 | ||
|
|
5e7be449d0 | ||
|
|
c8775e3c8c | ||
|
|
427efdef3b | ||
|
|
45815ff796 | ||
|
|
0dbc3bee23 | ||
|
|
9e143d9f19 | ||
|
|
bd2bd2035c | ||
|
|
6e71f4e039 | ||
|
|
f5229a1ddd | ||
|
|
d4eae2af8b | ||
|
|
ae3b837e81 | ||
|
|
20fbb2f68d | ||
|
|
2ddcf4249f | ||
|
|
c975a7b82f | ||
|
|
43c1eb58ad | ||
|
|
2b94205f2a | ||
|
|
15942e7a06 | ||
|
|
39f82816d8 | ||
|
|
d39a2ab0f7 | ||
|
|
095edc0804 | ||
|
|
7d70126eea | ||
|
|
b09e8d078a | ||
|
|
47d4221ada | ||
|
|
4578630b13 | ||
|
|
c4f018a757 | ||
|
|
49462df2fa |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "feroxbuster"
|
name = "feroxbuster"
|
||||||
version = "1.4.1"
|
version = "1.5.3"
|
||||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -33,6 +33,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
|||||||
dirs = "3.0"
|
dirs = "3.0"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
crossterm = "0.18"
|
crossterm = "0.18"
|
||||||
|
rlimit = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.1"
|
tempfile = "3.1"
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -82,6 +82,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di
|
|||||||
- [Pass auth token via query parameter](#pass-auth-token-via-query-parameter)
|
- [Pass auth token via query parameter](#pass-auth-token-via-query-parameter)
|
||||||
- [Limit Total Number of Concurrent Scans (new in `v1.2.0`)](#limit-total-number-of-concurrent-scans-new-in-v120)
|
- [Limit Total Number of Concurrent Scans (new in `v1.2.0`)](#limit-total-number-of-concurrent-scans-new-in-v120)
|
||||||
- [Filter Response by Status Code (new in `v1.3.0`)](#filter-response-by-status-code--new-in-v130)
|
- [Filter Response by Status Code (new in `v1.3.0`)](#filter-response-by-status-code--new-in-v130)
|
||||||
|
- [Replay Responses to a Proxy based on Status Code (new in `v1.5.0`)](#replay-responses-to-a-proxy-based-on-status-code-new-in-v150)
|
||||||
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
- [Comparison w/ Similar Tools](#-comparison-w-similar-tools)
|
||||||
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
||||||
- [No file descriptors available](#no-file-descriptors-available)
|
- [No file descriptors available](#no-file-descriptors-available)
|
||||||
@@ -276,9 +277,11 @@ A pre-made configuration file with examples of all available settings can be fou
|
|||||||
# wordlist = "/wordlists/jhaddix/all.txt"
|
# wordlist = "/wordlists/jhaddix/all.txt"
|
||||||
# status_codes = [200, 500]
|
# status_codes = [200, 500]
|
||||||
# filter_status = [301]
|
# filter_status = [301]
|
||||||
|
# replay_codes = [301]
|
||||||
# threads = 1
|
# threads = 1
|
||||||
# timeout = 5
|
# timeout = 5
|
||||||
# proxy = "http://127.0.0.1:8080"
|
# proxy = "http://127.0.0.1:8080"
|
||||||
|
# replay_proxy = "http://127.0.0.1:8081"
|
||||||
# verbosity = 1
|
# verbosity = 1
|
||||||
# scan_limit = 6
|
# scan_limit = 6
|
||||||
# quiet = true
|
# quiet = true
|
||||||
@@ -335,11 +338,15 @@ OPTIONS:
|
|||||||
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
||||||
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
||||||
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||||
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -S 401)
|
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -C 401)
|
||||||
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
-H, --headers <HEADER>... Specify HTTP headers (ex: -H Header:val 'stuff: things')
|
||||||
-o, --output <FILE> Output file to write results to (default: stdout)
|
-o, --output <FILE> Output file to write results to (default: stdout)
|
||||||
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
|
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
|
||||||
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
|
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
|
||||||
|
-R, --replay-codes <REPLAY_CODE>... Status Codes to send through a Replay Proxy when found (default: --status
|
||||||
|
-codes value)
|
||||||
|
-P, --replay-proxy <REPLAY_PROXY> Send only unfiltered requests through a Replay Proxy, instead of all
|
||||||
|
requests
|
||||||
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
|
-L, --scan-limit <SCAN_LIMIT> Limit total number of concurrent scans (default: 0, i.e. no limit)
|
||||||
-s, --status-codes <STATUS_CODE>... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401
|
-s, --status-codes <STATUS_CODE>... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401
|
||||||
403 405)
|
403 405)
|
||||||
@@ -459,6 +466,20 @@ each one is checked against a list of known filters and either displayed or not
|
|||||||
./feroxbuster -u http://127.1 --filter-status 301
|
./feroxbuster -u http://127.1 --filter-status 301
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Replay Responses to a Proxy based on Status Code (new in `v1.5.0`)
|
||||||
|
|
||||||
|
The `--replay-proxy` and `--replay-codes` options were added as a way to only send a select few responses to a proxy. This is in stark contrast to `--proxy` which proxies EVERY request.
|
||||||
|
|
||||||
|
Imagine you only care about proxying responses that have either the status code `200` or `302` (or you just don't want to clutter up your Burp history). These two options will allow you to fine-tune what gets proxied and what doesn't.
|
||||||
|
|
||||||
|
```
|
||||||
|
./feroxbuster -u http://127.1 --replay-proxy http://localhost:8080 --replay-codes 200 302 --insecure
|
||||||
|
```
|
||||||
|
|
||||||
|
Of note: this means that for every response that matches your replay criteria, you'll end up sending the request that generated that response a second time. Depending on the target and your engagement terms (if any), it may not make sense from a traffic generated perspective.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🧐 Comparison w/ Similar Tools
|
## 🧐 Comparison w/ Similar Tools
|
||||||
|
|
||||||
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
# threads = 1
|
# threads = 1
|
||||||
# timeout = 5
|
# timeout = 5
|
||||||
# proxy = "http://127.0.0.1:8080"
|
# proxy = "http://127.0.0.1:8080"
|
||||||
|
# replay_proxy = "http://127.0.0.1:8081"
|
||||||
|
# replay_codes = [200, 302]
|
||||||
# verbosity = 1
|
# verbosity = 1
|
||||||
# scan_limit = 6
|
# scan_limit = 6
|
||||||
# quiet = true
|
# quiet = true
|
||||||
|
|||||||
BIN
img/replay-proxy-demo.gif
Normal file
BIN
img/replay-proxy-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -246,6 +246,35 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
.unwrap_or_default(); // 💎
|
.unwrap_or_default(); // 💎
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.replay_proxy.is_empty() {
|
||||||
|
// i include replay codes logic here because in config.rs, replay codes are set to the
|
||||||
|
// value in status codes, meaning it's never empty
|
||||||
|
|
||||||
|
let mut replay_codes = vec![];
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!("\u{1f3a5}", "Replay Proxy", config.replay_proxy)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 🎥
|
||||||
|
|
||||||
|
for code in &config.replay_codes {
|
||||||
|
replay_codes.push(status_colorizer(&code.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
&mut writer,
|
||||||
|
"{}",
|
||||||
|
format_banner_entry!(
|
||||||
|
"\u{1f4fc}",
|
||||||
|
"Replay Proxy Codes",
|
||||||
|
format!("[{}]", replay_codes.join(", "))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.unwrap_or_default(); // 📼
|
||||||
|
}
|
||||||
|
|
||||||
if !config.headers.is_empty() {
|
if !config.headers.is_empty() {
|
||||||
for (name, value) in &config.headers {
|
for (name, value) in &config.headers {
|
||||||
writeln!(
|
writeln!(
|
||||||
@@ -438,7 +467,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
|||||||
// ⏯
|
// ⏯
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut writer,
|
&mut writer,
|
||||||
" \u{23ef} Press [{}] to {}|{} your scan",
|
" \u{23ef} Press [{}] to {}|{} your scan",
|
||||||
style("ENTER").yellow(),
|
style("ENTER").yellow(),
|
||||||
style("pause").red(),
|
style("pause").red(),
|
||||||
style("resume").green()
|
style("resume").green()
|
||||||
|
|||||||
@@ -62,11 +62,6 @@ pub fn initialize(
|
|||||||
match client.build() {
|
match client.build() {
|
||||||
Ok(client) => client,
|
Ok(client) => client,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(
|
|
||||||
"{} {} Could not create a Client with the given configuration, exiting.",
|
|
||||||
status_colorizer("ERROR"),
|
|
||||||
module_colorizer("Client::build")
|
|
||||||
);
|
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"{} {} {}",
|
"{} {} {}",
|
||||||
status_colorizer("ERROR"),
|
status_colorizer("ERROR"),
|
||||||
|
|||||||
131
src/config.rs
131
src/config.rs
@@ -10,6 +10,7 @@ use std::collections::HashMap;
|
|||||||
use std::env::{current_dir, current_exe};
|
use std::env::{current_dir, current_exe};
|
||||||
use std::fs::read_to_string;
|
use std::fs::read_to_string;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
#[cfg(not(test))]
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -23,6 +24,21 @@ lazy_static! {
|
|||||||
pub static ref PROGRESS_PRINTER: ProgressBar = progress::add_bar("", 0, true);
|
pub static ref PROGRESS_PRINTER: ProgressBar = progress::add_bar("", 0, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// simple helper to clean up some code reuse below; panics under test / exits in prod
|
||||||
|
fn report_and_exit(err: &str) -> ! {
|
||||||
|
eprintln!(
|
||||||
|
"{} {}: {}",
|
||||||
|
status_colorizer("ERROR"),
|
||||||
|
module_colorizer("Configuration::new"),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
panic!();
|
||||||
|
#[cfg(not(test))]
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the final, global configuration of the program.
|
/// Represents the final, global configuration of the program.
|
||||||
///
|
///
|
||||||
/// This struct is the combination of the following:
|
/// This struct is the combination of the following:
|
||||||
@@ -47,6 +63,10 @@ pub struct Configuration {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub proxy: String,
|
pub proxy: String,
|
||||||
|
|
||||||
|
/// Replay Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
|
||||||
|
#[serde(default)]
|
||||||
|
pub replay_proxy: String,
|
||||||
|
|
||||||
/// The target URL
|
/// The target URL
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub target_url: String,
|
pub target_url: String,
|
||||||
@@ -55,6 +75,10 @@ pub struct Configuration {
|
|||||||
#[serde(default = "status_codes")]
|
#[serde(default = "status_codes")]
|
||||||
pub status_codes: Vec<u16>,
|
pub status_codes: Vec<u16>,
|
||||||
|
|
||||||
|
/// Status Codes to replay to the Replay Proxy (default: whatever is passed to --status-code)
|
||||||
|
#[serde(default)]
|
||||||
|
pub replay_codes: Vec<u16>,
|
||||||
|
|
||||||
/// Status Codes to filter out (deny list)
|
/// Status Codes to filter out (deny list)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub filter_status: Vec<u16>,
|
pub filter_status: Vec<u16>,
|
||||||
@@ -63,6 +87,10 @@ pub struct Configuration {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub client: Client,
|
pub client: Client,
|
||||||
|
|
||||||
|
/// Instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html)
|
||||||
|
#[serde(skip)]
|
||||||
|
pub replay_client: Option<Client>,
|
||||||
|
|
||||||
/// Number of concurrent threads (default: 50)
|
/// Number of concurrent threads (default: 50)
|
||||||
#[serde(default = "threads")]
|
#[serde(default = "threads")]
|
||||||
pub threads: usize,
|
pub threads: usize,
|
||||||
@@ -183,11 +211,17 @@ impl Default for Configuration {
|
|||||||
let timeout = timeout();
|
let timeout = timeout();
|
||||||
let user_agent = user_agent();
|
let user_agent = user_agent();
|
||||||
let client = client::initialize(timeout, &user_agent, false, false, &HashMap::new(), None);
|
let client = client::initialize(timeout, &user_agent, false, false, &HashMap::new(), None);
|
||||||
|
let replay_client = None;
|
||||||
|
let status_codes = status_codes();
|
||||||
|
let replay_codes = status_codes.clone();
|
||||||
|
|
||||||
Configuration {
|
Configuration {
|
||||||
client,
|
client,
|
||||||
timeout,
|
timeout,
|
||||||
user_agent,
|
user_agent,
|
||||||
|
replay_codes,
|
||||||
|
status_codes,
|
||||||
|
replay_client,
|
||||||
dont_filter: false,
|
dont_filter: false,
|
||||||
quiet: false,
|
quiet: false,
|
||||||
stdin: false,
|
stdin: false,
|
||||||
@@ -202,15 +236,15 @@ impl Default for Configuration {
|
|||||||
config: String::new(),
|
config: String::new(),
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
target_url: String::new(),
|
target_url: String::new(),
|
||||||
|
replay_proxy: String::new(),
|
||||||
queries: Vec::new(),
|
queries: Vec::new(),
|
||||||
extensions: Vec::new(),
|
extensions: Vec::new(),
|
||||||
filter_size: Vec::new(),
|
filter_size: Vec::new(),
|
||||||
filter_status: Vec::new(),
|
filter_status: Vec::new(),
|
||||||
headers: HashMap::new(),
|
headers: HashMap::new(),
|
||||||
threads: threads(),
|
|
||||||
depth: depth(),
|
depth: depth(),
|
||||||
|
threads: threads(),
|
||||||
wordlist: wordlist(),
|
wordlist: wordlist(),
|
||||||
status_codes: status_codes(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -244,6 +278,8 @@ impl Configuration {
|
|||||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||||
/// - **depth**: `4` (maximum recursion depth)
|
/// - **depth**: `4` (maximum recursion depth)
|
||||||
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
/// - **scan_limit**: `0` (no limit on concurrent scans imposed)
|
||||||
|
/// - **replay_proxy**: `None` (no limit on concurrent scans imposed)
|
||||||
|
/// - **replay_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html)
|
||||||
///
|
///
|
||||||
/// After which, any values defined in a
|
/// After which, any values defined in a
|
||||||
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
|
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
|
||||||
@@ -348,35 +384,35 @@ impl Configuration {
|
|||||||
.unwrap() // already known good
|
.unwrap() // already known good
|
||||||
.map(|code| {
|
.map(|code| {
|
||||||
StatusCode::from_bytes(code.as_bytes())
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
eprintln!(
|
|
||||||
"{} {}: {}",
|
|
||||||
status_colorizer("ERROR"),
|
|
||||||
module_colorizer("Configuration::new"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
exit(1)
|
|
||||||
})
|
|
||||||
.as_u16()
|
.as_u16()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.values_of("replay_codes").is_some() {
|
||||||
|
// replay codes passed in by the user
|
||||||
|
config.replay_codes = args
|
||||||
|
.values_of("replay_codes")
|
||||||
|
.unwrap() // already known good
|
||||||
|
.map(|code| {
|
||||||
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
|
.as_u16()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
// not passed in by the user, use whatever value is held in status_codes
|
||||||
|
config.replay_codes = config.status_codes.clone();
|
||||||
|
}
|
||||||
|
|
||||||
if args.values_of("filter_status").is_some() {
|
if args.values_of("filter_status").is_some() {
|
||||||
config.filter_status = args
|
config.filter_status = args
|
||||||
.values_of("filter_status")
|
.values_of("filter_status")
|
||||||
.unwrap() // already known good
|
.unwrap() // already known good
|
||||||
.map(|code| {
|
.map(|code| {
|
||||||
StatusCode::from_bytes(code.as_bytes())
|
StatusCode::from_bytes(code.as_bytes())
|
||||||
.unwrap_or_else(|e| {
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
eprintln!(
|
|
||||||
"{} {}: {}",
|
|
||||||
status_colorizer("ERROR"),
|
|
||||||
module_colorizer("Configuration::new"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
exit(1)
|
|
||||||
})
|
|
||||||
.as_u16()
|
.as_u16()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -395,15 +431,8 @@ impl Configuration {
|
|||||||
.values_of("filter_size")
|
.values_of("filter_size")
|
||||||
.unwrap() // already known good
|
.unwrap() // already known good
|
||||||
.map(|size| {
|
.map(|size| {
|
||||||
size.parse::<u64>().unwrap_or_else(|e| {
|
size.parse::<u64>()
|
||||||
eprintln!(
|
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||||
"{} {}: {}",
|
|
||||||
status_colorizer("ERROR"),
|
|
||||||
module_colorizer("Configuration::new"),
|
|
||||||
e
|
|
||||||
);
|
|
||||||
exit(1)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
@@ -451,6 +480,10 @@ impl Configuration {
|
|||||||
config.proxy = String::from(args.value_of("proxy").unwrap());
|
config.proxy = String::from(args.value_of("proxy").unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.value_of("replay_proxy").is_some() {
|
||||||
|
config.replay_proxy = String::from(args.value_of("replay_proxy").unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
if args.value_of("user_agent").is_some() {
|
if args.value_of("user_agent").is_some() {
|
||||||
config.user_agent = String::from(args.value_of("user_agent").unwrap());
|
config.user_agent = String::from(args.value_of("user_agent").unwrap());
|
||||||
}
|
}
|
||||||
@@ -526,6 +559,18 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !config.replay_proxy.is_empty() {
|
||||||
|
// only set replay_client when replay_proxy is set
|
||||||
|
config.replay_client = Some(client::initialize(
|
||||||
|
config.timeout,
|
||||||
|
&config.user_agent,
|
||||||
|
config.redirects,
|
||||||
|
config.insecure,
|
||||||
|
&config.headers,
|
||||||
|
Some(&config.replay_proxy),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,6 +619,8 @@ impl Configuration {
|
|||||||
settings.filter_status = settings_to_merge.filter_status;
|
settings.filter_status = settings_to_merge.filter_status;
|
||||||
settings.dont_filter = settings_to_merge.dont_filter;
|
settings.dont_filter = settings_to_merge.dont_filter;
|
||||||
settings.scan_limit = settings_to_merge.scan_limit;
|
settings.scan_limit = settings_to_merge.scan_limit;
|
||||||
|
settings.replay_proxy = settings_to_merge.replay_proxy;
|
||||||
|
settings.replay_codes = settings_to_merge.replay_codes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
/// If present, read in `DEFAULT_CONFIG_NAME` and deserialize the specified values
|
||||||
@@ -610,9 +657,11 @@ mod tests {
|
|||||||
let data = r#"
|
let data = r#"
|
||||||
wordlist = "/some/path"
|
wordlist = "/some/path"
|
||||||
status_codes = [201, 301, 401]
|
status_codes = [201, 301, 401]
|
||||||
|
replay_codes = [201, 301]
|
||||||
threads = 40
|
threads = 40
|
||||||
timeout = 5
|
timeout = 5
|
||||||
proxy = "http://127.0.0.1:8080"
|
proxy = "http://127.0.0.1:8080"
|
||||||
|
replay_proxy = "http://127.0.0.1:8081"
|
||||||
quiet = true
|
quiet = true
|
||||||
verbosity = 1
|
verbosity = 1
|
||||||
scan_limit = 6
|
scan_limit = 6
|
||||||
@@ -645,7 +694,10 @@ mod tests {
|
|||||||
assert_eq!(config.proxy, String::new());
|
assert_eq!(config.proxy, String::new());
|
||||||
assert_eq!(config.target_url, String::new());
|
assert_eq!(config.target_url, String::new());
|
||||||
assert_eq!(config.config, String::new());
|
assert_eq!(config.config, String::new());
|
||||||
|
assert_eq!(config.replay_proxy, String::new());
|
||||||
assert_eq!(config.status_codes, status_codes());
|
assert_eq!(config.status_codes, status_codes());
|
||||||
|
assert_eq!(config.replay_codes, config.status_codes);
|
||||||
|
assert!(config.replay_client.is_none());
|
||||||
assert_eq!(config.threads, threads());
|
assert_eq!(config.threads, threads());
|
||||||
assert_eq!(config.depth, depth());
|
assert_eq!(config.depth, depth());
|
||||||
assert_eq!(config.timeout, timeout());
|
assert_eq!(config.timeout, timeout());
|
||||||
@@ -680,6 +732,13 @@ mod tests {
|
|||||||
assert_eq!(config.status_codes, vec![201, 301, 401]);
|
assert_eq!(config.status_codes, vec![201, 301, 401]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_replay_codes() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.replay_codes, vec![201, 301]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// 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_threads() {
|
fn config_reads_threads() {
|
||||||
@@ -715,6 +774,13 @@ mod tests {
|
|||||||
assert_eq!(config.proxy, "http://127.0.0.1:8080");
|
assert_eq!(config.proxy, "http://127.0.0.1:8080");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// parse the test config and see that the value parsed is correct
|
||||||
|
fn config_reads_replay_proxy() {
|
||||||
|
let config = setup_config_test();
|
||||||
|
assert_eq!(config.replay_proxy, "http://127.0.0.1:8081");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// 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_quiet() {
|
fn config_reads_quiet() {
|
||||||
@@ -825,4 +891,11 @@ mod tests {
|
|||||||
queries.push(("rick".to_string(), "astley".to_string()));
|
queries.push(("rick".to_string(), "astley".to_string()));
|
||||||
assert_eq!(config.queries, queries);
|
assert_eq!(config.queries, queries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
/// test that an error message is printed and panic is called when report_and_exit is called
|
||||||
|
fn config_report_and_exit_works() {
|
||||||
|
report_and_exit("some message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ pub type FeroxChannel<T> = (UnboundedSender<T>, UnboundedReceiver<T>);
|
|||||||
/// Version pulled from Cargo.toml at compile time
|
/// Version pulled from Cargo.toml at compile time
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
/// Maximum number of file descriptors that can be opened during a scan
|
||||||
|
pub const DEFAULT_OPEN_FILE_LIMIT: usize = 8192;
|
||||||
|
|
||||||
/// Default wordlist to use when `-w|--wordlist` isn't specified and not `wordlist` isn't set
|
/// 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.
|
/// in a [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use feroxbuster::{
|
|||||||
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
utils::{ferox_print, get_current_depth, module_colorizer, status_colorizer},
|
||||||
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
FeroxError, FeroxResponse, FeroxResult, SLEEP_DURATION, VERSION,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use feroxbuster::{utils::set_open_file_limit, DEFAULT_OPEN_FILE_LIMIT};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
@@ -294,6 +296,10 @@ fn main() {
|
|||||||
// setup logging based on the number of -v's used
|
// setup logging based on the number of -v's used
|
||||||
logger::initialize(CONFIGURATION.verbosity);
|
logger::initialize(CONFIGURATION.verbosity);
|
||||||
|
|
||||||
|
// this function uses rlimit, which is not supported on windows
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
set_open_file_limit(DEFAULT_OPEN_FILE_LIMIT);
|
||||||
|
|
||||||
if let Ok(mut runtime) = tokio::runtime::Runtime::new() {
|
if let Ok(mut runtime) = tokio::runtime::Runtime::new() {
|
||||||
let future = wrapped_main();
|
let future = wrapped_main();
|
||||||
runtime.block_on(future);
|
runtime.block_on(future);
|
||||||
|
|||||||
@@ -67,6 +67,29 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
"Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)",
|
"Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("replay_proxy")
|
||||||
|
.short("P")
|
||||||
|
.long("replay-proxy")
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("REPLAY_PROXY")
|
||||||
|
.help(
|
||||||
|
"Send only unfiltered requests through a Replay Proxy, instead of all requests",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("replay_codes")
|
||||||
|
.short("R")
|
||||||
|
.long("replay-codes")
|
||||||
|
.value_name("REPLAY_CODE")
|
||||||
|
.takes_value(true)
|
||||||
|
.multiple(true)
|
||||||
|
.use_delimiter(true)
|
||||||
|
.requires("replay_proxy")
|
||||||
|
.help(
|
||||||
|
"Status Codes to send through a Replay Proxy when found (default: --status-codes value)",
|
||||||
|
),
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("status_codes")
|
Arg::with_name("status_codes")
|
||||||
.short("s")
|
.short("s")
|
||||||
@@ -204,7 +227,7 @@ pub fn initialize() -> App<'static, 'static> {
|
|||||||
.multiple(true)
|
.multiple(true)
|
||||||
.use_delimiter(true)
|
.use_delimiter(true)
|
||||||
.help(
|
.help(
|
||||||
"Filter out status codes (deny list) (ex: -C 200 -S 401)",
|
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
use crate::config::{CONFIGURATION, PROGRESS_PRINTER};
|
||||||
use crate::utils::{ferox_print, status_colorizer};
|
use crate::utils::{ferox_print, make_request, status_colorizer};
|
||||||
use crate::{FeroxChannel, FeroxResponse};
|
use crate::{FeroxChannel, FeroxResponse};
|
||||||
use console::strip_ansi_codes;
|
use console::strip_ansi_codes;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -127,6 +127,19 @@ async fn spawn_terminal_reporter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log::trace!("report complete: {}", resp.url());
|
log::trace!("report complete: {}", resp.url());
|
||||||
|
|
||||||
|
if CONFIGURATION.replay_client.is_some()
|
||||||
|
&& CONFIGURATION.replay_codes.contains(&resp.status().as_u16())
|
||||||
|
{
|
||||||
|
// replay proxy specified/client created and this response's status code is one that
|
||||||
|
// should be replayed
|
||||||
|
match make_request(CONFIGURATION.replay_client.as_ref().unwrap(), &resp.url()).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("{}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
log::trace!("exit: spawn_terminal_reporter");
|
log::trace!("exit: spawn_terminal_reporter");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,17 +130,9 @@ fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock<HashSet<Str
|
|||||||
match scanned_urls.write() {
|
match scanned_urls.write() {
|
||||||
// check new url against what's already been scanned
|
// check new url against what's already been scanned
|
||||||
Ok(mut urls) => {
|
Ok(mut urls) => {
|
||||||
let normalized_url = if resp.ends_with('/') {
|
|
||||||
// append a / to the list of 'seen' urls, this is to prevent the case where
|
|
||||||
// 3xx and 2xx duplicate eachother
|
|
||||||
resp.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{}/", resp)
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the set did not contain resp, true is returned.
|
// If the set did not contain resp, true is returned.
|
||||||
// If the set did contain resp, false is returned.
|
// If the set did contain resp, false is returned.
|
||||||
let response = urls.insert(normalized_url);
|
let response = urls.insert(resp.to_string());
|
||||||
|
|
||||||
log::trace!("exit: add_url_to_list_of_scanned_urls -> {}", response);
|
log::trace!("exit: add_url_to_list_of_scanned_urls -> {}", response);
|
||||||
response
|
response
|
||||||
@@ -855,7 +847,7 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
urls.write()
|
urls.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert("http://unknown_url/".to_string()),
|
.insert("http://unknown_url".to_string()),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
117
src/utils.rs
117
src/utils.rs
@@ -1,8 +1,10 @@
|
|||||||
use crate::FeroxResult;
|
use crate::{FeroxError, FeroxResult};
|
||||||
use console::{strip_ansi_codes, style, user_attended};
|
use console::{strip_ansi_codes, style, user_attended};
|
||||||
use indicatif::ProgressBar;
|
use indicatif::ProgressBar;
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use reqwest::{Client, Response};
|
use reqwest::{Client, Response};
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
|
|
||||||
/// Helper function that determines the current depth of a given url
|
/// Helper function that determines the current depth of a given url
|
||||||
@@ -153,6 +155,27 @@ pub fn format_url(
|
|||||||
extension
|
extension
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if Url::parse(&word).is_ok() {
|
||||||
|
// when a full url is passed in as a word to be joined to a base url using
|
||||||
|
// reqwest::Url::join, the result is that the word (url) completely overwrites the base
|
||||||
|
// url, potentially resulting in requests to places that aren't actually the target
|
||||||
|
// specified.
|
||||||
|
//
|
||||||
|
// in order to resolve the issue, we check if the word from the wordlist is a parsable URL
|
||||||
|
// and if so, don't do any further processing
|
||||||
|
let message = format!(
|
||||||
|
"word ({}) from the wordlist is actually a URL, skipping...",
|
||||||
|
word
|
||||||
|
);
|
||||||
|
log::warn!("{}", message);
|
||||||
|
|
||||||
|
let mut err = FeroxError::default();
|
||||||
|
err.message = message;
|
||||||
|
|
||||||
|
log::trace!("exit: format_url -> {}", err);
|
||||||
|
return Err(Box::new(err));
|
||||||
|
}
|
||||||
|
|
||||||
// from reqwest::Url::join
|
// from reqwest::Url::join
|
||||||
// Note: a trailing slash is significant. Without it, the last path component
|
// Note: a trailing slash is significant. Without it, the last path component
|
||||||
// is considered to be a “file” name to be removed to get at the “directory”
|
// is considered to be a “file” name to be removed to get at the “directory”
|
||||||
@@ -238,10 +261,89 @@ pub async fn make_request(client: &Client, url: &Url) -> FeroxResult<Response> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Attempts to set the soft limit for the RLIMIT_NOFILE resource
|
||||||
|
///
|
||||||
|
/// RLIMIT_NOFILE is the maximum number of file descriptors that can be opened by this process
|
||||||
|
///
|
||||||
|
/// The soft limit is the value that the kernel enforces for the corresponding resource.
|
||||||
|
/// The hard limit acts as a ceiling for the soft limit: an unprivileged process may set only its
|
||||||
|
/// soft limit to a value in the range from 0 up to the hard limit, and (irreversibly) lower its
|
||||||
|
/// hard limit.
|
||||||
|
///
|
||||||
|
/// A child process created via fork(2) inherits its parent's resource limits. Resource limits are
|
||||||
|
/// per-process attributes that are shared by all of the threads in a process.
|
||||||
|
///
|
||||||
|
/// Based on the above information, no attempt is made to restore the limit to its pre-scan value
|
||||||
|
/// as the adjustment made here is only valid for the scan itself (and any child processes, of which
|
||||||
|
/// there are none).
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
pub fn set_open_file_limit(limit: usize) -> bool {
|
||||||
|
log::trace!("enter: set_open_file_limit");
|
||||||
|
|
||||||
|
if let Ok((soft, hard)) = getrlimit(Resource::NOFILE) {
|
||||||
|
if hard.as_usize() > limit {
|
||||||
|
// our default open file limit is less than the current hard limit, this means we can
|
||||||
|
// set the soft limit to our default
|
||||||
|
let new_soft_limit = Rlim::from_usize(limit);
|
||||||
|
|
||||||
|
if setrlimit(Resource::NOFILE, new_soft_limit, hard).is_ok() {
|
||||||
|
log::debug!("set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if soft != hard {
|
||||||
|
// hard limit is lower than our default, the next best option is to set the soft limit as
|
||||||
|
// high as the hard limit will allow
|
||||||
|
if setrlimit(Resource::NOFILE, hard, hard).is_ok() {
|
||||||
|
log::debug!("set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed to set a new limit, as limit adjustments are a 'nice to have', we'll just log
|
||||||
|
// and move along
|
||||||
|
log::warn!("could not set open file descriptor limit to {}", limit);
|
||||||
|
|
||||||
|
log::trace!("exit: set_open_file_limit -> {}", false);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit with a low requested limit succeeds
|
||||||
|
fn utils_set_open_file_limit_with_low_requested_limit() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
let lower_limit = hard.as_usize() - 1;
|
||||||
|
assert!(set_open_file_limit(lower_limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit with a high requested limit succeeds
|
||||||
|
fn utils_set_open_file_limit_with_high_requested_limit() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
let higher_limit = hard.as_usize() + 1;
|
||||||
|
// calculate a new soft to ensure soft != hard and hit that logic branch
|
||||||
|
let new_soft = Rlim::from_usize(hard.as_usize() - 1);
|
||||||
|
setrlimit(Resource::NOFILE, new_soft, hard).unwrap();
|
||||||
|
assert!(set_open_file_limit(higher_limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// set_open_file_limit should fail when hard == soft
|
||||||
|
fn utils_set_open_file_limit_with_fails_when_both_limits_are_equal() {
|
||||||
|
let (_, hard) = getrlimit(Resource::NOFILE).unwrap();
|
||||||
|
// calculate a new soft to ensure soft == hard and hit the failure logic branch
|
||||||
|
setrlimit(Resource::NOFILE, hard, hard).unwrap();
|
||||||
|
assert!(!set_open_file_limit(hard.as_usize())); // returns false
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// base url returns 1
|
/// base url returns 1
|
||||||
fn get_current_depth_base_url_returns_1() {
|
fn get_current_depth_base_url_returns_1() {
|
||||||
@@ -352,6 +454,19 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// word that is a fully formed url, should return an error
|
||||||
|
fn format_url_word_that_is_a_url() {
|
||||||
|
let url = format_url(
|
||||||
|
"http://localhost",
|
||||||
|
"http://schmocalhost",
|
||||||
|
false,
|
||||||
|
&Vec::new(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
assert!(url.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// status colorizer uses red for 500s
|
/// status colorizer uses red for 500s
|
||||||
fn status_colorizer_uses_red_for_500s() {
|
fn status_colorizer_uses_red_for_500s() {
|
||||||
|
|||||||
@@ -43,6 +43,46 @@ fn banner_prints_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
|
/// expect to see all mandatory prints + replay proxy
|
||||||
|
fn banner_prints_replay_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let urls = vec![
|
||||||
|
String::from("http://localhost"),
|
||||||
|
String::from("http://schmocalhost"),
|
||||||
|
];
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&urls, "wordlist")?;
|
||||||
|
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--stdin")
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--replay-proxy")
|
||||||
|
.arg("http://127.0.0.1:8081")
|
||||||
|
.pipe_stdin(file)
|
||||||
|
.unwrap()
|
||||||
|
.assert()
|
||||||
|
.success()
|
||||||
|
.stderr(
|
||||||
|
predicate::str::contains("─┬─")
|
||||||
|
.and(predicate::str::contains("Target Url"))
|
||||||
|
.and(predicate::str::contains("http://localhost"))
|
||||||
|
.and(predicate::str::contains("http://schmocalhost"))
|
||||||
|
.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("Replay Proxy"))
|
||||||
|
.and(predicate::str::contains("http://127.0.0.1:8081"))
|
||||||
|
.and(predicate::str::contains("─┴─")),
|
||||||
|
);
|
||||||
|
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
/// expect to see all mandatory prints + multiple headers
|
/// expect to see all mandatory prints + multiple headers
|
||||||
@@ -163,6 +203,37 @@ fn banner_prints_status_codes() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
|
/// expect to see all mandatory prints + replay codes
|
||||||
|
fn banner_prints_replay_codes() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg("http://localhost")
|
||||||
|
.arg("--replay-codes")
|
||||||
|
.arg("200,302")
|
||||||
|
.arg("--replay-proxy")
|
||||||
|
.arg("http://localhost:8081")
|
||||||
|
.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("Timeout (secs)"))
|
||||||
|
.and(predicate::str::contains("User-Agent"))
|
||||||
|
.and(predicate::str::contains("Replay Proxy"))
|
||||||
|
.and(predicate::str::contains("http://localhost:8081"))
|
||||||
|
.and(predicate::str::contains("Replay Proxy Codes"))
|
||||||
|
.and(predicate::str::contains("[200, 302]"))
|
||||||
|
.and(predicate::str::contains("─┴─")),
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||||
/// expect to see all mandatory prints + output file
|
/// expect to see all mandatory prints + output file
|
||||||
|
|||||||
@@ -411,3 +411,52 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box<dyn std:
|
|||||||
teardown_tmp_directory(tmp_dir);
|
teardown_tmp_directory(tmp_dir);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// send a single valid request, expect a 200 response that then gets routed to the replay
|
||||||
|
/// proxy
|
||||||
|
fn scanner_single_request_replayed_to_proxy() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let srv = MockServer::start();
|
||||||
|
let proxy = MockServer::start();
|
||||||
|
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?;
|
||||||
|
|
||||||
|
let mock = Mock::new()
|
||||||
|
.expect_method(GET)
|
||||||
|
.expect_path("/LICENSE")
|
||||||
|
.return_status(200)
|
||||||
|
.return_body("this is a test")
|
||||||
|
.create_on(&srv);
|
||||||
|
|
||||||
|
let mock_two = Mock::new()
|
||||||
|
.expect_method(GET)
|
||||||
|
.expect_path("/LICENSE")
|
||||||
|
.return_status(200)
|
||||||
|
.return_body("this is a test")
|
||||||
|
.create_on(&proxy);
|
||||||
|
|
||||||
|
let cmd = Command::cargo_bin("feroxbuster")
|
||||||
|
.unwrap()
|
||||||
|
.arg("--url")
|
||||||
|
.arg(srv.url("/"))
|
||||||
|
.arg("--wordlist")
|
||||||
|
.arg(file.as_os_str())
|
||||||
|
.arg("--replay-proxy")
|
||||||
|
.arg(format!("http://{}", proxy.address().to_string()))
|
||||||
|
.arg("--replay-codes")
|
||||||
|
.arg("200")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
cmd.assert()
|
||||||
|
.success()
|
||||||
|
.stdout(
|
||||||
|
predicate::str::contains("/LICENSE")
|
||||||
|
.and(predicate::str::contains("200"))
|
||||||
|
.and(predicate::str::contains("14")),
|
||||||
|
)
|
||||||
|
.stderr(predicate::str::contains("Replay Proxy Codes"));
|
||||||
|
|
||||||
|
assert_eq!(mock.times_called(), 1);
|
||||||
|
assert_eq!(mock_two.times_called(), 1);
|
||||||
|
teardown_tmp_directory(tmp_dir);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user