mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-24 14:01:12 -03:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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]
|
||||
name = "feroxbuster"
|
||||
version = "1.4.1"
|
||||
version = "1.5.2"
|
||||
authors = ["Ben 'epi' Risher <epibar052@gmail.com>"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
@@ -33,6 +33,7 @@ openssl = { version = "0.10", features = ["vendored"] }
|
||||
dirs = "3.0"
|
||||
regex = "1"
|
||||
crossterm = "0.18"
|
||||
rlimit = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
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)
|
||||
- [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)
|
||||
- [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)
|
||||
- [Common Problems/Issues (FAQ)](#-common-problemsissues-faq)
|
||||
- [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"
|
||||
# status_codes = [200, 500]
|
||||
# filter_status = [301]
|
||||
# replay_codes = [301]
|
||||
# threads = 1
|
||||
# timeout = 5
|
||||
# proxy = "http://127.0.0.1:8080"
|
||||
# replay_proxy = "http://127.0.0.1:8081"
|
||||
# verbosity = 1
|
||||
# scan_limit = 6
|
||||
# quiet = true
|
||||
@@ -335,11 +338,15 @@ OPTIONS:
|
||||
-d, --depth <RECURSION_DEPTH> Maximum recursion depth, a depth of 0 is infinite recursion (default: 4)
|
||||
-x, --extensions <FILE_EXTENSION>... File extension(s) to search for (ex: -x php -x pdf js)
|
||||
-S, --filter-size <SIZE>... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)
|
||||
-C, --filter-status <STATUS_CODE>... Filter out status codes (deny list) (ex: -C 200 -S 401)
|
||||
-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')
|
||||
-o, --output <FILE> Output file to write results to (default: stdout)
|
||||
-p, --proxy <PROXY> Proxy to use for requests (ex: http(s)://host:port, socks5://host:port)
|
||||
-Q, --query <QUERY>... Specify URL query parameters (ex: -Q token=stuff -Q secret=key)
|
||||
-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)
|
||||
-s, --status-codes <STATUS_CODE>... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401
|
||||
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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc...
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
# threads = 1
|
||||
# timeout = 5
|
||||
# proxy = "http://127.0.0.1:8080"
|
||||
# replay_proxy = "http://127.0.0.1:8081"
|
||||
# replay_codes = [200, 302]
|
||||
# verbosity = 1
|
||||
# scan_limit = 6
|
||||
# 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(); // 💎
|
||||
}
|
||||
|
||||
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() {
|
||||
for (name, value) in &config.headers {
|
||||
writeln!(
|
||||
@@ -438,7 +467,7 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
// ⏯
|
||||
writeln!(
|
||||
&mut writer,
|
||||
" \u{23ef} Press [{}] to {}|{} your scan",
|
||||
" \u{23ef} Press [{}] to {}|{} your scan",
|
||||
style("ENTER").yellow(),
|
||||
style("pause").red(),
|
||||
style("resume").green()
|
||||
|
||||
@@ -62,11 +62,6 @@ pub fn initialize(
|
||||
match client.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{} {} Could not create a Client with the given configuration, exiting.",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Client::build")
|
||||
);
|
||||
eprintln!(
|
||||
"{} {} {}",
|
||||
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::fs::read_to_string;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(not(test))]
|
||||
use std::process::exit;
|
||||
|
||||
lazy_static! {
|
||||
@@ -23,6 +24,21 @@ lazy_static! {
|
||||
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.
|
||||
///
|
||||
/// This struct is the combination of the following:
|
||||
@@ -47,6 +63,10 @@ pub struct Configuration {
|
||||
#[serde(default)]
|
||||
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
|
||||
#[serde(default)]
|
||||
pub target_url: String,
|
||||
@@ -55,6 +75,10 @@ pub struct Configuration {
|
||||
#[serde(default = "status_codes")]
|
||||
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)
|
||||
#[serde(default)]
|
||||
pub filter_status: Vec<u16>,
|
||||
@@ -63,6 +87,10 @@ pub struct Configuration {
|
||||
#[serde(skip)]
|
||||
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)
|
||||
#[serde(default = "threads")]
|
||||
pub threads: usize,
|
||||
@@ -183,11 +211,17 @@ impl Default for Configuration {
|
||||
let timeout = timeout();
|
||||
let user_agent = user_agent();
|
||||
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 {
|
||||
client,
|
||||
timeout,
|
||||
user_agent,
|
||||
replay_codes,
|
||||
status_codes,
|
||||
replay_client,
|
||||
dont_filter: false,
|
||||
quiet: false,
|
||||
stdin: false,
|
||||
@@ -202,15 +236,15 @@ impl Default for Configuration {
|
||||
config: String::new(),
|
||||
output: String::new(),
|
||||
target_url: String::new(),
|
||||
replay_proxy: String::new(),
|
||||
queries: Vec::new(),
|
||||
extensions: Vec::new(),
|
||||
filter_size: Vec::new(),
|
||||
filter_status: Vec::new(),
|
||||
headers: HashMap::new(),
|
||||
threads: threads(),
|
||||
depth: depth(),
|
||||
threads: threads(),
|
||||
wordlist: wordlist(),
|
||||
status_codes: status_codes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +278,8 @@ impl Configuration {
|
||||
/// - **dont_filter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
/// - **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
|
||||
/// [ferox-config.toml](constant.DEFAULT_CONFIG_NAME.html) config file will override the
|
||||
@@ -348,35 +384,35 @@ impl Configuration {
|
||||
.unwrap() // already known good
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||
.as_u16()
|
||||
})
|
||||
.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() {
|
||||
config.filter_status = args
|
||||
.values_of("filter_status")
|
||||
.unwrap() // already known good
|
||||
.map(|code| {
|
||||
StatusCode::from_bytes(code.as_bytes())
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||
.as_u16()
|
||||
})
|
||||
.collect();
|
||||
@@ -395,15 +431,8 @@ impl Configuration {
|
||||
.values_of("filter_size")
|
||||
.unwrap() // already known good
|
||||
.map(|size| {
|
||||
size.parse::<u64>().unwrap_or_else(|e| {
|
||||
eprintln!(
|
||||
"{} {}: {}",
|
||||
status_colorizer("ERROR"),
|
||||
module_colorizer("Configuration::new"),
|
||||
e
|
||||
);
|
||||
exit(1)
|
||||
})
|
||||
size.parse::<u64>()
|
||||
.unwrap_or_else(|e| report_and_exit(&e.to_string()))
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
@@ -451,6 +480,10 @@ impl Configuration {
|
||||
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() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -574,6 +619,8 @@ impl Configuration {
|
||||
settings.filter_status = settings_to_merge.filter_status;
|
||||
settings.dont_filter = settings_to_merge.dont_filter;
|
||||
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
|
||||
@@ -610,9 +657,11 @@ mod tests {
|
||||
let data = r#"
|
||||
wordlist = "/some/path"
|
||||
status_codes = [201, 301, 401]
|
||||
replay_codes = [201, 301]
|
||||
threads = 40
|
||||
timeout = 5
|
||||
proxy = "http://127.0.0.1:8080"
|
||||
replay_proxy = "http://127.0.0.1:8081"
|
||||
quiet = true
|
||||
verbosity = 1
|
||||
scan_limit = 6
|
||||
@@ -645,7 +694,10 @@ mod tests {
|
||||
assert_eq!(config.proxy, String::new());
|
||||
assert_eq!(config.target_url, 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.replay_codes, config.status_codes);
|
||||
assert!(config.replay_client.is_none());
|
||||
assert_eq!(config.threads, threads());
|
||||
assert_eq!(config.depth, depth());
|
||||
assert_eq!(config.timeout, timeout());
|
||||
@@ -680,6 +732,13 @@ mod tests {
|
||||
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]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_threads() {
|
||||
@@ -715,6 +774,13 @@ mod tests {
|
||||
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]
|
||||
/// parse the test config and see that the value parsed is correct
|
||||
fn config_reads_quiet() {
|
||||
@@ -825,4 +891,11 @@ mod tests {
|
||||
queries.push(("rick".to_string(), "astley".to_string()));
|
||||
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
|
||||
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
|
||||
/// 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},
|
||||
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 std::{
|
||||
collections::HashSet,
|
||||
@@ -294,6 +296,10 @@ fn main() {
|
||||
// setup logging based on the number of -v's used
|
||||
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() {
|
||||
let future = wrapped_main();
|
||||
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)",
|
||||
),
|
||||
)
|
||||
.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::with_name("status_codes")
|
||||
.short("s")
|
||||
@@ -204,7 +227,7 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
.help(
|
||||
"Filter out status codes (deny list) (ex: -C 200 -S 401)",
|
||||
"Filter out status codes (deny list) (ex: -C 200 -C 401)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 console::strip_ansi_codes;
|
||||
use std::io::Write;
|
||||
@@ -127,6 +127,19 @@ async fn spawn_terminal_reporter(
|
||||
}
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
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 indicatif::ProgressBar;
|
||||
use reqwest::Url;
|
||||
use reqwest::{Client, Response};
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use rlimit::{getrlimit, setrlimit, Resource, Rlim};
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Helper function that determines the current depth of a given url
|
||||
@@ -153,6 +155,27 @@ pub fn format_url(
|
||||
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
|
||||
// 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”
|
||||
@@ -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)]
|
||||
mod tests {
|
||||
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]
|
||||
/// 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]
|
||||
/// 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(())
|
||||
}
|
||||
|
||||
#[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 allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + multiple headers
|
||||
@@ -163,6 +203,37 @@ fn banner_prints_status_codes() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// expect to see all mandatory prints + 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 allows non-existent wordlist to trigger the banner printing to stderr
|
||||
/// 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);
|
||||
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