From 2d67336b86b254a8ddd3b25cfa90cca2c3e2019f Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 06:05:25 -0500 Subject: [PATCH 01/13] new filter system appears to work --- src/filters.rs | 93 ++++++++++++++++++++++++++++++ src/heuristics.rs | 63 +++++++-------------- src/lib.rs | 3 +- src/scanner.rs | 141 +++++++++++++++++++++++----------------------- 4 files changed, 185 insertions(+), 115 deletions(-) create mode 100644 src/filters.rs diff --git a/src/filters.rs b/src/filters.rs new file mode 100644 index 00000000..89c4bc28 --- /dev/null +++ b/src/filters.rs @@ -0,0 +1,93 @@ +use crate::utils::get_url_path_length; +use crate::FeroxResponse; +use std::any::Any; +use std::fmt::Debug; + +// references: +// https://dev.to/magnusstrale/rust-trait-objects-in-a-vector-non-trivial-4co5 +// https://stackoverflow.com/questions/25339603/how-to-test-for-equality-between-trait-objects + +/// FeroxFilter trait; represents different types of possible filters that can be applied to +/// responses +pub trait FeroxFilter: Debug + Send + Sync { + /// Determine whether or not this particular filter should be applied or not + fn should_filter_response(&self, response: &FeroxResponse) -> bool; + + /// delegates to the FeroxFilter-implementing type which gives us the actual type of self + fn box_eq(&self, other: &dyn Any) -> bool; + + /// gives us `other` as Any in box_eq + fn as_any(&self) -> &dyn Any; +} + +/// implementation of PartialEq, necessary long-form due to "trait cannot be made into an object" +/// error when attempting to derive PartialEq on the trait itself +impl PartialEq for Box { + /// Perform a comparison of two implementors of the FeroxFilter trait + fn eq(&self, other: &Box) -> bool { + self.box_eq(other.as_any()) + } +} + +/// Data holder for two pieces of data needed when auto-filtering out wildcard responses +/// +/// `dynamic` is the size of the response that will later be combined with the length +/// of the path of the url requested and used to determine interesting pages from custom +/// 404s where the requested url is reflected back in the response +/// +/// `size` is size of the response that should be included with filters passed via runtime +/// configuration and any static wildcard lengths. +#[derive(Debug, Default, Clone, PartialEq)] +pub struct WildcardFilter { + /// size of the response that will later be combined with the length of the path of the url + /// requested + pub dynamic: u64, + + /// size of the response that should be included with filters passed via runtime configuration + pub size: u64, +} + +impl FeroxFilter for WildcardFilter { + /// Examine size, dynamic, and content_len to determine whether or not the response received + /// is a wildcard response and therefore should be filtered out + fn should_filter_response(&self, response: &FeroxResponse) -> bool { + log::trace!("enter: should_filter_response({:?} {:?})", self, response); + + if self.size > 0 && self.size == response.content_length() { + // static wildcard size found during testing + // size isn't default, size equals response length, and auto-filter is on + log::debug!("static wildcard: filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + + if self.dynamic > 0 { + // dynamic wildcard offset found during testing + + // I'm about to manually split this url path instead of using reqwest::Url's + // builtin parsing. The reason is that they call .split() on the url path + // except that I don't want an empty string taking up the last index in the + // event that the url ends with a forward slash. It's ugly enough to be split + // into its own function for readability. + let url_len = get_url_path_length(&response.url()); + + if url_len + self.dynamic == response.content_length() { + log::debug!("dynamic wildcard: filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + log::trace!("exit: should_filter_response -> false"); + false + } + + /// Compare one WildcardFilter to another + fn box_eq(&self, other: &dyn Any) -> bool { + other.downcast_ref::().map_or(false, |a| self == a) + } + + /// Return seld as Any for dynamic dispatch purposes + fn as_any(&self) -> &dyn Any { + self + } +} diff --git a/src/heuristics.rs b/src/heuristics.rs index 1e0d7be3..c67feaa1 100644 --- a/src/heuristics.rs +++ b/src/heuristics.rs @@ -1,11 +1,12 @@ use crate::config::{CONFIGURATION, PROGRESS_PRINTER}; +use crate::filters::WildcardFilter; use crate::scanner::should_filter_response; use crate::utils::{ ferox_print, format_url, get_url_path_length, make_request, module_colorizer, status_colorizer, }; +use crate::FeroxResponse; use console::style; use indicatif::ProgressBar; -use reqwest::Response; use std::process; use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; @@ -13,24 +14,6 @@ use uuid::Uuid; /// length of a standard UUID, used when determining wildcard responses const UUID_LENGTH: u64 = 32; -/// Data holder for two pieces of data needed when auto-filtering out wildcard responses -/// -/// `dynamic` is the size of the response that will later be combined with the length -/// of the path of the url requested and used to determine interesting pages from custom -/// 404s where the requested url is reflected back in the response -/// -/// `size` is size of the response that should be included with filters passed via runtime -/// configuration and any static wildcard lengths. -#[derive(Default, Debug, PartialEq, Copy, Clone)] -pub struct WildcardFilter { - /// size of the response that will later be combined with the length of the path of the url - /// requested - pub dynamic: u64, - - /// size of the response that should be included with filters passed via runtime configuration - pub size: u64, -} - /// Simple helper to return a uuid, formatted as lowercase without hyphens /// /// `length` determines the number of uuids to string together. Each uuid @@ -75,13 +58,13 @@ pub async fn wildcard_test( let clone_req_one = tx_file.clone(); let clone_req_two = tx_file.clone(); - if let Some(resp_one) = make_wildcard_request(&target_url, 1, clone_req_one).await { + if let Some(ferox_response) = make_wildcard_request(&target_url, 1, clone_req_one).await { bar.inc(1); // found a wildcard response let mut wildcard = WildcardFilter::default(); - let wc_length = resp_one.content_length().unwrap_or(0); + let wc_length = ferox_response.content_length(); if wc_length == 0 { log::trace!("exit: wildcard_test -> Some({:?})", wildcard); @@ -93,18 +76,16 @@ pub async fn wildcard_test( if let Some(resp_two) = make_wildcard_request(&target_url, 3, clone_req_two).await { bar.inc(1); - let wc2_length = resp_two.content_length().unwrap_or(0); + let wc2_length = resp_two.content_length(); if wc2_length == wc_length + (UUID_LENGTH * 2) { // second length is what we'd expect to see if the requested url is // reflected in the response along with some static content; aka custom 404 - let url_len = get_url_path_length(&resp_one.url()); + let url_len = get_url_path_length(&ferox_response.url()); wildcard.dynamic = wc_length - url_len; - if !CONFIGURATION.quiet - && !should_filter_response(&wildcard.dynamic, &resp_one.url()) - { + if !CONFIGURATION.quiet { // && !wildcard.should_filter_response(&ferox_response) { let msg = format!( "{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", status_colorizer("WLD"), @@ -125,8 +106,7 @@ pub async fn wildcard_test( } else if wc_length == wc2_length { wildcard.size = wc_length; - if !CONFIGURATION.quiet && !should_filter_response(&wildcard.size, &resp_one.url()) - { + if !CONFIGURATION.quiet { // && !wildcard.should_filter_response(&ferox_response) { let msg = format!( "{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", status_colorizer("WLD"), @@ -167,7 +147,7 @@ async fn make_wildcard_request( target_url: &str, length: usize, tx_file: UnboundedSender, -) -> Option { +) -> Option { log::trace!( "enter: make_wildcard_request({}, {}, {:?})", target_url, @@ -201,16 +181,17 @@ async fn make_wildcard_request( .contains(&response.status().as_u16()) { // found a wildcard response - let url_len = get_url_path_length(&response.url()); - let content_len = response.content_length().unwrap_or(0); + let ferox_response = FeroxResponse::from(response, false).await; + let url_len = get_url_path_length(&ferox_response.url()); + let content_len = ferox_response.content_length(); - if !CONFIGURATION.quiet && !should_filter_response(&content_len, &response.url()) { + if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) { let msg = format!( "{} {:>10} Got {} for {} (url length: {})\n", wildcard, content_len, - status_colorizer(&response.status().as_str()), - response.url(), + status_colorizer(&ferox_response.status().as_str()), + ferox_response.url(), url_len ); @@ -223,18 +204,16 @@ async fn make_wildcard_request( ); } - if response.status().is_redirection() { + if ferox_response.status().is_redirection() { // show where it goes, if possible - if let Some(next_loc) = response.headers().get("Location") { + if let Some(next_loc) = ferox_response.headers().get("Location") { let next_loc_str = next_loc.to_str().unwrap_or("Unknown"); - if !CONFIGURATION.quiet - && !should_filter_response(&content_len, &response.url()) - { + if !CONFIGURATION.quiet && !should_filter_response(&ferox_response) { let msg = format!( "{} {:>10} {} redirects to => {}\n", wildcard, content_len, - response.url(), + ferox_response.url(), next_loc_str ); @@ -248,8 +227,8 @@ async fn make_wildcard_request( } } } - log::trace!("exit: make_wildcard_request -> {:?}", response); - return Some(response); + log::trace!("exit: make_wildcard_request -> {:?}", ferox_response); + return Some(ferox_response); } } Err(e) => { diff --git a/src/lib.rs b/src/lib.rs index 73a92d41..8d92bc81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod banner; pub mod client; pub mod config; pub mod extractor; +pub mod filters; pub mod heuristics; pub mod logger; pub mod parser; @@ -61,7 +62,7 @@ pub const DEFAULT_STATUS_CODES: [StatusCode; 9] = [ pub const DEFAULT_CONFIG_NAME: &str = "ferox-config.toml"; /// A `FeroxResponse`, derived from a `Response` to a submitted `Request` -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FeroxResponse { /// The final `Url` of this `FeroxResponse` url: Url, diff --git a/src/scanner.rs b/src/scanner.rs index 0bd49314..3f1324c6 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -1,7 +1,7 @@ use crate::config::{CONFIGURATION, PROGRESS_BAR}; use crate::extractor::get_links; -use crate::heuristics::WildcardFilter; -use crate::utils::{format_url, get_current_depth, get_url_path_length, make_request}; +use crate::filters::{FeroxFilter, WildcardFilter}; +use crate::utils::{format_url, get_current_depth, make_request}; use crate::{heuristics, progress, FeroxChannel, FeroxResponse}; use futures::future::{BoxFuture, FutureExt}; use futures::{stream, StreamExt}; @@ -23,8 +23,8 @@ lazy_static! { /// Set of urls that have been sent to [scan_url](fn.scan_url.html), used for deduplication static ref SCANNED_URLS: RwLock> = RwLock::new(HashSet::new()); - /// Vector of WildcardFilters that have been ID'd through heuristics - static ref WILDCARD_FILTERS: Arc>>> = Arc::new(RwLock::new(Vec::>::new())); + /// Vector of implementors of the FeroxFilter trait + static ref FILTERS: Arc>>> = Arc::new(RwLock::new(Vec::>::new())); /// Bounded semaphore used as a barrier to limit concurrent scans static ref SCAN_LIMITER: Semaphore = Semaphore::new(CONFIGURATION.scan_limit); @@ -67,12 +67,12 @@ fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock, - wildcard_filters: Arc>>>, + filter: Box, + wildcard_filters: Arc>>>, ) -> bool { log::trace!( "enter: add_filter_to_list_of_wildcard_filters({:?}, {:?})", @@ -207,7 +207,9 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec /// or if the Location header is present and matches the base url + / (3xx) fn response_is_directory(response: &FeroxResponse) -> bool { log::trace!("enter: is_directory({:?})", response); - + if response.url().as_str().contains("/api") { + log::warn!("response: {:?}", response); + } if response.status().is_redirection() { // status code is 3xx match response.headers().get("Location") { @@ -335,45 +337,42 @@ async fn try_recursion( log::trace!("exit: try_recursion"); } +/// Given a `FeroxResponse` and a `FeroxFilter` determine whether or not to apply the filter to +/// the response +pub fn should_filter(response: &FeroxResponse, filter: Box) -> bool { + log::trace!("enter: should_filter({:?}, {:?})", response, filter); + + let result = filter.should_filter_response(&response); + + log::trace!("exit: should_filter -> {}", result); + result +} + /// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported /// to the user or not. -pub fn should_filter_response(content_len: &u64, url: &Url) -> bool { - if CONFIGURATION.sizefilters.contains(content_len) { - // filtered value from --sizefilters, move on to the next url - log::debug!("size filter: filtered out {}", url); +pub fn should_filter_response(response: &FeroxResponse) -> bool { + if CONFIGURATION + .sizefilters + .contains(&response.content_length()) + { + // filtered value from --sizefilters, sizefilters and wildcards are two separate filters + // and are applied independently + log::debug!("size filter: filtered out {}", response.url()); return true; } - match WILDCARD_FILTERS.read() { + if CONFIGURATION.dontfilter { + // quick return if dontfilter is set + return false; + } + + match FILTERS.read() { Ok(filters) => { for filter in filters.iter() { - if CONFIGURATION.dontfilter { - // quick return if dontfilter is set - return false; - } - - if filter.size > 0 && filter.size == *content_len { - // static wildcard size found during testing - // size isn't default, size equals response length, and auto-filter is on - log::debug!("static wildcard: filtered out {}", url); + // wildcard.should_filter goes here + if filter.should_filter_response(&response) { return true; } - - if filter.dynamic > 0 { - // dynamic wildcard offset found during testing - - // I'm about to manually split this url path instead of using reqwest::Url's - // builtin parsing. The reason is that they call .split() on the url path - // except that I don't want an empty string taking up the last index in the - // event that the url ends with a forward slash. It's ugly enough to be split - // into its own function for readability. - let url_len = get_url_path_length(&url); - - if url_len + filter.dynamic == *content_len { - log::debug!("dynamic wildcard: filtered out {}", url); - return true; - } - } } } Err(e) => { @@ -419,9 +418,7 @@ async fn make_requests( // purposefully doing recursion before filtering. the thought process is that // even though this particular url is filtered, subsequent urls may not - let content_len = &ferox_response.content_length(); - - if should_filter_response(content_len, &ferox_response.url()) { + if should_filter_response(&ferox_response) { continue; } @@ -458,8 +455,7 @@ async fn make_requests( FeroxResponse::from(new_response, CONFIGURATION.extract_links).await; // filter if necessary - let new_content_len = &new_ferox_response.content_length(); - if should_filter_response(new_content_len, &new_ferox_response.url()) { + if should_filter_response(&new_ferox_response) { continue; } @@ -596,11 +592,11 @@ pub async fn scan_url( let filter = match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_file_clone).await { - Some(f) => Arc::new(f), - None => Arc::new(WildcardFilter::default()), + Some(f) => Box::new(f), + None => Box::new(WildcardFilter::default()), }; - add_filter_to_list_of_wildcard_filters(filter.clone(), WILDCARD_FILTERS.clone()); + add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); // producer tasks (mp of mpsc); responsible for making requests let producers = stream::iter(looping_words.deref().to_owned()) @@ -780,29 +776,30 @@ mod tests { assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false); } - #[test] - /// add a wildcard filter with the `size` attribute set to WILDCARD_FILTERS and ensure that - /// should_filter_response correctly returns true - fn should_filter_response_filters_wildcard_size() { - let mut filter = WildcardFilter::default(); - let url = Url::parse("http://localhost").unwrap(); - filter.size = 18; - let filter = Arc::new(filter); - add_filter_to_list_of_wildcard_filters(filter, WILDCARD_FILTERS.clone()); - let result = should_filter_response(&18, &url); - assert!(result); - } - - #[test] - /// add a wildcard filter with the `dynamic` attribute set to WILDCARD_FILTERS and ensure that - /// should_filter_response correctly returns true - fn should_filter_response_filters_wildcard_dynamic() { - let mut filter = WildcardFilter::default(); - let url = Url::parse("http://localhost/some-path").unwrap(); - filter.dynamic = 9; - let filter = Arc::new(filter); - add_filter_to_list_of_wildcard_filters(filter, WILDCARD_FILTERS.clone()); - let result = should_filter_response(&18, &url); - assert!(result); - } + // todo check coverage and remove + // #[test] + // /// add a wildcard filter with the `size` attribute set to FILTERS and ensure that + // /// should_filter_response correctly returns true + // fn should_filter_response_filters_wildcard_size() { + // let mut filter = WildcardFilter::default(); + // let url = Url::parse("http://localhost").unwrap(); + // filter.size = 18; + // let filter = Box::new(filter); + // add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); + // let result = should_filter_response(&18, &url); + // assert!(result); + // } + // + // #[test] + // /// add a wildcard filter with the `dynamic` attribute set to FILTERS and ensure that + // /// should_filter_response correctly returns true + // fn should_filter_response_filters_wildcard_dynamic() { + // let mut filter = WildcardFilter::default(); + // let url = Url::parse("http://localhost/some-path").unwrap(); + // filter.dynamic = 9; + // let filter = Arc::new(filter); + // add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); + // let result = should_filter_response(&18, &url); + // assert!(result); + // } } From acf16c92cd7756aaf9afa066344018caf42568cd Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 06:11:07 -0500 Subject: [PATCH 02/13] removed lint from heuristics --- src/heuristics.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/heuristics.rs b/src/heuristics.rs index c67feaa1..1ab31bc2 100644 --- a/src/heuristics.rs +++ b/src/heuristics.rs @@ -85,7 +85,7 @@ pub async fn wildcard_test( wildcard.dynamic = wc_length - url_len; - if !CONFIGURATION.quiet { // && !wildcard.should_filter_response(&ferox_response) { + if !CONFIGURATION.quiet { let msg = format!( "{} {:>10} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", status_colorizer("WLD"), @@ -106,7 +106,7 @@ pub async fn wildcard_test( } else if wc_length == wc2_length { wildcard.size = wc_length; - if !CONFIGURATION.quiet { // && !wildcard.should_filter_response(&ferox_response) { + if !CONFIGURATION.quiet { let msg = format!( "{} {:>10} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", status_colorizer("WLD"), From d3ddefa0b73e69eb524bc9e3201ab496de72d65d Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 06:13:25 -0500 Subject: [PATCH 03/13] removed lint and dead code from scanner --- src/scanner.rs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/scanner.rs b/src/scanner.rs index 3f1324c6..a6900068 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -207,9 +207,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec /// or if the Location header is present and matches the base url + / (3xx) fn response_is_directory(response: &FeroxResponse) -> bool { log::trace!("enter: is_directory({:?})", response); - if response.url().as_str().contains("/api") { - log::warn!("response: {:?}", response); - } + if response.status().is_redirection() { // status code is 3xx match response.headers().get("Location") { @@ -337,17 +335,6 @@ async fn try_recursion( log::trace!("exit: try_recursion"); } -/// Given a `FeroxResponse` and a `FeroxFilter` determine whether or not to apply the filter to -/// the response -pub fn should_filter(response: &FeroxResponse, filter: Box) -> bool { - log::trace!("enter: should_filter({:?}, {:?})", response, filter); - - let result = filter.should_filter_response(&response); - - log::trace!("exit: should_filter -> {}", result); - result -} - /// Simple helper to stay DRY; determines whether or not a given `FeroxResponse` should be reported /// to the user or not. pub fn should_filter_response(response: &FeroxResponse) -> bool { From 254f502ed3bd88e4d0eab7fcdb34b061f2591da6 Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 06:33:21 -0500 Subject: [PATCH 04/13] removed lint from scanner --- src/scanner.rs | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/src/scanner.rs b/src/scanner.rs index a6900068..6cc229e2 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -762,31 +762,4 @@ mod tests { assert_eq!(add_url_to_list_of_scanned_urls(url, &urls), false); } - - // todo check coverage and remove - // #[test] - // /// add a wildcard filter with the `size` attribute set to FILTERS and ensure that - // /// should_filter_response correctly returns true - // fn should_filter_response_filters_wildcard_size() { - // let mut filter = WildcardFilter::default(); - // let url = Url::parse("http://localhost").unwrap(); - // filter.size = 18; - // let filter = Box::new(filter); - // add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); - // let result = should_filter_response(&18, &url); - // assert!(result); - // } - // - // #[test] - // /// add a wildcard filter with the `dynamic` attribute set to FILTERS and ensure that - // /// should_filter_response correctly returns true - // fn should_filter_response_filters_wildcard_dynamic() { - // let mut filter = WildcardFilter::default(); - // let url = Url::parse("http://localhost/some-path").unwrap(); - // filter.dynamic = 9; - // let filter = Arc::new(filter); - // add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); - // let result = should_filter_response(&18, &url); - // assert!(result); - // } } From 665564bbfe15d0410d79edb9cbeefd82e6dc8f3e Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 16:17:50 -0500 Subject: [PATCH 05/13] refactored long option names --- Cargo.toml | 2 +- README.md | 30 ++++---- ferox-config.toml.example | 13 ++-- src/banner.rs | 22 +++--- src/client.rs | 4 +- src/config.rs | 146 ++++++++++++++++++++------------------ src/heuristics.rs | 14 ++-- src/parser.rs | 40 +++++++---- src/reporter.rs | 2 +- src/scanner.rs | 19 ++--- src/utils.rs | 6 +- tests/test_banner.rs | 10 +-- tests/test_extractor.rs | 4 +- tests/test_heuristics.rs | 18 ++--- 14 files changed, 175 insertions(+), 155 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3f92644b..f4101ade 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "feroxbuster" -version = "1.2.0" +version = "1.3.0" authors = ["Ben 'epi' Risher "] license = "MIT" edition = "2018" diff --git a/README.md b/README.md index b8d17c71..2c15bf14 100644 --- a/README.md +++ b/README.md @@ -221,8 +221,8 @@ Configuration begins with with the following built-in default values baked into - threads: `50` - verbosity: `0` (no logging enabled) - scan_limit: `0` (no limit imposed on concurrent scans) -- statuscodes: `200 204 301 302 307 308 401 403 405` -- useragent: `feroxbuster/VERSION` +- status_codes: `200 204 301 302 307 308 401 403 405` +- user_agent: `feroxbuster/VERSION` - recursion depth: `4` - auto-filter wildcards - `true` - output: `stdout` @@ -272,7 +272,7 @@ A pre-made configuration file with examples of all available settings can be fou # Any setting used here can be overridden by the corresponding command line option/argument # # wordlist = "/wordlists/jhaddix/all.txt" -# statuscodes = [200, 500] +# status_codes = [200, 500] # threads = 1 # timeout = 5 # proxy = "http://127.0.0.1:8080" @@ -280,17 +280,17 @@ A pre-made configuration file with examples of all available settings can be fou # scan_limit = 6 # quiet = true # output = "/targets/ellingson_mineral_company/gibson.txt" -# useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" +# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" # redirects = true # insecure = true # extensions = ["php", "html"] -# norecursion = true -# addslash = true +# no_recursion = true +# add_slash = true # stdin = true -# dontfilter = true +# dont_filter = true # extract_links = true # depth = 1 -# sizefilters = [5174] +# filter_size = [5174] # queries = [["name","value"], ["rick", "astley"]] # headers can be specified on multiple lines or as an inline table @@ -315,13 +315,13 @@ USAGE: feroxbuster [FLAGS] [OPTIONS] --url ... FLAGS: - -f, --addslash Append / to each request - -D, --dontfilter Don't auto-filter wildcard responses + -f, --add-slash Append / to each request + -D, --dont-filter Don't auto-filter wildcard responses -e, --extract-links Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false) -h, --help Prints help information -k, --insecure Disables TLS certificate validation - -n, --norecursion Do not scan recursively + -n, --no-recursion Do not scan recursively -q, --quiet Only print URLs; Don't print status codes, response size, running config, etc... -r, --redirects Follow redirects --stdin Read url(s) from STDIN @@ -336,12 +336,12 @@ OPTIONS: -p, --proxy Proxy to use for requests (ex: http(s)://host:port, socks5://host:port) -Q, --query ... Specify URL query parameters (ex: -Q token=stuff -Q secret=key) -L, --scan-limit Limit total number of concurrent scans (default: 7) - -S, --sizefilter ... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970) - -s, --statuscodes ... Status Codes of interest (default: 200 204 301 302 307 308 401 403 405) + -S, --filter-size ... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970) + -s, --status-codes ... Status Codes of interest (default: 200 204 301 302 307 308 401 403 405) -t, --threads Number of concurrent threads (default: 50) -T, --timeout Number of seconds before a request times out (default: 7) -u, --url ... The target URL(s) (required, unless --stdin used) - -a, --useragent Sets the User-Agent (default: feroxbuster/VERSION) + -a, --user-agent Sets the User-Agent (default: feroxbuster/VERSION) -w, --wordlist Path to the wordlist ``` @@ -399,7 +399,7 @@ With `--extract-links` ### IPv6, non-recursive scan with INFO-level logging enabled ``` -./feroxbuster -u http://[::1] --norecursion -vv +./feroxbuster -u http://[::1] --no-recursion -vv ``` ### Read urls from STDIN; pipe only resulting urls out to another tool diff --git a/ferox-config.toml.example b/ferox-config.toml.example index 10a52352..a079a55a 100644 --- a/ferox-config.toml.example +++ b/ferox-config.toml.example @@ -8,7 +8,8 @@ # Any setting used here can be overridden by the corresponding command line option/argument # # wordlist = "/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt" -# statuscodes = [200, 500] +# status_codes = [200, 500] +# filter_status = [301] # threads = 1 # timeout = 5 # proxy = "http://127.0.0.1:8080" @@ -16,17 +17,17 @@ # scan_limit = 6 # quiet = true # output = "/targets/ellingson_mineral_company/gibson.txt" -# useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" +# user_agent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0" # redirects = true # insecure = true # extensions = ["php", "html"] -# norecursion = true -# addslash = true +# no_recursion = true +# add_slash = true # stdin = true -# dontfilter = true +# dont_filter = true # extract_links = true # depth = 1 -# sizefilters = [5174] +# filter_size = [5174] # queries = [["name","value"], ["rick", "astley"]] # headers can be specified on multiple lines or as an inline table diff --git a/src/banner.rs b/src/banner.rs index fe94b547..87bed6d4 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -161,7 +161,7 @@ by Ben "epi" Risher {} ver: {}"#, let mut codes = vec![]; - for code in &config.statuscodes { + for code in &config.status_codes { codes.push(status_colorizer(&code.to_string())) } @@ -200,7 +200,7 @@ by Ben "epi" Risher {} ver: {}"#, writeln!( &mut writer, "{}", - format_banner_entry!("\u{1F9a1}", "User-Agent", config.useragent) + format_banner_entry!("\u{1F9a1}", "User-Agent", config.user_agent) ) .unwrap_or_default(); // ðŸĶĄ @@ -234,8 +234,8 @@ by Ben "epi" Risher {} ver: {}"#, } } - if !config.sizefilters.is_empty() { - for filter in &config.sizefilters { + if !config.filter_size.is_empty() { + for filter in &config.filter_size { writeln!( &mut writer, "{}", @@ -309,11 +309,11 @@ by Ben "epi" Risher {} ver: {}"#, .unwrap_or_default(); // 📍 } - if config.dontfilter { + if config.dont_filter { writeln!( &mut writer, "{}", - format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dontfilter) + format_banner_entry!("\u{1f92a}", "Filter Wildcards", !config.dont_filter) ) .unwrap_or_default(); // ðŸĪŠ } @@ -355,16 +355,16 @@ by Ben "epi" Risher {} ver: {}"#, _ => {} } - if config.addslash { + if config.add_slash { writeln!( &mut writer, "{}", - format_banner_entry!("\u{1fa93}", "Add Slash", config.addslash) + format_banner_entry!("\u{1fa93}", "Add Slash", config.add_slash) ) .unwrap_or_default(); // 🊓 } - if !config.norecursion { + if !config.no_recursion { if config.depth == 0 { writeln!( &mut writer, @@ -384,7 +384,7 @@ by Ben "epi" Risher {} ver: {}"#, writeln!( &mut writer, "{}", - format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.norecursion) + format_banner_entry!("\u{1f6ab}", "Do Not Recurse", config.no_recursion) ) .unwrap_or_default(); // ðŸšŦ } @@ -436,7 +436,7 @@ mod tests { /// test to hit no execution of statuscode for loop in banner async fn banner_intialize_without_status_codes() { let mut config = Configuration::default(); - config.statuscodes = vec![]; + config.status_codes = vec![]; initialize( &[String::from("http://localhost")], &config, diff --git a/src/client.rs b/src/client.rs index 4b4e13ca..abed9af5 100644 --- a/src/client.rs +++ b/src/client.rs @@ -10,7 +10,7 @@ use std::time::Duration; /// Create and return an instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html) pub fn initialize( timeout: u64, - useragent: &str, + user_agent: &str, redirects: bool, insecure: bool, headers: &HashMap, @@ -27,7 +27,7 @@ pub fn initialize( let client = Client::builder() .timeout(Duration::new(timeout, 0)) - .user_agent(useragent) + .user_agent(user_agent) .danger_accept_invalid_certs(insecure) .default_headers(header_map) .redirect(policy); diff --git a/src/config.rs b/src/config.rs index 2102512e..0993e903 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,9 +51,13 @@ pub struct Configuration { #[serde(default)] pub target_url: String, - /// Status Codes of interest (default: 200 204 301 302 307 308 401 403 405) - #[serde(default = "statuscodes")] - pub statuscodes: Vec, + /// Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405) + #[serde(default = "status_codes")] + pub status_codes: Vec, + + /// Status Codes to filter out (deny list) + #[serde(default)] + pub filter_status: Vec, /// Instance of [reqwest::Client](https://docs.rs/reqwest/latest/reqwest/struct.Client.html) #[serde(skip)] @@ -80,8 +84,8 @@ pub struct Configuration { pub output: String, /// Sets the User-Agent (default: feroxbuster/VERSION) - #[serde(default = "useragent")] - pub useragent: String, + #[serde(default = "user_agent")] + pub user_agent: String, /// Follow redirects #[serde(default)] @@ -105,7 +109,7 @@ pub struct Configuration { /// Do not scan recursively #[serde(default)] - pub norecursion: bool, + pub no_recursion: bool, /// Extract links from html/javscript #[serde(default)] @@ -113,7 +117,7 @@ pub struct Configuration { /// Append / to each request #[serde(default)] - pub addslash: bool, + pub add_slash: bool, /// Read url(s) from STDIN #[serde(default)] @@ -129,14 +133,14 @@ pub struct Configuration { /// Filter out messages of a particular size #[serde(default)] - pub sizefilters: Vec, + pub filter_size: Vec, /// Don't auto-filter wildcard responses #[serde(default)] - pub dontfilter: bool, + pub dont_filter: bool, } -// functions timeout, threads, statuscodes, useragent, wordlist, and depth are used to provide +// functions timeout, threads, status_codes, user_agent, wordlist, and depth are used to provide // defaults in the event that a ferox-config.toml is found but one or more of the values below // aren't listed in the config. This way, we get the correct defaults upon Deserialization @@ -151,7 +155,7 @@ fn threads() -> usize { } /// default status codes -fn statuscodes() -> Vec { +fn status_codes() -> Vec { DEFAULT_STATUS_CODES .iter() .map(|code| code.as_u16()) @@ -163,8 +167,8 @@ fn wordlist() -> String { String::from(DEFAULT_WORDLIST) } -/// default useragent -fn useragent() -> String { +/// default user-agent +fn user_agent() -> String { format!("feroxbuster/{}", VERSION) } @@ -177,22 +181,22 @@ impl Default for Configuration { /// Builds the default Configuration for feroxbuster fn default() -> Self { let timeout = timeout(); - let useragent = useragent(); - let client = client::initialize(timeout, &useragent, false, false, &HashMap::new(), None); + let user_agent = user_agent(); + let client = client::initialize(timeout, &user_agent, false, false, &HashMap::new(), None); Configuration { client, timeout, - useragent, - dontfilter: false, + user_agent, + dont_filter: false, quiet: false, stdin: false, verbosity: 0, scan_limit: 0, - addslash: false, + add_slash: false, insecure: false, redirects: false, - norecursion: false, + no_recursion: false, extract_links: false, proxy: String::new(), config: String::new(), @@ -200,12 +204,13 @@ impl Default for Configuration { target_url: String::new(), queries: Vec::new(), extensions: Vec::new(), - sizefilters: Vec::new(), + filter_size: Vec::new(), + filter_status: Vec::new(), headers: HashMap::new(), threads: threads(), depth: depth(), wordlist: wordlist(), - statuscodes: statuscodes(), + status_codes: status_codes(), } } } @@ -223,19 +228,20 @@ impl Configuration { /// - **timeout**: `7` seconds /// - **verbosity**: `0` (no logging enabled) /// - **proxy**: `None` - /// - **statuscodes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html) + /// - **status_codes**: [`DEFAULT_RESPONSE_CODES`](constant.DEFAULT_RESPONSE_CODES.html) + /// - **filter_status**: `None` /// - **output**: `None` (print to stdout) /// - **quiet**: `false` - /// - **useragent**: `feroxer/VERSION` + /// - **user_agent**: `feroxer/VERSION` /// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs) /// - **extensions**: `None` - /// - **sizefilters**: `None` + /// - **filter_size**: `None` /// - **headers**: `None` /// - **queries**: `None` - /// - **norecursion**: `false` (recursively scan enumerated sub-directories) - /// - **addslash**: `false` + /// - **no_recursion**: `false` (recursively scan enumerated sub-directories) + /// - **add_slash**: `false` /// - **stdin**: `false` - /// - **dontfilter**: `false` (auto filter wildcard responses) + /// - **dont_filter**: `false` (auto filter wildcard responses) /// - **depth**: `4` (maximum recursion depth) /// - **scan_limit**: `0` (no limit on concurrent scans imposed) /// @@ -336,9 +342,9 @@ impl Configuration { config.output = String::from(args.value_of("output").unwrap()); } - if args.values_of("statuscodes").is_some() { - config.statuscodes = args - .values_of("statuscodes") + if args.values_of("status_codes").is_some() { + config.status_codes = args + .values_of("status_codes") .unwrap() // already known good .map(|code| { StatusCode::from_bytes(code.as_bytes()) @@ -364,9 +370,9 @@ impl Configuration { .collect(); } - if args.values_of("sizefilters").is_some() { - config.sizefilters = args - .values_of("sizefilters") + if args.values_of("filter_size").is_some() { + config.filter_size = args + .values_of("filter_size") .unwrap() // already known good .map(|size| { size.parse::().unwrap_or_else(|e| { @@ -390,8 +396,8 @@ impl Configuration { config.quiet = args.is_present("quiet"); } - if args.is_present("dontfilter") { - config.dontfilter = args.is_present("dontfilter"); + if args.is_present("dont_filter") { + config.dont_filter = args.is_present("dont_filter"); } if args.occurrences_of("verbosity") > 0 { @@ -400,12 +406,12 @@ impl Configuration { config.verbosity = args.occurrences_of("verbosity") as u8; } - if args.is_present("norecursion") { - config.norecursion = args.is_present("norecursion"); + if args.is_present("no_recursion") { + config.no_recursion = args.is_present("no_recursion"); } - if args.is_present("addslash") { - config.addslash = args.is_present("addslash"); + if args.is_present("add_slash") { + config.add_slash = args.is_present("add_slash"); } if args.is_present("extract_links") { @@ -425,8 +431,8 @@ impl Configuration { config.proxy = String::from(args.value_of("proxy").unwrap()); } - if args.value_of("useragent").is_some() { - config.useragent = String::from(args.value_of("useragent").unwrap()); + if args.value_of("user_agent").is_some() { + config.user_agent = String::from(args.value_of("user_agent").unwrap()); } if args.value_of("timeout").is_some() { @@ -474,7 +480,7 @@ impl Configuration { // the client and store it in the config struct if !config.proxy.is_empty() || config.timeout != timeout() - || config.useragent != useragent() + || config.user_agent != user_agent() || config.redirects || config.insecure || !config.headers.is_empty() @@ -482,7 +488,7 @@ impl Configuration { if config.proxy.is_empty() { config.client = client::initialize( config.timeout, - &config.useragent, + &config.user_agent, config.redirects, config.insecure, &config.headers, @@ -491,7 +497,7 @@ impl Configuration { } else { config.client = client::initialize( config.timeout, - &config.useragent, + &config.user_agent, config.redirects, config.insecure, &config.headers, @@ -527,25 +533,25 @@ impl Configuration { fn merge_config(settings: &mut Self, settings_to_merge: Self) { settings.threads = settings_to_merge.threads; settings.wordlist = settings_to_merge.wordlist; - settings.statuscodes = settings_to_merge.statuscodes; + settings.status_codes = settings_to_merge.status_codes; settings.proxy = settings_to_merge.proxy; settings.timeout = settings_to_merge.timeout; settings.verbosity = settings_to_merge.verbosity; settings.quiet = settings_to_merge.quiet; settings.output = settings_to_merge.output; - settings.useragent = settings_to_merge.useragent; + settings.user_agent = settings_to_merge.user_agent; settings.redirects = settings_to_merge.redirects; settings.insecure = settings_to_merge.insecure; settings.extract_links = settings_to_merge.extract_links; settings.extensions = settings_to_merge.extensions; settings.headers = settings_to_merge.headers; settings.queries = settings_to_merge.queries; - settings.norecursion = settings_to_merge.norecursion; - settings.addslash = settings_to_merge.addslash; + settings.no_recursion = settings_to_merge.no_recursion; + settings.add_slash = settings_to_merge.add_slash; settings.stdin = settings_to_merge.stdin; settings.depth = settings_to_merge.depth; - settings.sizefilters = settings_to_merge.sizefilters; - settings.dontfilter = settings_to_merge.dontfilter; + settings.filter_size = settings_to_merge.filter_size; + settings.dont_filter = settings_to_merge.dont_filter; settings.scan_limit = settings_to_merge.scan_limit; } @@ -582,7 +588,7 @@ mod tests { fn setup_config_test() -> Configuration { let data = r#" wordlist = "/some/path" - statuscodes = [201, 301, 401] + status_codes = [201, 301, 401] threads = 40 timeout = 5 proxy = "http://127.0.0.1:8080" @@ -595,13 +601,13 @@ mod tests { extensions = ["html", "php", "js"] headers = {stuff = "things", mostuff = "mothings"} queries = [["name","value"], ["rick", "astley"]] - norecursion = true - addslash = true + no_recursion = true + add_slash = true stdin = true - dontfilter = true + dont_filter = true extract_links = true depth = 1 - sizefilters = [4120] + filter_size = [4120] "#; let tmp_dir = TempDir::new().unwrap(); let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME); @@ -617,23 +623,23 @@ mod tests { assert_eq!(config.proxy, String::new()); assert_eq!(config.target_url, String::new()); assert_eq!(config.config, String::new()); - assert_eq!(config.statuscodes, statuscodes()); + assert_eq!(config.status_codes, status_codes()); assert_eq!(config.threads, threads()); assert_eq!(config.depth, depth()); assert_eq!(config.timeout, timeout()); assert_eq!(config.verbosity, 0); assert_eq!(config.scan_limit, 0); assert_eq!(config.quiet, false); - assert_eq!(config.dontfilter, false); - assert_eq!(config.norecursion, false); + assert_eq!(config.dont_filter, false); + assert_eq!(config.no_recursion, false); assert_eq!(config.stdin, false); - assert_eq!(config.addslash, false); + assert_eq!(config.add_slash, false); assert_eq!(config.redirects, false); assert_eq!(config.extract_links, false); assert_eq!(config.insecure, false); assert_eq!(config.queries, Vec::new()); assert_eq!(config.extensions, Vec::::new()); - assert_eq!(config.sizefilters, Vec::::new()); + assert_eq!(config.filter_size, Vec::::new()); assert_eq!(config.headers, HashMap::new()); } @@ -646,9 +652,9 @@ mod tests { #[test] /// parse the test config and see that the value parsed is correct - fn config_reads_statuscodes() { + fn config_reads_status_codes() { let config = setup_config_test(); - assert_eq!(config.statuscodes, vec![201, 301, 401]); + assert_eq!(config.status_codes, vec![201, 301, 401]); } #[test] @@ -723,9 +729,9 @@ mod tests { #[test] /// parse the test config and see that the value parsed is correct - fn config_reads_norecursion() { + fn config_reads_no_recursion() { let config = setup_config_test(); - assert_eq!(config.norecursion, true); + assert_eq!(config.no_recursion, true); } #[test] @@ -737,16 +743,16 @@ mod tests { #[test] /// parse the test config and see that the value parsed is correct - fn config_reads_dontfilter() { + fn config_reads_dont_filter() { let config = setup_config_test(); - assert_eq!(config.dontfilter, true); + assert_eq!(config.dont_filter, true); } #[test] /// parse the test config and see that the value parsed is correct - fn config_reads_addslash() { + fn config_reads_add_slash() { let config = setup_config_test(); - assert_eq!(config.addslash, true); + assert_eq!(config.add_slash, true); } #[test] @@ -765,9 +771,9 @@ mod tests { #[test] /// parse the test config and see that the value parsed is correct - fn config_reads_sizefilters() { + fn config_reads_filter_size() { let config = setup_config_test(); - assert_eq!(config.sizefilters, vec![4120]); + assert_eq!(config.filter_size, vec![4120]); } #[test] diff --git a/src/heuristics.rs b/src/heuristics.rs index 1ab31bc2..28b8311a 100644 --- a/src/heuristics.rs +++ b/src/heuristics.rs @@ -49,8 +49,8 @@ pub async fn wildcard_test( tx_file ); - if CONFIGURATION.dontfilter { - // early return, dontfilter scans don't need tested + if CONFIGURATION.dont_filter { + // early return, dont_filter scans don't need tested log::trace!("exit: wildcard_test -> None"); return None; } @@ -92,7 +92,7 @@ pub async fn wildcard_test( wildcard.dynamic, style("auto-filtering").yellow(), style(wc_length - url_len).cyan(), - style("--dontfilter").yellow() + style("--dont-filter").yellow() ); ferox_print(&msg, &PROGRESS_PRINTER); @@ -113,7 +113,7 @@ pub async fn wildcard_test( wc_length, style("auto-filtering").yellow(), style(wc_length).cyan(), - style("--dontfilter").yellow() + style("--dont-filter").yellow() ); ferox_print(&msg, &PROGRESS_PRINTER); @@ -160,7 +160,7 @@ async fn make_wildcard_request( let nonexistent = match format_url( target_url, &unique_str, - CONFIGURATION.addslash, + CONFIGURATION.add_slash, &CONFIGURATION.queries, None, ) { @@ -177,7 +177,7 @@ async fn make_wildcard_request( match make_request(&CONFIGURATION.client, &nonexistent.to_owned()).await { Ok(response) => { if CONFIGURATION - .statuscodes + .status_codes .contains(&response.status().as_u16()) { // found a wildcard response @@ -255,7 +255,7 @@ pub async fn connectivity_test(target_urls: &[String]) -> Vec { let request = match format_url( target_url, "", - CONFIGURATION.addslash, + CONFIGURATION.add_slash, &CONFIGURATION.queries, None, ) { diff --git a/src/parser.rs b/src/parser.rs index 0c5ce681..2c7c55c7 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -68,15 +68,15 @@ pub fn initialize() -> App<'static, 'static> { ), ) .arg( - Arg::with_name("statuscodes") + Arg::with_name("status_codes") .short("s") - .long("statuscodes") + .long("status-codes") .value_name("STATUS_CODE") .takes_value(true) .multiple(true) .use_delimiter(true) .help( - "Status Codes of interest (default: 200 204 301 302 307 308 401 403 405)", + "Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 403 405)", ), ) .arg( @@ -87,9 +87,9 @@ pub fn initialize() -> App<'static, 'static> { .help("Only print URLs; Don't print status codes, response size, running config, etc...") ) .arg( - Arg::with_name("dontfilter") + Arg::with_name("dont_filter") .short("D") - .long("dontfilter") + .long("dont-filter") .takes_value(false) .help("Don't auto-filter wildcard responses") ) @@ -102,9 +102,9 @@ pub fn initialize() -> App<'static, 'static> { .takes_value(true), ) .arg( - Arg::with_name("useragent") + Arg::with_name("user_agent") .short("a") - .long("useragent") + .long("user-agent") .value_name("USER_AGENT") .takes_value(true) .help( @@ -162,16 +162,16 @@ pub fn initialize() -> App<'static, 'static> { ), ) .arg( - Arg::with_name("norecursion") + Arg::with_name("no_recursion") .short("n") - .long("norecursion") + .long("no-recursion") .takes_value(false) .help("Do not scan recursively") ) .arg( - Arg::with_name("addslash") + Arg::with_name("add_slash") .short("f") - .long("addslash") + .long("add-slash") .takes_value(false) .conflicts_with("extensions") .help("Append / to each request") @@ -184,9 +184,9 @@ pub fn initialize() -> App<'static, 'static> { .conflicts_with("url") ) .arg( - Arg::with_name("sizefilters") + Arg::with_name("filter_size") .short("S") - .long("sizefilter") + .long("filter-size") .value_name("SIZE") .takes_value(true) .multiple(true) @@ -195,6 +195,18 @@ pub fn initialize() -> App<'static, 'static> { "Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)", ), ) + .arg( + Arg::with_name("status_code_filters") + .short("C") + .long("filter-status") + .value_name("STATUS_CODE") + .takes_value(true) + .multiple(true) + .use_delimiter(true) + .help( + "Filter out status codes (deny list) (ex: -C 200 -S 401)", + ), + ) .arg( Arg::with_name("extract_links") .short("e") @@ -225,7 +237,7 @@ EXAMPLES: ./feroxbuster -u http://127.1 -H Accept:application/json "Authorization: Bearer {token}" IPv6, non-recursive scan with INFO-level logging enabled: - ./feroxbuster -u http://[::1] --norecursion -vv + ./feroxbuster -u http://[::1] --no-recursion -vv Read urls from STDIN; pipe only resulting urls out to another tool cat targets | ./feroxbuster --stdin --quiet -s 200 301 302 --redirects -x js | fff -s 200 -o js-files diff --git a/src/reporter.rs b/src/reporter.rs index 8465ad71..7fc7e789 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -94,7 +94,7 @@ async fn spawn_terminal_reporter( while let Some(resp) = resp_chan.recv().await { log::debug!("received {} on reporting channel", resp.url()); - if CONFIGURATION.statuscodes.contains(&resp.status().as_u16()) { + if CONFIGURATION.status_codes.contains(&resp.status().as_u16()) { let report = if CONFIGURATION.quiet { // -q used, just need the url format!("{}\n", resp.url()) diff --git a/src/scanner.rs b/src/scanner.rs index 6cc229e2..ec71255d 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -178,7 +178,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec if let Ok(url) = format_url( &target_url, &word, - CONFIGURATION.addslash, + CONFIGURATION.add_slash, &CONFIGURATION.queries, None, ) { @@ -189,7 +189,7 @@ fn create_urls(target_url: &str, word: &str, extensions: &[String]) -> Vec if let Ok(url) = format_url( &target_url, &word, - CONFIGURATION.addslash, + CONFIGURATION.add_slash, &CONFIGURATION.queries, Some(ext), ) { @@ -242,6 +242,7 @@ fn response_is_directory(response: &FeroxResponse) -> bool { } } else if response.status().is_success() { // status code is 2xx, need to check if it ends in / + if response.url().as_str().ends_with('/') { log::debug!("{} is directory suitable for recursion", response.url()); log::trace!("exit: is_directory -> true"); @@ -339,17 +340,17 @@ async fn try_recursion( /// to the user or not. pub fn should_filter_response(response: &FeroxResponse) -> bool { if CONFIGURATION - .sizefilters + .filter_size .contains(&response.content_length()) { - // filtered value from --sizefilters, sizefilters and wildcards are two separate filters + // filtered value from --filter-size, size filters and wildcards are two separate filters // and are applied independently log::debug!("size filter: filtered out {}", response.url()); return true; } - if CONFIGURATION.dontfilter { - // quick return if dontfilter is set + if CONFIGURATION.dont_filter { + // quick return if dont_filter is set return false; } @@ -398,7 +399,7 @@ async fn make_requests( let ferox_response = FeroxResponse::from(response, CONFIGURATION.extract_links).await; // do recursion if appropriate - if !CONFIGURATION.norecursion { + if !CONFIGURATION.no_recursion { try_recursion(&ferox_response, base_depth, dir_chan.clone()).await; } @@ -424,7 +425,7 @@ async fn make_requests( let new_url = match format_url( &new_link, &"", - CONFIGURATION.addslash, + CONFIGURATION.add_slash, &CONFIGURATION.queries, None, ) { @@ -459,7 +460,7 @@ async fn make_requests( continue; } - if !CONFIGURATION.norecursion { + if !CONFIGURATION.no_recursion { log::debug!( "Recursive extraction: {} ({})", new_ferox_response.url(), diff --git a/src/utils.rs b/src/utils.rs index 048cfbd6..ead6475f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -140,7 +140,7 @@ pub fn ferox_print(msg: &str, bar: &ProgressBar) { pub fn format_url( url: &str, word: &str, - addslash: bool, + add_slash: bool, queries: &[(String, String)], extension: Option<&str>, ) -> FeroxResult { @@ -148,7 +148,7 @@ pub fn format_url( "enter: format_url({}, {}, {}, {:?} {:?})", url, word, - addslash, + add_slash, queries, extension ); @@ -175,7 +175,7 @@ pub fn format_url( // extensions and slashes are mutually exclusive cases let word = if extension.is_some() { format!("{}.{}", word, extension.unwrap()) - } else if addslash && !word.ends_with('/') { + } else if add_slash && !word.ends_with('/') { // -f used, and word doesn't already end with a / format!("{}/", word) } else { diff --git a/tests/test_banner.rs b/tests/test_banner.rs index 459def50..f8ec1d42 100644 --- a/tests/test_banner.rs +++ b/tests/test_banner.rs @@ -77,14 +77,14 @@ fn banner_prints_headers() -> Result<(), Box> { #[test] /// test allows non-existent wordlist to trigger the banner printing to stderr /// expect to see all mandatory prints + multiple size filters -fn banner_prints_size_filters() -> Result<(), Box> { +fn banner_prints_filter_sizes() -> Result<(), Box> { Command::cargo_bin("feroxbuster") .unwrap() .arg("--url") .arg("http://localhost") .arg("-S") .arg("789456123") - .arg("--sizefilter") + .arg("--filter-size") .arg("44444444") .assert() .failure() @@ -277,13 +277,13 @@ fn banner_prints_extensions() -> Result<(), Box> { #[test] /// test allows non-existent wordlist to trigger the banner printing to stderr -/// expect to see all mandatory prints + dontfilter -fn banner_prints_dontfilter() -> Result<(), Box> { +/// expect to see all mandatory prints + dont_filter +fn banner_prints_dont_filter() -> Result<(), Box> { Command::cargo_bin("feroxbuster") .unwrap() .arg("--url") .arg("http://localhost") - .arg("--dontfilter") + .arg("--dont-filter") .assert() .failure() .stderr( diff --git a/tests/test_extractor.rs b/tests/test_extractor.rs index f88f229d..fb5f7a36 100644 --- a/tests/test_extractor.rs +++ b/tests/test_extractor.rs @@ -181,7 +181,7 @@ fn extractor_finds_same_relative_url_twice() -> Result<(), Box Result<(), Box> { let srv = MockServer::start(); @@ -209,7 +209,7 @@ fn extractor_finds_filtered_content() -> Result<(), Box> .arg("--wordlist") .arg(file.as_os_str()) .arg("--extract-links") - .arg("--sizefilter") + .arg("--filter-size") .arg("18") .unwrap(); diff --git a/tests/test_heuristics.rs b/tests/test_heuristics.rs index 86b7278d..dfb3146a 100644 --- a/tests/test_heuristics.rs +++ b/tests/test_heuristics.rs @@ -115,7 +115,7 @@ fn test_static_wildcard_request_found() -> Result<(), Box .arg(srv.url("/")) .arg("--wordlist") .arg(file.as_os_str()) - .arg("--addslash") + .arg("--add-slash") .unwrap(); teardown_tmp_directory(tmp_dir); @@ -158,7 +158,7 @@ fn test_dynamic_wildcard_request_found() -> Result<(), Box Result<(), Box Result<(), Box> { +/// uses dont_filter, so the normal wildcard test should never happen +fn heuristics_static_wildcard_request_with_dont_filter() -> Result<(), Box> { let srv = MockServer::start(); let (tmp_dir, file) = setup_tmp_directory(&["LICENSE".to_string()], "wordlist")?; @@ -216,7 +216,7 @@ fn heuristics_static_wildcard_request_with_dontfilter() -> Result<(), Box Result<(), Box Date: Thu, 29 Oct 2020 20:22:39 -0500 Subject: [PATCH 06/13] added status code filter option to banner and config --- src/banner.rs | 21 +++++++++++++++++++++ src/config.rs | 30 ++++++++++++++++++++++++++++++ src/parser.rs | 2 +- tests/test_banner.rs | 28 ++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/banner.rs b/src/banner.rs index 87bed6d4..22bcefc0 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -190,6 +190,27 @@ by Ben "epi" Risher {} ver: {}"#, ) .unwrap_or_default(); // 🆗 + if !config.filter_status.is_empty() { + // exception here for optional print due to me wanting the allows and denys to be printed + // one after the other + let mut code_filters = vec![]; + + for code in &config.filter_status { + code_filters.push(status_colorizer(&code.to_string())) + } + + writeln!( + &mut writer, + "{}", + format_banner_entry!( + "\u{1f5d1}", + "Status Code Filters", + format!("[{}]", code_filters.join(", ")) + ) + ) + .unwrap_or_default(); // 🗑 + } + writeln!( &mut writer, "{}", diff --git a/src/config.rs b/src/config.rs index 0993e903..daac55db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -362,6 +362,26 @@ impl Configuration { .collect(); } + 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) + }) + .as_u16() + }) + .collect(); + } + if args.values_of("extensions").is_some() { config.extensions = args .values_of("extensions") @@ -551,6 +571,7 @@ impl Configuration { settings.stdin = settings_to_merge.stdin; settings.depth = settings_to_merge.depth; settings.filter_size = settings_to_merge.filter_size; + settings.filter_status = settings_to_merge.filter_status; settings.dont_filter = settings_to_merge.dont_filter; settings.scan_limit = settings_to_merge.scan_limit; } @@ -608,6 +629,7 @@ mod tests { extract_links = true depth = 1 filter_size = [4120] + filter_status = [201] "#; let tmp_dir = TempDir::new().unwrap(); let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME); @@ -640,6 +662,7 @@ mod tests { assert_eq!(config.queries, Vec::new()); assert_eq!(config.extensions, Vec::::new()); assert_eq!(config.filter_size, Vec::::new()); + assert_eq!(config.filter_status, Vec::::new()); assert_eq!(config.headers, HashMap::new()); } @@ -776,6 +799,13 @@ mod tests { assert_eq!(config.filter_size, vec![4120]); } + #[test] + /// parse the test config and see that the value parsed is correct + fn config_reads_filter_status() { + let config = setup_config_test(); + assert_eq!(config.filter_status, vec![201]); + } + #[test] /// parse the test config and see that the values parsed are correct fn config_reads_headers() { diff --git a/src/parser.rs b/src/parser.rs index 2c7c55c7..2671b59d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -196,7 +196,7 @@ pub fn initialize() -> App<'static, 'static> { ), ) .arg( - Arg::with_name("status_code_filters") + Arg::with_name("filter_status") .short("C") .long("filter-status") .value_name("STATUS_CODE") diff --git a/tests/test_banner.rs b/tests/test_banner.rs index f8ec1d42..ce1ff294 100644 --- a/tests/test_banner.rs +++ b/tests/test_banner.rs @@ -591,3 +591,31 @@ fn banner_prints_scan_limit() -> Result<(), Box> { ); Ok(()) } + +#[test] +/// test allows non-existent wordlist to trigger the banner printing to stderr +/// expect to see all mandatory prints + filter-status +fn banner_prints_filter_status() -> Result<(), Box> { + Command::cargo_bin("feroxbuster") + .unwrap() + .arg("--url") + .arg("http://localhost") + .arg("-C") + .arg("200") + .assert() + .failure() + .stderr( + predicate::str::contains("─┮─") + .and(predicate::str::contains("Target Url")) + .and(predicate::str::contains("http://localhost")) + .and(predicate::str::contains("Threads")) + .and(predicate::str::contains("Wordlist")) + .and(predicate::str::contains("Status Codes")) + .and(predicate::str::contains("Timeout (secs)")) + .and(predicate::str::contains("User-Agent")) + .and(predicate::str::contains("Status Code Filters")) + .and(predicate::str::contains("│ [200]")) + .and(predicate::str::contains("─â”ī─")), + ); + Ok(()) +} From dc89f3b5aabb6166d35e108f0c85c00904c1fc75 Mon Sep 17 00:00:00 2001 From: epi Date: Thu, 29 Oct 2020 20:58:44 -0500 Subject: [PATCH 07/13] implemented deny list --- src/filters.rs | 42 +++++++++++++++++++++++++++++++++++++++++- src/scanner.rs | 30 ++++++++++++++++++++---------- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/filters.rs b/src/filters.rs index 89c4bc28..b97da65d 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -47,6 +47,7 @@ pub struct WildcardFilter { pub size: u64, } +/// implementation of FeroxFilter for WildcardFilter impl FeroxFilter for WildcardFilter { /// Examine size, dynamic, and content_len to determine whether or not the response received /// is a wildcard response and therefore should be filtered out @@ -86,7 +87,46 @@ impl FeroxFilter for WildcardFilter { other.downcast_ref::().map_or(false, |a| self == a) } - /// Return seld as Any for dynamic dispatch purposes + /// Return self as Any for dynamic dispatch purposes + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Simple implementor of FeroxFilter; used to filter out status codes specified using +/// -C|--filter-status +#[derive(Default, Debug, PartialEq)] +pub struct StatusCodeFilter { + /// Status code that should not be displayed to the user + pub filter_code: u16, +} + +/// implementation of FeroxFilter for StatusCodeFilter +impl FeroxFilter for StatusCodeFilter { + /// Check `filter_code` against what was passed in via -C|--filter-status + fn should_filter_response(&self, response: &FeroxResponse) -> bool { + log::trace!("enter: should_filter_response({:?} {:?})", self, response); + + if response.status().as_u16() == self.filter_code { + log::debug!( + "filtered out {} based on --filter-status of {}", + response.url(), + self.filter_code + ); + log::trace!("exit: should_filter_response -> true"); + return true; + } + + log::trace!("exit: should_filter_response -> false"); + false + } + + /// Compare one StatusCodeFilter to another + fn box_eq(&self, other: &dyn Any) -> bool { + other.downcast_ref::().map_or(false, |a| self == a) + } + + /// Return self as Any for dynamic dispatch purposes fn as_any(&self) -> &dyn Any { self } diff --git a/src/scanner.rs b/src/scanner.rs index ec71255d..d2f7f958 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -1,6 +1,6 @@ use crate::config::{CONFIGURATION, PROGRESS_BAR}; use crate::extractor::get_links; -use crate::filters::{FeroxFilter, WildcardFilter}; +use crate::filters::{FeroxFilter, StatusCodeFilter, WildcardFilter}; use crate::utils::{format_url, get_current_depth, make_request}; use crate::{heuristics, progress, FeroxChannel, FeroxResponse}; use futures::future::{BoxFuture, FutureExt}; @@ -70,34 +70,34 @@ fn add_url_to_list_of_scanned_urls(resp: &str, scanned_urls: &RwLock, - wildcard_filters: Arc>>>, + ferox_filters: Arc>>>, ) -> bool { log::trace!( - "enter: add_filter_to_list_of_wildcard_filters({:?}, {:?})", + "enter: add_filter_to_list_of_ferox_filters({:?}, {:?})", filter, - wildcard_filters + ferox_filters ); - match wildcard_filters.write() { + match ferox_filters.write() { Ok(mut filters) => { // If the set did not contain the assigned filter, true is returned. // If the set did contain the assigned filter, false is returned. if filters.contains(&filter) { - log::trace!("exit: add_filter_to_list_of_wildcard_filters -> false"); + log::trace!("exit: add_filter_to_list_of_ferox_filters -> false"); return false; } filters.push(filter); - log::trace!("exit: add_filter_to_list_of_wildcard_filters -> true"); + log::trace!("exit: add_filter_to_list_of_ferox_filters -> true"); true } Err(e) => { // poisoned lock log::error!("Set of wildcard filters poisoned: {}", e); - log::trace!("exit: add_filter_to_list_of_wildcard_filters -> false"); + log::trace!("exit: add_filter_to_list_of_ferox_filters -> false"); false } } @@ -578,13 +578,23 @@ pub async fn scan_url( .await }); + // add any wildcard filters to `FILTERS` let filter = match heuristics::wildcard_test(&target_url, wildcard_bar, heuristics_file_clone).await { Some(f) => Box::new(f), None => Box::new(WildcardFilter::default()), }; - add_filter_to_list_of_wildcard_filters(filter, FILTERS.clone()); + add_filter_to_list_of_ferox_filters(filter, FILTERS.clone()); + + // add any status code filters to `FILTERS` + for code_filter in &CONFIGURATION.filter_status { + let filter = StatusCodeFilter { + filter_code: *code_filter, + }; + let boxed_filter = Box::new(filter); + add_filter_to_list_of_ferox_filters(boxed_filter, FILTERS.clone()); + } // producer tasks (mp of mpsc); responsible for making requests let producers = stream::iter(looping_words.deref().to_owned()) From 6fe5ae0d0c530d1347ea3d747b6096c2ead38031 Mon Sep 17 00:00:00 2001 From: epi Date: Fri, 30 Oct 2020 05:19:38 -0500 Subject: [PATCH 08/13] added integration test for status code filter --- tests/test_filters.rs | 57 +++++++++++++++++++++++++++++++++++++++++++ tests/test_scanner.rs | 5 ++-- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/test_filters.rs diff --git a/tests/test_filters.rs b/tests/test_filters.rs new file mode 100644 index 00000000..c9de7978 --- /dev/null +++ b/tests/test_filters.rs @@ -0,0 +1,57 @@ +mod utils; +use assert_cmd::prelude::*; +use httpmock::Method::GET; +use httpmock::{Mock, MockServer}; +use predicates::prelude::*; +use std::process::Command; +use utils::{setup_tmp_directory, teardown_tmp_directory}; + +#[test] +/// create a FeroxResponse that should elicit a true from +/// StatusCodeFilter::should_filter_response +fn filters_status_code_should_filter_response() { + let srv = MockServer::start(); + let (tmp_dir, file) = + setup_tmp_directory(&["LICENSE".to_string(), "file.js".to_string()], "wordlist").unwrap(); + + let mock = Mock::new() + .expect_method(GET) + .expect_path("/LICENSE") + .return_status(302) + .return_body("this is a test") + .create_on(&srv); + + let mock_two = Mock::new() + .expect_method(GET) + .expect_path("/file.js") + .return_status(200) + .return_body("this is also a test of some import") + .create_on(&srv); + + let cmd = Command::cargo_bin("feroxbuster") + .unwrap() + .arg("--url") + .arg(srv.url("/")) + .arg("--wordlist") + .arg(file.as_os_str()) + .arg("-vvvv") + .arg("--filter-status") + .arg("302") + .unwrap(); + + cmd.assert().success().stdout( + predicate::str::contains("/LICENSE") + .not() + .and(predicate::str::contains("302")) + .not() + .and(predicate::str::contains("14")) + .not() + .and(predicate::str::contains("/file.js")) + .and(predicate::str::contains("200")) + .and(predicate::str::contains("34")), + ); + + assert_eq!(mock.times_called(), 1); + assert_eq!(mock_two.times_called(), 1); + teardown_tmp_directory(tmp_dir); +} diff --git a/tests/test_scanner.rs b/tests/test_scanner.rs index cd10bdfb..87af42f5 100644 --- a/tests/test_scanner.rs +++ b/tests/test_scanner.rs @@ -336,6 +336,7 @@ fn scanner_single_request_returns_301_without_location_header( let mock = Mock::new() .expect_method(GET) .expect_path("/LICENSE") + .return_body("this is a test") .return_status(301) .create_on(&srv); @@ -345,9 +346,9 @@ fn scanner_single_request_returns_301_without_location_header( .arg(srv.url("/")) .arg("--wordlist") .arg(file.as_os_str()) - .arg("-T") + .arg("--timeout") .arg("5") - .arg("-a") + .arg("--user-agent") .arg("some-user-agent-string") .unwrap(); From e35f86876d8a1ef03728039aadd68cd239bfdc4d Mon Sep 17 00:00:00 2001 From: epi Date: Fri, 30 Oct 2020 05:26:16 -0500 Subject: [PATCH 09/13] fixed oddly failing tests /shrug --- tests/test_scanner.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_scanner.rs b/tests/test_scanner.rs index 87af42f5..08559d4a 100644 --- a/tests/test_scanner.rs +++ b/tests/test_scanner.rs @@ -327,7 +327,8 @@ fn scanner_single_request_quiet_scan() -> Result<(), Box> } #[test] -/// send single valid request, get back a 301 without a Location header, expect false +/// send single valid request, get back a 301 without a Location header +/// expect response_is_directory to return false when called fn scanner_single_request_returns_301_without_location_header( ) -> Result<(), Box> { let srv = MockServer::start(); @@ -355,8 +356,7 @@ fn scanner_single_request_returns_301_without_location_header( cmd.assert().success().stdout( predicate::str::contains(srv.url("/LICENSE")) .and(predicate::str::contains("301")) - .and(predicate::str::contains("14")) - .not(), + .and(predicate::str::contains("14")), ); assert_eq!(mock.times_called(), 1); From 6e981e6d3a6fd5f12ebdc0dfcd17c6a63b3c6387 Mon Sep 17 00:00:00 2001 From: epi Date: Fri, 30 Oct 2020 05:58:17 -0500 Subject: [PATCH 10/13] added whitespace around response size; server port number can clash with size --- tests/test_scanner.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_scanner.rs b/tests/test_scanner.rs index 08559d4a..988aff54 100644 --- a/tests/test_scanner.rs +++ b/tests/test_scanner.rs @@ -402,7 +402,7 @@ fn scanner_single_request_scan_with_filtered_result() -> Result<(), Box Date: Fri, 30 Oct 2020 07:17:10 -0500 Subject: [PATCH 11/13] updated readme to reflect 1.3.0 changes --- README.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2c15bf14..db8716d6 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ This attack is also known as Predictable Resource Location, File Enumeration, Di - [Proxy traffic through a SOCKS proxy](#proxy-traffic-through-a-socks-proxy) - [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) - [Comparison w/ Similar Tools](#-comparison-w-similar-tools) - [Common Problems/Issues (FAQ)](#-common-problemsissues-faq) - [No file descriptors available](#no-file-descriptors-available) @@ -273,6 +274,7 @@ 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] # threads = 1 # timeout = 5 # proxy = "http://127.0.0.1:8080" @@ -315,13 +317,13 @@ USAGE: feroxbuster [FLAGS] [OPTIONS] --url ... FLAGS: - -f, --add-slash Append / to each request - -D, --dont-filter Don't auto-filter wildcard responses + -f, --add-slash Append / to each request + -D, --dont-filter Don't auto-filter wildcard responses -e, --extract-links Extract links from response body (html, javascript, etc...); make new requests based on findings (default: false) -h, --help Prints help information -k, --insecure Disables TLS certificate validation - -n, --no-recursion Do not scan recursively + -n, --no-recursion Do not scan recursively -q, --quiet Only print URLs; Don't print status codes, response size, running config, etc... -r, --redirects Follow redirects --stdin Read url(s) from STDIN @@ -331,17 +333,19 @@ FLAGS: OPTIONS: -d, --depth Maximum recursion depth, a depth of 0 is infinite recursion (default: 4) -x, --extensions ... File extension(s) to search for (ex: -x php -x pdf js) + -S, --filter-size ... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970) + -C, --filter-status ... Filter out status codes (deny list) (ex: -C 200 -S 401) -H, --headers
... Specify HTTP headers (ex: -H Header:val 'stuff: things') -o, --output Output file to write results to (default: stdout) -p, --proxy Proxy to use for requests (ex: http(s)://host:port, socks5://host:port) -Q, --query ... Specify URL query parameters (ex: -Q token=stuff -Q secret=key) - -L, --scan-limit Limit total number of concurrent scans (default: 7) - -S, --filter-size ... Filter out messages of a particular size (ex: -S 5120 -S 4927,1970) - -s, --status-codes ... Status Codes of interest (default: 200 204 301 302 307 308 401 403 405) + -L, --scan-limit Limit total number of concurrent scans (default: 0, i.e. no limit) + -s, --status-codes ... Status Codes to include (allow list) (default: 200 204 301 302 307 308 401 + 403 405) -t, --threads Number of concurrent threads (default: 50) -T, --timeout Number of seconds before a request times out (default: 7) -u, --url ... The target URL(s) (required, unless --stdin used) - -a, --user-agent Sets the User-Agent (default: feroxbuster/VERSION) + -a, --user-agent Sets the User-Agent (default: feroxbuster/VERSION) -w, --wordlist Path to the wordlist ``` @@ -436,6 +440,16 @@ discovered directories can only begin scanning when the total number of active s ./feroxbuster -u http://127.1 --scan-limit 2 ``` +### Filter Response by Status Code (new in `v1.3.0`) + +Version 1.3.0 included an overhaul to the filtering system which will allow for a wide array of filters to be added +with minimal effort. The first such filter is a Status Code Filter. As responses come back from the scanned server, +each one is checked against a list of known filters and either displayed or not according to which filters are set. + +``` +./feroxbuster -u http://127.1 --filter-status 301 +``` + ![limit-demo](img/limit-demo.gif) ## 🧐 Comparison w/ Similar Tools @@ -460,7 +474,7 @@ a few of the use-cases in which feroxbuster may be a better fit: |------------------------------------------------------------------|---|---|---| | fast | ✔ | ✔ | ✔ | | easy to use | ✔ | ✔ | | -| blacklist status codes (in addition to whitelist) | | ✔ | ✔ | +| filter out responses by status code (new in `v1.3.0`) | ✔ | ✔ | ✔ | | allows recursion | ✔ | | ✔ | | can specify query parameters | ✔ | | ✔ | | SOCKS proxy support | ✔ | | | From db5e1e2e2d2d90fd687215c1e2afc9061e76c4da Mon Sep 17 00:00:00 2001 From: epi Date: Fri, 30 Oct 2020 07:18:48 -0500 Subject: [PATCH 12/13] gif was out of place --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index db8716d6..14d0c9c7 100644 --- a/README.md +++ b/README.md @@ -440,6 +440,8 @@ discovered directories can only begin scanning when the total number of active s ./feroxbuster -u http://127.1 --scan-limit 2 ``` +![limit-demo](img/limit-demo.gif) + ### Filter Response by Status Code (new in `v1.3.0`) Version 1.3.0 included an overhaul to the filtering system which will allow for a wide array of filters to be added @@ -450,8 +452,6 @@ each one is checked against a list of known filters and either displayed or not ./feroxbuster -u http://127.1 --filter-status 301 ``` -![limit-demo](img/limit-demo.gif) - ## 🧐 Comparison w/ Similar Tools There are quite a few similar tools for forced browsing/content discovery. Burp Suite Pro, Dirb, Dirbuster, etc... From f64f02135ebb9766b9b0b03b72aa7d702acd8103 Mon Sep 17 00:00:00 2001 From: epi Date: Sat, 31 Oct 2020 06:54:19 -0500 Subject: [PATCH 13/13] moved dont_filter from scanner to WildcardFilter --- src/filters.rs | 9 +++++++++ src/scanner.rs | 5 ----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/filters.rs b/src/filters.rs index b97da65d..12442156 100644 --- a/src/filters.rs +++ b/src/filters.rs @@ -1,3 +1,4 @@ +use crate::config::CONFIGURATION; use crate::utils::get_url_path_length; use crate::FeroxResponse; use std::any::Any; @@ -54,6 +55,14 @@ impl FeroxFilter for WildcardFilter { fn should_filter_response(&self, response: &FeroxResponse) -> bool { log::trace!("enter: should_filter_response({:?} {:?})", self, response); + // quick return if dont_filter is set + if CONFIGURATION.dont_filter { + // --dont-filter applies specifically to wildcard filters, it is not a 100% catch all + // for not filtering anything. As such, it should live in the implementation of + // a wildcard filter + return false; + } + if self.size > 0 && self.size == response.content_length() { // static wildcard size found during testing // size isn't default, size equals response length, and auto-filter is on diff --git a/src/scanner.rs b/src/scanner.rs index d2f7f958..c56a5ec8 100644 --- a/src/scanner.rs +++ b/src/scanner.rs @@ -349,11 +349,6 @@ pub fn should_filter_response(response: &FeroxResponse) -> bool { return true; } - if CONFIGURATION.dont_filter { - // quick return if dont_filter is set - return false; - } - match FILTERS.read() { Ok(filters) => { for filter in filters.iter() {