added query params and dontfilter options

This commit is contained in:
epi
2020-09-20 19:36:28 -05:00
parent 6b2e54b35b
commit ab450d67fc
9 changed files with 176 additions and 27 deletions

View File

@@ -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"

View File

@@ -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 => {

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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(), // ¯\_(ツ)_/¯
}
}

View File

@@ -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",
)),
);