diff --git a/Cargo.lock b/Cargo.lock index 977b5c0..be63ad4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "feroxbuster" -version = "2.8.0" +version = "2.8.1" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 0a4c9fd..6455a1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "feroxbuster" -version = "2.8.0" +version = "2.8.1" authors = ["Ben 'epi' Risher (@epi052)"] license = "MIT" edition = "2021" diff --git a/shell_completions/_feroxbuster b/shell_completions/_feroxbuster index 7049945..fb86f81 100644 --- a/shell_completions/_feroxbuster +++ b/shell_completions/_feroxbuster @@ -24,8 +24,8 @@ _feroxbuster() { '--replay-proxy=[Send only unfiltered requests through a Replay Proxy, instead of all requests]:REPLAY_PROXY:_urls' \ '*-R+[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ '*--replay-codes=[Status Codes to send through a Replay Proxy when found (default: --status-codes value)]:REPLAY_CODE: ' \ -'-a+[Sets the User-Agent (default: feroxbuster/2.8.0)]:USER_AGENT: ' \ -'--user-agent=[Sets the User-Agent (default: feroxbuster/2.8.0)]:USER_AGENT: ' \ +'-a+[Sets the User-Agent (default: feroxbuster/2.8.1)]:USER_AGENT: ' \ +'--user-agent=[Sets the User-Agent (default: feroxbuster/2.8.1)]:USER_AGENT: ' \ '*-x+[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*--extensions=[File extension(s) to search for (ex: -x php -x pdf js)]:FILE_EXTENSION: ' \ '*-m+[Which HTTP request method(s) should be sent (default: GET)]:HTTP_METHODS: ' \ diff --git a/shell_completions/_feroxbuster.ps1 b/shell_completions/_feroxbuster.ps1 index 9c52c88..f461d70 100644 --- a/shell_completions/_feroxbuster.ps1 +++ b/shell_completions/_feroxbuster.ps1 @@ -30,8 +30,8 @@ Register-ArgumentCompleter -Native -CommandName 'feroxbuster' -ScriptBlock { [CompletionResult]::new('--replay-proxy', 'replay-proxy', [CompletionResultType]::ParameterName, 'Send only unfiltered requests through a Replay Proxy, instead of all requests') [CompletionResult]::new('-R', 'R', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') [CompletionResult]::new('--replay-codes', 'replay-codes', [CompletionResultType]::ParameterName, 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)') - [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.8.0)') - [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.8.0)') + [CompletionResult]::new('-a', 'a', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.8.1)') + [CompletionResult]::new('--user-agent', 'user-agent', [CompletionResultType]::ParameterName, 'Sets the User-Agent (default: feroxbuster/2.8.1)') [CompletionResult]::new('-x', 'x', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('--extensions', 'extensions', [CompletionResultType]::ParameterName, 'File extension(s) to search for (ex: -x php -x pdf js)') [CompletionResult]::new('-m', 'm', [CompletionResultType]::ParameterName, 'Which HTTP request method(s) should be sent (default: GET)') diff --git a/shell_completions/feroxbuster.elv b/shell_completions/feroxbuster.elv index 7d67ebb..e569124 100644 --- a/shell_completions/feroxbuster.elv +++ b/shell_completions/feroxbuster.elv @@ -27,8 +27,8 @@ set edit:completion:arg-completer[feroxbuster] = {|@words| cand --replay-proxy 'Send only unfiltered requests through a Replay Proxy, instead of all requests' cand -R 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' cand --replay-codes 'Status Codes to send through a Replay Proxy when found (default: --status-codes value)' - cand -a 'Sets the User-Agent (default: feroxbuster/2.8.0)' - cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.8.0)' + cand -a 'Sets the User-Agent (default: feroxbuster/2.8.1)' + cand --user-agent 'Sets the User-Agent (default: feroxbuster/2.8.1)' cand -x 'File extension(s) to search for (ex: -x php -x pdf js)' cand --extensions 'File extension(s) to search for (ex: -x php -x pdf js)' cand -m 'Which HTTP request method(s) should be sent (default: GET)' diff --git a/src/event_handlers/outputs.rs b/src/event_handlers/outputs.rs index 0c58b6e..e6f19ef 100644 --- a/src/event_handlers/outputs.rs +++ b/src/event_handlers/outputs.rs @@ -248,7 +248,7 @@ impl TermOutHandler { .unwrap() .filters .data - .should_filter_response(&resp); + .should_filter_response(&resp, tx_stats.clone()); let contains_sentry = if !self.config.filter_status.is_empty() { // -C was used, meaning -s was not and we should ignore the defaults diff --git a/src/event_handlers/scans.rs b/src/event_handlers/scans.rs index 8329c20..9b18fe4 100644 --- a/src/event_handlers/scans.rs +++ b/src/event_handlers/scans.rs @@ -6,7 +6,7 @@ use tokio::sync::{mpsc, Semaphore}; use crate::{ response::FeroxResponse, scan_manager::{FeroxScan, FeroxScans, ScanOrder}, - scanner::FeroxScanner, + scanner::{FeroxScanner, RESPONSES}, statistics::StatField::TotalScans, url::FeroxUrl, utils::should_deny_url, @@ -395,6 +395,58 @@ impl ScanHandler { return Ok(()); } + if let Ok(responses) = RESPONSES.responses.read() { + for maybe_wild in responses.iter() { + if !maybe_wild.wildcard() || !maybe_wild.is_directory() { + // if the stored response isn't a wildcard, skip it + // if the stored response isn't a directory, skip it + // we're only interested in preventing recursion into wildcard directories + continue; + } + + if maybe_wild.method() != response.method() { + // methods don't match, skip it + continue; + } + + // methods match and is a directory wildcard + // need to check the wildcard's parent directory + // for equality with the incoming response's parent directory + // + // if the parent directories match, we need to prevent recursion + // into the wildcard directory + + match ( + maybe_wild.url().path_segments(), + response.url().path_segments(), + ) { + // both urls must have path segments + (Some(mut maybe_wild_segments), Some(mut response_segments)) => { + match ( + maybe_wild_segments.nth_back(1), + response_segments.nth_back(1), + ) { + // both urls must have at least 2 path segments, the next to last being the parent + (Some(maybe_wild_parent), Some(response_parent)) => { + if maybe_wild_parent == response_parent { + // the parent directories match, so we need to prevent recursion + return Ok(()); + } + } + _ => { + // we couldn't get the parent directory, so we'll skip this + continue; + } + } + } + _ => { + // we couldn't get the path segments, so we'll skip this + continue; + } + } + } + } + let targets = vec![response.url().to_string()]; self.ordered_scan_url(targets, ScanOrder::Latest).await?; diff --git a/src/extractor/container.rs b/src/extractor/container.rs index e7f5e1f..291bd2e 100644 --- a/src/extractor/container.rs +++ b/src/extractor/container.rs @@ -144,7 +144,7 @@ impl<'a> Extractor<'a> { }; // filter if necessary - if self.handles.filters.data.should_filter_response(&resp) { + if self.handles.filters.data.should_filter_response(&resp, self.handles.stats.tx.clone()) { continue; } diff --git a/src/filters/container.rs b/src/filters/container.rs index 7788bf8..f28e16d 100644 --- a/src/filters/container.rs +++ b/src/filters/container.rs @@ -7,9 +7,12 @@ use crate::response::FeroxResponse; use super::{ FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, - WordsFilter, + WildcardFilter, WordsFilter, +}; +use crate::{ + event_handlers::Command::AddToUsizeField, statistics::StatField::WildcardsFiltered, + CommandSender, }; - /// Container around a collection of `FeroxFilters`s #[derive(Debug, Default)] pub struct FeroxFilters { @@ -64,12 +67,21 @@ impl FeroxFilters { /// 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(&self, response: &FeroxResponse) -> bool { + pub fn should_filter_response( + &self, + response: &FeroxResponse, + tx_stats: CommandSender, + ) -> bool { if let Ok(filters) = self.filters.read() { for filter in filters.iter() { // wildcard.should_filter goes here if filter.should_filter_response(response) { log::debug!("filtering response due to: {:?}", filter); + if filter.as_any().downcast_ref::().is_some() { + tx_stats + .send(AddToUsizeField(WildcardsFiltered, 1)) + .unwrap_or_default(); + } return true; } } @@ -93,6 +105,10 @@ impl Serialize for FeroxFilters { seq.serialize_element(word_filter).unwrap_or_default(); } else if let Some(size_filter) = filter.as_any().downcast_ref::() { seq.serialize_element(size_filter).unwrap_or_default(); + } else if let Some(wildcard_filter) = + filter.as_any().downcast_ref::() + { + seq.serialize_element(wildcard_filter).unwrap_or_default(); } else if let Some(status_filter) = filter.as_any().downcast_ref::() { diff --git a/src/filters/mod.rs b/src/filters/mod.rs index 44fa127..d6582d0 100644 --- a/src/filters/mod.rs +++ b/src/filters/mod.rs @@ -15,6 +15,7 @@ pub use self::similarity::{SimilarityFilter, SIM_HASHER}; pub use self::size::SizeFilter; pub use self::status_code::StatusCodeFilter; pub(crate) use self::utils::{create_similarity_filter, filter_lookup}; +pub use self::wildcard::WildcardFilter; pub use self::words::WordsFilter; mod status_code; @@ -28,4 +29,5 @@ mod container; mod tests; mod init; mod utils; +mod wildcard; mod empty; diff --git a/src/filters/wildcard.rs b/src/filters/wildcard.rs new file mode 100644 index 0000000..7a54d16 --- /dev/null +++ b/src/filters/wildcard.rs @@ -0,0 +1,180 @@ +use console::style; + +use super::*; +use crate::utils::create_report_string; +use crate::{config::OutputLevel, DEFAULT_METHOD}; + +/// Data holder for all relevant data needed when auto-filtering out wildcard responses +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct WildcardFilter { + /// The content-length of this response, if known + pub content_length: Option, + + /// The number of lines contained in the body of this response, if known + pub line_count: Option, + + /// The number of words contained in the body of this response, if known + pub word_count: Option, + + /// method used in request that should be included with filters passed via runtime configuration + pub method: String, + + /// the status code returned in the response + pub status_code: u16, + + /// whether or not the user passed -D on the command line + pub dont_filter: bool, +} + +/// implementation of WildcardFilter +impl WildcardFilter { + /// given a boolean representing whether -D was used or not, create a new WildcardFilter + pub fn new(dont_filter: bool) -> Self { + Self { + dont_filter, + ..Default::default() + } + } +} + +/// implement default that populates `method` with its default value +impl Default for WildcardFilter { + fn default() -> Self { + Self { + content_length: None, + line_count: None, + word_count: None, + method: DEFAULT_METHOD.to_string(), + status_code: 0, + dont_filter: false, + } + } +} + +/// implementation of FeroxFilter for WildcardFilter +impl FeroxFilter for WildcardFilter { + /// Examine size/words/lines and method 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); + + // quick return if dont_filter is set + if self.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.method != response.method().as_str() { + // method's don't match, so this response should not be filtered out + log::trace!("exit: should_filter_response -> false"); + return false; + } + + if self.status_code != response.status().as_u16() { + // status codes don't match, so this response should not be filtered out + log::trace!("exit: should_filter_response -> false"); + return false; + } + + // methods and status codes match at this point, just need to check the other fields + + match (self.content_length, self.word_count, self.line_count) { + (Some(cl), Some(wc), Some(lc)) => { + if cl == response.content_length() + && wc == response.word_count() + && lc == response.line_count() + { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (Some(cl), Some(wc), None) => { + if cl == response.content_length() && wc == response.word_count() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (Some(cl), None, Some(lc)) => { + if cl == response.content_length() && lc == response.line_count() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (None, Some(wc), Some(lc)) => { + if wc == response.word_count() && lc == response.line_count() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (Some(cl), None, None) => { + if cl == response.content_length() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (None, Some(wc), None) => { + if wc == response.word_count() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (None, None, Some(lc)) => { + if lc == response.line_count() { + log::debug!("filtered out {}", response.url()); + log::trace!("exit: should_filter_response -> true"); + return true; + } + } + (None, None, None) => { + unreachable!("wildcard filter without any filters set"); + } + } + + 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 self as Any for dynamic dispatch purposes + fn as_any(&self) -> &dyn Any { + self + } +} + +impl std::fmt::Display for WildcardFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let msg = create_report_string( + self.status_code.to_string().as_str(), + self.method.as_str(), + &self + .line_count + .map_or_else(|| "-".to_string(), |x| x.to_string()), + &self + .word_count + .map_or_else(|| "-".to_string(), |x| x.to_string()), + &self + .content_length + .map_or_else(|| "-".to_string(), |x| x.to_string()), + &format!( + "{} found {}-like response and created new filter; toggle off with {}", + style("Auto-filtering").bright().green(), + style("404").red(), + style("--dont-filter").yellow() + ), + OutputLevel::Default, + ); + write!(f, "{}", msg) + } +} diff --git a/src/heuristics.rs b/src/heuristics.rs index 66d6275..ee23c47 100644 --- a/src/heuristics.rs +++ b/src/heuristics.rs @@ -1,22 +1,21 @@ use std::sync::Arc; use anyhow::{bail, Result}; -use console::style; use scraper::{Html, Selector}; use uuid::Uuid; -use crate::filters::{SimilarityFilter, SIM_HASHER}; +use crate::filters::{SimilarityFilter, WildcardFilter, SIM_HASHER}; use crate::message::FeroxMessage; use crate::nlp::preprocess; +use crate::scanner::RESPONSES; use crate::{ config::OutputLevel, event_handlers::{Command, Handles}, - filters::{LinesFilter, SizeFilter, WordsFilter}, progress::PROGRESS_PRINTER, response::FeroxResponse, skip_fail, url::FeroxUrl, - utils::{ferox_print, fmt_err, logged_request, status_colorizer}, + utils::{ferox_print, fmt_err, logged_request}, DEFAULT_METHOD, }; @@ -50,6 +49,16 @@ pub struct DirListingResult { pub response: FeroxResponse, } +/// wrapper around the results of running a wildcard detection against a target web page +#[derive(Copy, Debug, Clone)] +pub enum WildcardResult { + /// variant that represents a wildcard directory + WildcardDirectory(usize), + + /// variant that represents the presence of a 404-like response + FourOhFourLike(usize), +} + /// container for heuristics related info pub struct HeuristicTests { /// Handles object for event handler interaction @@ -240,13 +249,16 @@ impl HeuristicTests { /// given a target's base url, attempt to automatically detect its 404 response /// pattern(s), and then set filters that will exclude those patterns from future /// responses - pub async fn detect_404_like_responses(&self, target_url: &str) -> Result { + pub async fn detect_404_like_responses( + &self, + target_url: &str, + ) -> Result> { log::trace!("enter: detect_404_like_responses({:?})", target_url); if self.handles.config.dont_filter { // early return, dont_filter scans don't need tested log::trace!("exit: detect_404_like_responses -> dont_filter is true"); - return Ok(0); + return Ok(None); } let mut req_counter = 0; @@ -323,28 +335,41 @@ impl HeuristicTests { } // Command::AddFilter, &str (bytes/words/lines), usize (i.e. length associated with the type) - let Some((command, filter_type, filter_length)) = self.examine_404_like_responses(&responses) else { + let Some(filter) = self.examine_404_like_responses(&responses) else { // no match was found during analysis of responses responses.clear(); continue; }; - // check whether we already know about this filter - match command { - Command::AddFilter(ref filter) => { - if let Ok(guard) = self.handles.filters.data.filters.read() { - if guard.contains(filter) { - // match was found, but already known; clear the vec and continue to the next - responses.clear(); - continue; + // report to the user, if appropriate + if matches!( + self.handles.config.output_level, + OutputLevel::Default | OutputLevel::Quiet + ) { + // sentry value to control whether or not to print the filter + // used because we only want to print the same filter once + let mut print_sentry = true; + + if let Ok(filters) = self.handles.filters.data.filters.read() { + for other in filters.iter() { + if let Some(other_wildcard) = + other.as_any().downcast_ref::() + { + if &*filter == other_wildcard { + print_sentry = false; + break; + } } } } - _ => unreachable!(), + + if print_sentry { + ferox_print(&format!("{}", filter), &PROGRESS_PRINTER); + } } // create the new filter - self.handles.filters.send(command)?; + self.handles.filters.send(Command::AddFilter(filter))?; // if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line // @@ -360,22 +385,35 @@ impl HeuristicTests { .filters .send(Command::AddFilter(Box::new(sim_filter)))?; + if responses[0].is_directory() { + // response is either a 3XX with a Location header that matches url + '/' + // or it's a 2XX that ends with a '/' + // or it's a 403 that ends with a '/' + + // set the wildcard flag to true, so we can check it when preventing + // recursion in event_handlers/scans.rs + responses[0].set_wildcard(true); + + // add the response to the global list of responses + RESPONSES.insert(responses[0].clone()); + + // function-internal magic number, indicates that we've detected a wildcard directory + req_counter += 100; + } + // reset the responses for the next method, if it exists responses.clear(); - - // report to the user, if appropriate - if matches!( - self.handles.config.output_level, - OutputLevel::Default | OutputLevel::Quiet - ) { - let msg = format!("{} {:>8} {:>9} {:>9} {:>9} {} => {} {}-like response ({} {}); toggle this behavior by using {}\n", status_colorizer("WLD"), "-", "-", "-", "-", style(target_url).cyan(), style("auto-filtering").bright().green(), style("404").red(), style(filter_length).cyan(), filter_type, style("--dont-filter").yellow()); - ferox_print(&msg, &PROGRESS_PRINTER); - } } log::trace!("exit: detect_404_like_responses"); - Ok(req_counter) + let retval = if req_counter > 100 { + WildcardResult::WildcardDirectory(req_counter) + } else { + WildcardResult::FourOhFourLike(req_counter) + }; + + Ok(Some(retval)) } /// for all responses, examine chars/words/lines @@ -386,11 +424,13 @@ impl HeuristicTests { fn examine_404_like_responses( &self, responses: &[FeroxResponse], - ) -> Option<(Command, &'static str, usize)> { + ) -> Option> { let mut size_sentry = true; let mut word_sentry = true; let mut line_sentry = true; + let method = responses[0].method(); + let status_code = responses[0].status(); let content_length = responses[0].content_length(); let word_count = responses[0].word_count(); let line_count = responses[0].line_count(); @@ -411,36 +451,61 @@ impl HeuristicTests { } } - // the if/else-if/else nature of the block means that we'll get the most - // specific match, if one is to be had - // - // each block returns the information needed to send the filter away and - // display a message to the user - if size_sentry { - // - command to send to the filters handler - // - the unit-type we're filtering on (bytes/words/lines) - // - the value associated with the unit-type on which we're filtering - Some(( - Command::AddFilter(Box::new(SizeFilter { content_length })), - "bytes", - content_length as usize, - )) - } else if word_sentry { - Some(( - Command::AddFilter(Box::new(WordsFilter { word_count })), - "words", - word_count, - )) - } else if line_sentry { - Some(( - Command::AddFilter(Box::new(LinesFilter { line_count })), - "lines", - line_count, - )) - } else { - // no match was found; clear the vec and continue to the next - None + if !size_sentry && !word_sentry && !line_sentry { + // none of the response lengths match, so we can't filter on any of them + return None; } + + let mut wildcard = WildcardFilter { + content_length: None, + line_count: None, + word_count: None, + method: method.to_string(), + status_code: status_code.as_u16(), + dont_filter: self.handles.config.dont_filter, + }; + + match (size_sentry, word_sentry, line_sentry) { + (true, true, true) => { + // all three types of length match, so we can't filter on any of them + wildcard.content_length = Some(content_length); + wildcard.word_count = Some(word_count); + wildcard.line_count = Some(line_count); + } + (true, true, false) => { + // content length and word count match, so we can filter on either + wildcard.content_length = Some(content_length); + wildcard.word_count = Some(word_count); + } + (true, false, true) => { + // content length and line count match, so we can filter on either + wildcard.content_length = Some(content_length); + wildcard.line_count = Some(line_count); + } + (false, true, true) => { + // word count and line count match, so we can filter on either + wildcard.word_count = Some(word_count); + wildcard.line_count = Some(line_count); + } + (true, false, false) => { + // content length matches, so we can filter on that + wildcard.content_length = Some(content_length); + } + (false, true, false) => { + // word count matches, so we can filter on that + wildcard.word_count = Some(word_count); + } + (false, false, true) => { + // line count matches, so we can filter on that + wildcard.line_count = Some(line_count); + } + (false, false, false) => { + // none of the length types match, so we can't filter on any of them + unreachable!("no wildcard size matches; handled by the if statement above"); + } + }; + + Some(Box::new(wildcard)) } } diff --git a/src/scan_manager/scan_container.rs b/src/scan_manager/scan_container.rs index 290e22c..d3118bb 100644 --- a/src/scan_manager/scan_container.rs +++ b/src/scan_manager/scan_container.rs @@ -3,7 +3,7 @@ use super::*; use crate::event_handlers::Handles; use crate::filters::{ EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, - WordsFilter, + WildcardFilter, WordsFilter, }; use crate::traits::FeroxFilter; use crate::Command::AddFilter; @@ -182,6 +182,10 @@ impl FeroxScans { serde_json::from_value::(filter.clone()) { Box::new(deserialized) + } else if let Ok(deserialized) = + serde_json::from_value::(filter.clone()) + { + Box::new(deserialized) } else if let Ok(deserialized) = serde_json::from_value::(filter.clone()) { diff --git a/src/scanner/ferox_scanner.rs b/src/scanner/ferox_scanner.rs index 5e884e6..3c68479 100644 --- a/src/scanner/ferox_scanner.rs +++ b/src/scanner/ferox_scanner.rs @@ -10,6 +10,7 @@ use lazy_static::lazy_static; use tokio::sync::Semaphore; use crate::filters::{create_similarity_filter, EmptyFilter, SimilarityFilter}; +use crate::heuristics::WildcardResult; use crate::Command::AddFilter; use crate::{ event_handlers::{ @@ -303,7 +304,21 @@ impl FeroxScanner { // wildcard test let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?; - progress_bar.inc(num_reqs_made); + match num_reqs_made { + Some(WildcardResult::WildcardDirectory(num_reqs)) => { + let message = format!( + "=> {} dir! {} recursion", + style("Wildcard").blue().bright(), + style("stopped").red() + ); + progress_bar.set_message(&message); + progress_bar.inc(num_reqs as u64); + } + Some(WildcardResult::FourOhFourLike(num_reqs)) => { + progress_bar.inc(num_reqs as u64); + } + _ => {} + } } // Arc clones to be passed around to the various scans diff --git a/src/scanner/requester.rs b/src/scanner/requester.rs index bc69a8c..3822a11 100644 --- a/src/scanner/requester.rs +++ b/src/scanner/requester.rs @@ -437,7 +437,7 @@ impl Requester { .handles .filters .data - .should_filter_response(&ferox_response) + .should_filter_response(&ferox_response, self.handles.stats.tx.clone()) { continue; } diff --git a/src/traits.rs b/src/traits.rs index c742778..e285931 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,6 +1,7 @@ //! collection of all traits used use crate::filters::{ - LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WordsFilter, + LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter, + WordsFilter, }; use crate::response::FeroxResponse; use anyhow::Result; @@ -36,6 +37,8 @@ impl Display for dyn FeroxFilter { write!(f, "Response size: {}", style(filter.content_length).cyan()) } else if let Some(filter) = self.as_any().downcast_ref::() { write!(f, "Regex: {}", style(&filter.raw_string).cyan()) + } else if let Some(filter) = self.as_any().downcast_ref::() { + write!(f, "{}", filter) // real Display in the wildcard module } else if let Some(filter) = self.as_any().downcast_ref::() { write!(f, "Status code: {}", style(filter.filter_code).cyan()) } else if let Some(filter) = self.as_any().downcast_ref::() {