mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-06-07 10:01:12 -03:00
added query params and dontfilter options
This commit is contained in:
@@ -21,14 +21,16 @@
|
||||
# norecursion = true
|
||||
# addslash = true
|
||||
# stdin = true
|
||||
# dontfilter = true
|
||||
# depth = 1
|
||||
# sizefilters = [5174]
|
||||
|
||||
|
||||
# headers can be specified on multiple lines or as an inline table
|
||||
# headers and queries can be specified on multiple lines or as an inline table
|
||||
#
|
||||
# inline example
|
||||
# headers = {"stuff" = "things"}
|
||||
# queries = {"mostuff" = "mothings"}
|
||||
#
|
||||
# multi-line example
|
||||
# note: if multi-line is used, all key/value pairs under it belong to the headers table until the next table
|
||||
@@ -36,4 +38,8 @@
|
||||
#
|
||||
# [headers]
|
||||
# stuff = "things"
|
||||
# mostuff = "mothings"
|
||||
# more = "headers"
|
||||
#
|
||||
# [queries]
|
||||
# mostuff = "mothings"
|
||||
# more = "queries"
|
||||
@@ -122,6 +122,15 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
}
|
||||
}
|
||||
|
||||
if !CONFIGURATION.queries.is_empty() {
|
||||
for query in &CONFIGURATION.queries {
|
||||
println!(
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f914}", "Query Parameter", format!("{}={}", query.0, query.1))
|
||||
); // 🤔
|
||||
}
|
||||
}
|
||||
|
||||
if !CONFIGURATION.output.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -154,6 +163,13 @@ by Ben "epi" Risher {} ver: {}"#,
|
||||
); // 📍
|
||||
}
|
||||
|
||||
if CONFIGURATION.dontfilter {
|
||||
println!(
|
||||
"{}",
|
||||
format_banner_entry!("\u{1f92a}", "Filter Wildcards", !CONFIGURATION.dontfilter)
|
||||
); // 🤪
|
||||
}
|
||||
|
||||
match CONFIGURATION.verbosity {
|
||||
//speaker medium volume (increasing with verbosity to loudspeaker)
|
||||
1 => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::utils::status_colorizer;
|
||||
use reqwest::header::HeaderMap;
|
||||
use reqwest::{redirect::Policy, Client, Proxy};
|
||||
use crate::utils::status_colorizer;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryInto;
|
||||
use std::process::exit;
|
||||
@@ -24,7 +24,11 @@ pub fn initialize(
|
||||
let header_map: HeaderMap = match headers.try_into() {
|
||||
Ok(map) => map,
|
||||
Err(e) => {
|
||||
eprintln!("[{}] - Client::initialize: {}", status_colorizer("ERROR"), e);
|
||||
eprintln!(
|
||||
"[{}] - Client::initialize: {}",
|
||||
status_colorizer("ERROR"),
|
||||
e
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
@@ -40,8 +44,16 @@ pub fn initialize(
|
||||
match Proxy::all(proxy.unwrap()) {
|
||||
Ok(proxy_obj) => client.proxy(proxy_obj),
|
||||
Err(e) => {
|
||||
eprintln!("[{}] - Could not add proxy ({:?}) to Client configuration", status_colorizer("ERROR"), proxy);
|
||||
eprintln!("[{}] - Client::initialize: {}", status_colorizer("ERROR"), e);
|
||||
eprintln!(
|
||||
"[{}] - Could not add proxy ({:?}) to Client configuration",
|
||||
status_colorizer("ERROR"),
|
||||
proxy
|
||||
);
|
||||
eprintln!(
|
||||
"[{}] - Client::initialize: {}",
|
||||
status_colorizer("ERROR"),
|
||||
e
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +64,10 @@ 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"));
|
||||
eprintln!(
|
||||
"[{}] - Could not create a Client with the given configuration, exiting.",
|
||||
status_colorizer("ERROR")
|
||||
);
|
||||
eprintln!("[{}] - Client::build: {}", status_colorizer("ERROR"), e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::utils::status_colorizer;
|
||||
use crate::{client, parser};
|
||||
use crate::{DEFAULT_CONFIG_NAME, DEFAULT_STATUS_CODES, DEFAULT_WORDLIST, VERSION};
|
||||
use crate::utils::status_colorizer;
|
||||
use clap::value_t;
|
||||
use lazy_static::lazy_static;
|
||||
use reqwest::{Client, StatusCode};
|
||||
@@ -59,6 +59,8 @@ pub struct Configuration {
|
||||
#[serde(default)]
|
||||
pub headers: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub queries: Vec<(String, String)>,
|
||||
#[serde(default)]
|
||||
pub norecursion: bool,
|
||||
#[serde(default)]
|
||||
pub addslash: bool,
|
||||
@@ -68,6 +70,8 @@ pub struct Configuration {
|
||||
pub depth: usize,
|
||||
#[serde(default)]
|
||||
pub sizefilters: Vec<u64>,
|
||||
#[serde(default)]
|
||||
pub dontfilter: bool,
|
||||
}
|
||||
|
||||
// functions timeout, threads, statuscodes, useragent, wordlist, and depth are used to provide
|
||||
@@ -105,6 +109,7 @@ impl Default for Configuration {
|
||||
client,
|
||||
timeout,
|
||||
useragent,
|
||||
dontfilter: false,
|
||||
quiet: false,
|
||||
stdin: false,
|
||||
verbosity: 0,
|
||||
@@ -115,6 +120,7 @@ impl Default for Configuration {
|
||||
proxy: String::new(),
|
||||
output: String::new(),
|
||||
target_url: String::new(),
|
||||
queries: Vec::new(),
|
||||
extensions: Vec::new(),
|
||||
sizefilters: Vec::new(),
|
||||
headers: HashMap::new(),
|
||||
@@ -145,9 +151,11 @@ impl Configuration {
|
||||
/// - **extensions**: `None`
|
||||
/// - **sizefilters**: `None`
|
||||
/// - **headers**: `None`
|
||||
/// - **queries**: `None`
|
||||
/// - **norecursion**: `false` (recursively scan enumerated sub-directories)
|
||||
/// - **addslash**: `false`
|
||||
/// - **stdin**: `false`
|
||||
/// - **dontfilter**: `false` (auto filter wildcard responses)
|
||||
/// - **depth**: `4` (maximum recursion depth)
|
||||
///
|
||||
/// After which, any values defined in a
|
||||
@@ -184,11 +192,13 @@ impl Configuration {
|
||||
config.insecure = settings.insecure;
|
||||
config.extensions = settings.extensions;
|
||||
config.headers = settings.headers;
|
||||
config.queries = settings.queries;
|
||||
config.norecursion = settings.norecursion;
|
||||
config.addslash = settings.addslash;
|
||||
config.stdin = settings.stdin;
|
||||
config.depth = settings.depth;
|
||||
config.sizefilters = settings.sizefilters;
|
||||
config.dontfilter = settings.dontfilter;
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -259,6 +269,10 @@ impl Configuration {
|
||||
config.quiet = args.is_present("quiet");
|
||||
}
|
||||
|
||||
if args.is_present("dontfilter") {
|
||||
config.dontfilter = args.is_present("dontfilter");
|
||||
}
|
||||
|
||||
if args.occurrences_of("verbosity") > 0 {
|
||||
// occurrences_of returns 0 if none are found; this is protected in
|
||||
// an if block for the same reason as the quiet option
|
||||
@@ -317,6 +331,19 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
if args.values_of("queries").is_some() {
|
||||
for val in args.values_of("queries").unwrap() {
|
||||
// same basic logic used as reading in the headers HashMap above
|
||||
let mut split_val = val.split('=');
|
||||
|
||||
let name = split_val.next().unwrap().trim();
|
||||
|
||||
let value = split_val.collect::<Vec<&str>>().join("=");
|
||||
|
||||
config.queries.push((name.to_string(), value.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
// this if statement determines if we've gotten a Client configuration change from
|
||||
// either the config file or command line arguments; if we have, we need to rebuild
|
||||
// the client and store it in the config struct
|
||||
@@ -365,7 +392,11 @@ impl Configuration {
|
||||
return Some(config);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[{}] - config::parse_config {}", status_colorizer("ERROR"), e);
|
||||
println!(
|
||||
"[{}] - config::parse_config {}",
|
||||
status_colorizer("ERROR"),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -393,9 +424,11 @@ mod tests {
|
||||
insecure = true
|
||||
extensions = ["html", "php", "js"]
|
||||
headers = {stuff = "things", mostuff = "mothings"}
|
||||
queries = {name = "value", rick = "astley"}
|
||||
norecursion = true
|
||||
addslash = true
|
||||
stdin = true
|
||||
dontfilter = true
|
||||
depth = 1
|
||||
sizefilters = [4120]
|
||||
"#;
|
||||
@@ -417,6 +450,7 @@ mod tests {
|
||||
assert_eq!(config.timeout, timeout());
|
||||
assert_eq!(config.verbosity, 0);
|
||||
assert_eq!(config.quiet, false);
|
||||
assert_eq!(config.dontfilter, false);
|
||||
assert_eq!(config.norecursion, false);
|
||||
assert_eq!(config.stdin, false);
|
||||
assert_eq!(config.addslash, false);
|
||||
@@ -425,6 +459,7 @@ mod tests {
|
||||
assert_eq!(config.extensions, Vec::<String>::new());
|
||||
assert_eq!(config.sizefilters, Vec::<u64>::new());
|
||||
assert_eq!(config.headers, HashMap::new());
|
||||
assert_eq!(config.queries, HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -505,6 +540,12 @@ mod tests {
|
||||
assert_eq!(config.stdin, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_reads_dontfilter() {
|
||||
let config = setup_config_test();
|
||||
assert_eq!(config.dontfilter, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_reads_addslash() {
|
||||
let config = setup_config_test();
|
||||
@@ -531,4 +572,13 @@ mod tests {
|
||||
headers.insert("mostuff".to_string(), "mothings".to_string());
|
||||
assert_eq!(config.headers, headers);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_reads_queries() {
|
||||
let config = setup_config_test();
|
||||
let mut queries = HashMap::new();
|
||||
queries.insert("name".to_string(), "value".to_string());
|
||||
queries.insert("rick".to_string(), "astley".to_string());
|
||||
assert_eq!(config.queries, queries);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,12 @@ fn unique_string(length: usize) -> String {
|
||||
pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
||||
log::trace!("enter: wildcard_test({:?})", target_url);
|
||||
|
||||
if CONFIGURATION.dontfilter {
|
||||
// early return, dontfilter scans don't need tested
|
||||
log::trace!("exit: wildcard_test -> None");
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(resp_one) = make_wildcard_request(&target_url, 1).await {
|
||||
// found a wildcard response
|
||||
let mut wildcard = WildcardFilter::default();
|
||||
@@ -73,14 +79,14 @@ pub async fn wildcard_test(target_url: &str) -> Option<WildcardFilter> {
|
||||
status_colorizer("WILDCARD")
|
||||
);
|
||||
println!(
|
||||
"[{}] - Auto-filtering out responses that are [({} + url length) bytes] long; this behavior can be turned off by using --dumb",
|
||||
"[{}] - Auto-filtering out responses that are [({} + url length) bytes] long; this behavior can be turned off by using --dontfilter",
|
||||
status_colorizer("WILDCARD"),
|
||||
wc_length - url_len,
|
||||
);
|
||||
|
||||
wildcard.dynamic = wc_length - url_len;
|
||||
} else if wc_length == wc2_length {
|
||||
println!("[{}] - Wildcard response is a static size; auto-filtering out responses of size [{} bytes]; this behavior can be turned off by using --dumb", status_colorizer("WILDCARD"), wc_length);
|
||||
println!("[{}] - Wildcard response is a static size; auto-filtering out responses of size [{} bytes]; this behavior can be turned off by using --dontfilter", status_colorizer("WILDCARD"), wc_length);
|
||||
|
||||
wildcard.size = wc_length;
|
||||
}
|
||||
@@ -208,3 +214,15 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec<String> {
|
||||
|
||||
good_urls
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unique_string_returns_correct_length() {
|
||||
for i in 0..10 {
|
||||
assert_eq!(unique_string(i).len(), i * 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,13 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
.takes_value(false)
|
||||
.help("Only print URLs; Don't print status codes, response size, running config, etc...")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("dontfilter")
|
||||
.short("D")
|
||||
.long("dontfilter")
|
||||
.takes_value(false)
|
||||
.help("Don't auto-filter wildcard responses")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("output")
|
||||
.short("o")
|
||||
@@ -142,6 +149,18 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
"Specify HTTP headers (ex: -H Header:val 'stuff: things')",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("queries")
|
||||
.short("Q")
|
||||
.long("query")
|
||||
.value_name("QUERY")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.use_delimiter(true)
|
||||
.help(
|
||||
"Specify URL query parameters (ex: -Q token=stuff -Q secret=key)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("norecursion")
|
||||
.short("n")
|
||||
@@ -185,7 +204,7 @@ pub fn initialize() -> App<'static, 'static> {
|
||||
The command above adds .pdf, .js, .html, .php, .txt, .json, and .docx to each url
|
||||
|
||||
All of the methods above (multiple flags, space separated, comma separated, etc...) are valid
|
||||
and interchangeable. The same goes for urls, headers, status codes, and size filters.
|
||||
and interchangeable. The same goes for urls, headers, status codes, queries, and size filters.
|
||||
|
||||
EXAMPLES:
|
||||
Multiple headers:
|
||||
@@ -198,7 +217,13 @@ EXAMPLES:
|
||||
cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files
|
||||
|
||||
Proxy traffic through Burp
|
||||
./feroxbuster -u http://127.1 --insecure -p http://127.0.0.1:8080
|
||||
./feroxbuster -u http://127.1 --insecure --proxy http://127.0.0.1:8080
|
||||
|
||||
Proxy traffic through a SOCKS proxy
|
||||
./feroxbuster -u http://127.1 --proxy socks5://127.0.0.1:9050
|
||||
|
||||
Pass auth token via query parameter
|
||||
./feroxbuster -u http://127.1 --query token=0123456789ABCDEF
|
||||
|
||||
Ludicrous speed... go!
|
||||
./feroxbuster -u http://127.1 -t 200
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn format_url(
|
||||
url.to_string()
|
||||
};
|
||||
|
||||
let base_url = reqwest::Url::parse(&url)?;
|
||||
let base_url = reqwest::Url::parse(&url)?;
|
||||
|
||||
// extensions and slashes are mutually exclusive cases
|
||||
let word = if extension.is_some() {
|
||||
@@ -57,8 +57,23 @@ pub fn format_url(
|
||||
|
||||
match base_url.join(&word) {
|
||||
Ok(request) => {
|
||||
log::trace!("exit: format_url -> {}", request);
|
||||
Ok(request)
|
||||
if CONFIGURATION.queries.is_empty() {
|
||||
// no query params to process
|
||||
log::trace!("exit: format_url -> {}", request);
|
||||
Ok(request)
|
||||
} else {
|
||||
match reqwest::Url::parse_with_params(request.as_str(), &CONFIGURATION.queries) {
|
||||
Ok(req_w_params) => {
|
||||
log::trace!("exit: format_url -> {}", req_w_params);
|
||||
Ok(req_w_params) // request with params attached
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Could not add query params {:?} to {}: {}", CONFIGURATION.queries, request, e);
|
||||
log::trace!("exit: format_url -> {}", request);
|
||||
Ok(request) // couldn't process params, return initially ok url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::trace!("exit: format_url -> {}", e);
|
||||
@@ -402,16 +417,14 @@ async fn make_requests(
|
||||
continue;
|
||||
}
|
||||
|
||||
if filter.size > 0 && filter.size == *content_len && true {
|
||||
// todo replace with --dumb logic
|
||||
if filter.size > 0 && filter.size == *content_len && !CONFIGURATION.dontfilter {
|
||||
// static wildcard size found during testing
|
||||
// size isn't default, size equals response length, and it's not a 'dumb' scan
|
||||
// size isn't default, size equals response length, and auto-filter is on
|
||||
log::debug!("static wildcard: filtered out {}", response.url());
|
||||
continue;
|
||||
}
|
||||
|
||||
if filter.dynamic > 0 && true {
|
||||
// todo replace with --dumb logic
|
||||
if filter.dynamic > 0 && !CONFIGURATION.dontfilter {
|
||||
// dynamic wildcard offset found during testing
|
||||
|
||||
// I'm about to manually split this url path instead of using reqwest::Url's
|
||||
@@ -476,10 +489,16 @@ pub async fn scan_url(target_url: &str, wordlist: Arc<HashSet<String>>, base_dep
|
||||
async move { spawn_recursion_handler(rx_dir, recurser_words, base_depth).await },
|
||||
);
|
||||
|
||||
let filter = if let Some(f) = heuristics::wildcard_test(&target_url).await {
|
||||
Arc::new(f)
|
||||
} else {
|
||||
Arc::new(WildcardFilter::default())
|
||||
let filter = match heuristics::wildcard_test(&target_url).await {
|
||||
Some(f) => {
|
||||
if CONFIGURATION.dontfilter {
|
||||
// don't auto filter, i.e. use the defaults
|
||||
Arc::new(WildcardFilter::default())
|
||||
} else {
|
||||
Arc::new(f)
|
||||
}
|
||||
}
|
||||
None => Arc::new(WildcardFilter::default()),
|
||||
};
|
||||
|
||||
// producer tasks (mp of mpsc); responsible for making requests
|
||||
|
||||
@@ -65,7 +65,7 @@ pub fn status_colorizer(status: &str) -> String {
|
||||
Some('4') => Red.paint(status).to_string(), // client error
|
||||
Some('5') => Red.paint(status).to_string(), // server error
|
||||
Some('W') => Cyan.paint(status).to_string(), // wildcard
|
||||
Some('E') => Red.paint(status).to_string(), // wildcard
|
||||
Some('E') => Red.paint(status).to_string(), // wildcard
|
||||
_ => status.to_string(), // ¯\_(ツ)_/¯
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box<dyn std::error::Error
|
||||
"Url is being reflected in wildcard response, i.e. a dynamic wildcard",
|
||||
))
|
||||
.and(predicate::str::contains(
|
||||
"Auto-filtering out responses that are [(14 + url length) bytes] long; this behavior can be turned off by using --dumb",
|
||||
"Auto-filtering out responses that are [(14 + url length) bytes] long; this behavior can be turned off by using --dontfilter",
|
||||
)),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user