mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-28 01:11:12 -03:00
implemented fix for wildcard directories
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -761,7 +761,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "feroxbuster"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "feroxbuster"
|
||||
version = "2.8.0"
|
||||
version = "2.8.1"
|
||||
authors = ["Ben 'epi' Risher (@epi052)"]
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
|
||||
@@ -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: ' \
|
||||
|
||||
@@ -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)')
|
||||
|
||||
@@ -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)'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<WildcardFilter>().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::<SizeFilter>() {
|
||||
seq.serialize_element(size_filter).unwrap_or_default();
|
||||
} else if let Some(wildcard_filter) =
|
||||
filter.as_any().downcast_ref::<WildcardFilter>()
|
||||
{
|
||||
seq.serialize_element(wildcard_filter).unwrap_or_default();
|
||||
} else if let Some(status_filter) =
|
||||
filter.as_any().downcast_ref::<StatusCodeFilter>()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
180
src/filters/wildcard.rs
Normal file
180
src/filters/wildcard.rs
Normal file
@@ -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<u64>,
|
||||
|
||||
/// The number of lines contained in the body of this response, if known
|
||||
pub line_count: Option<usize>,
|
||||
|
||||
/// The number of words contained in the body of this response, if known
|
||||
pub word_count: Option<usize>,
|
||||
|
||||
/// 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::<Self>().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)
|
||||
}
|
||||
}
|
||||
@@ -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<u64> {
|
||||
pub async fn detect_404_like_responses(
|
||||
&self,
|
||||
target_url: &str,
|
||||
) -> Result<Option<WildcardResult>> {
|
||||
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::<WildcardFilter>()
|
||||
{
|
||||
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<Box<WildcardFilter>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::<WordsFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<WildcardFilter>(filter.clone())
|
||||
{
|
||||
Box::new(deserialized)
|
||||
} else if let Ok(deserialized) =
|
||||
serde_json::from_value::<SizeFilter>(filter.clone())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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::<RegexFilter>() {
|
||||
write!(f, "Regex: {}", style(&filter.raw_string).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<WildcardFilter>() {
|
||||
write!(f, "{}", filter) // real Display in the wildcard module
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<StatusCodeFilter>() {
|
||||
write!(f, "Status code: {}", style(filter.filter_code).cyan())
|
||||
} else if let Some(filter) = self.as_any().downcast_ref::<SimilarityFilter>() {
|
||||
|
||||
Reference in New Issue
Block a user