mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-04-19 06:31:13 -03:00
finalized new detections; removed wildcard filter and supporting code
This commit is contained in:
@@ -10,7 +10,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
FeroxFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WildcardFilter, WordsFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
|
||||
/// Container around a collection of `FeroxFilters`s
|
||||
@@ -76,13 +76,7 @@ impl FeroxFilters {
|
||||
for filter in filters.iter() {
|
||||
// wildcard.should_filter goes here
|
||||
if filter.should_filter_response(response) {
|
||||
log::warn!("filtering response due to: {:?}", filter);
|
||||
log::warn!("filtering response due to: {:?}", filters);
|
||||
if filter.as_any().downcast_ref::<WildcardFilter>().is_some() {
|
||||
tx_stats
|
||||
.send(AddToUsizeField(WildcardsFiltered, 1))
|
||||
.unwrap_or_default();
|
||||
}
|
||||
log::debug!("filtering response due to: {:?}", filter);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -116,10 +110,6 @@ impl Serialize for FeroxFilters {
|
||||
filter.as_any().downcast_ref::<SimilarityFilter>()
|
||||
{
|
||||
seq.serialize_element(similarity_filter).unwrap_or_default();
|
||||
} else if let Some(wildcard_filter) =
|
||||
filter.as_any().downcast_ref::<WildcardFilter>()
|
||||
{
|
||||
seq.serialize_element(wildcard_filter).unwrap_or_default();
|
||||
}
|
||||
}
|
||||
seq.end()
|
||||
|
||||
@@ -15,7 +15,6 @@ 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 wildcard;
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
use super::*;
|
||||
use ::fuzzyhash::FuzzyHash;
|
||||
use ::regex::Regex;
|
||||
use super::similarity::HashValueType;
|
||||
|
||||
#[test]
|
||||
/// simply test the default values for wildcardfilter, expect 0, 0
|
||||
fn wildcard_filter_default() {
|
||||
let wcf = WildcardFilter::default();
|
||||
assert_eq!(wcf.size, u64::MAX);
|
||||
assert_eq!(wcf.dynamic, u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
fn wildcard_filter_as_any() {
|
||||
let filter = WildcardFilter::default();
|
||||
let filter2 = WildcardFilter::default();
|
||||
|
||||
assert!(filter.box_eq(filter2.as_any()));
|
||||
|
||||
assert_eq!(
|
||||
*filter.as_any().downcast_ref::<WildcardFilter>().unwrap(),
|
||||
filter
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// just a simple test to increase code coverage by hitting as_any and the inner value
|
||||
@@ -109,59 +87,6 @@ fn regex_filter_as_any() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on WilcardFilter where static logic matches
|
||||
fn wildcard_should_filter_when_static_wildcard_found() {
|
||||
let mut resp = FeroxResponse::default();
|
||||
resp.set_wildcard(true);
|
||||
resp.set_url("http://localhost");
|
||||
resp.set_text(
|
||||
"pellentesque diam volutpat commodo sed egestas egestas fringilla phasellus faucibus",
|
||||
);
|
||||
|
||||
let filter = WildcardFilter {
|
||||
size: 83,
|
||||
dynamic: 0,
|
||||
dont_filter: false,
|
||||
method: "GET".to_owned(),
|
||||
};
|
||||
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on WilcardFilter where static logic matches but response length is 0
|
||||
fn wildcard_should_filter_when_static_wildcard_len_is_zero() {
|
||||
let mut resp = FeroxResponse::default();
|
||||
resp.set_wildcard(true);
|
||||
resp.set_url("http://localhost");
|
||||
|
||||
// default WildcardFilter is used in the code that executes when response.content_length() == 0
|
||||
let filter = WildcardFilter::new(false);
|
||||
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on WilcardFilter where dynamic logic matches
|
||||
fn wildcard_should_filter_when_dynamic_wildcard_found() {
|
||||
let mut resp = FeroxResponse::default();
|
||||
resp.set_wildcard(true);
|
||||
resp.set_url("http://localhost/stuff");
|
||||
resp.set_text("pellentesque diam volutpat commodo sed egestas egestas fringilla");
|
||||
|
||||
let filter = WildcardFilter {
|
||||
size: 0,
|
||||
dynamic: 59, // content-length - 5 (len('stuff'))
|
||||
dont_filter: false,
|
||||
method: "GET".to_owned(),
|
||||
};
|
||||
|
||||
println!("resp: {resp:?}: filter: {filter:?}");
|
||||
|
||||
assert!(filter.should_filter_response(&resp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test should_filter on RegexFilter where regex matches body
|
||||
fn regexfilter_should_filter_when_regex_matches_on_response_body() {
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
use super::*;
|
||||
use crate::{url::FeroxUrl, DEFAULT_METHOD};
|
||||
|
||||
/// 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, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
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,
|
||||
|
||||
/// method used in request that should be included with filters passed via runtime configuration
|
||||
pub method: String,
|
||||
|
||||
/// whether or not the user passed -D on the command line
|
||||
pub(super) 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 both values with u64::MAX
|
||||
impl Default for WildcardFilter {
|
||||
/// populate both values with u64::MAX
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dont_filter: false,
|
||||
size: u64::MAX,
|
||||
method: DEFAULT_METHOD.to_owned(),
|
||||
dynamic: u64::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
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.size != u64::MAX
|
||||
&& self.size == response.content_length()
|
||||
&& self.method == response.method().as_str()
|
||||
{
|
||||
// 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.size == u64::MAX
|
||||
&& response.content_length() == 0
|
||||
&& self.method == response.method().as_str()
|
||||
{
|
||||
// static wildcard size found during testing
|
||||
// but response length was zero; pointed out by @Tib3rius
|
||||
log::debug!("static wildcard: filtered out {}", response.url());
|
||||
log::trace!("exit: should_filter_response -> true");
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.dynamic != u64::MAX {
|
||||
// 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 = FeroxUrl::path_length_of_url(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::<Self>().map_or(false, |a| self == a)
|
||||
}
|
||||
|
||||
/// Return self as Any for dynamic dispatch purposes
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use crate::nlp::preprocess;
|
||||
use crate::{
|
||||
config::OutputLevel,
|
||||
event_handlers::{Command, Handles},
|
||||
filters::{LinesFilter, SizeFilter, WildcardFilter, WordsFilter},
|
||||
filters::{LinesFilter, SizeFilter, WordsFilter},
|
||||
progress::PROGRESS_PRINTER,
|
||||
response::FeroxResponse,
|
||||
skip_fail,
|
||||
@@ -23,23 +23,6 @@ use crate::{
|
||||
/// length of a standard UUID, used when determining wildcard responses
|
||||
const UUID_LENGTH: u64 = 32;
|
||||
|
||||
/// wrapper around ugly string formatting
|
||||
macro_rules! format_template {
|
||||
($template:expr, $method:expr, $length:expr) => {
|
||||
format!(
|
||||
$template,
|
||||
status_colorizer("WLD"),
|
||||
$method,
|
||||
"-",
|
||||
"-",
|
||||
"-",
|
||||
style("auto-filtering").yellow(),
|
||||
style($length).cyan(),
|
||||
style("--dont-filter").yellow()
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// enum representing the different servers that `parse_html` can detect when directory listing is
|
||||
/// enabled
|
||||
#[derive(Copy, Debug, Clone)]
|
||||
@@ -76,26 +59,6 @@ pub struct HeuristicTests {
|
||||
handles: Arc<Handles>,
|
||||
}
|
||||
|
||||
/// simple way to pass around a wildcard filter to the internal helper below
|
||||
/// in a way that quickly tells us if it's static or dynamic
|
||||
enum WildcardType<'a> {
|
||||
Static(&'a WildcardFilter),
|
||||
Dynamic(&'a WildcardFilter),
|
||||
}
|
||||
|
||||
/// internal helper to stop repeating the same pattern
|
||||
fn print_dont_filter_message(wildcard_type: WildcardType, output_level: OutputLevel) {
|
||||
if matches!(output_level, OutputLevel::Default | OutputLevel::Quiet) {
|
||||
let msg = match wildcard_type {
|
||||
WildcardType::Static(wc) => {
|
||||
format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is static; {} {} responses; toggle this behavior by using {}\n", wc.method.as_str(), wc.size)
|
||||
}
|
||||
WildcardType::Dynamic(wc) => format_template!("{} {:>8} {:>9} {:>9} {:>9} Wildcard response is dynamic; {} ({} + url length) responses; toggle this behavior by using {}\n", wc.method.as_str(), wc.dynamic),
|
||||
};
|
||||
ferox_print(&msg, &PROGRESS_PRINTER);
|
||||
}
|
||||
}
|
||||
|
||||
/// HeuristicTests implementation
|
||||
impl HeuristicTests {
|
||||
/// create a new HeuristicTests struct
|
||||
@@ -122,167 +85,6 @@ impl HeuristicTests {
|
||||
unique_id
|
||||
}
|
||||
|
||||
/// wrapper for sending a filter to the filters event handler
|
||||
fn send_filter(&self, filter: WildcardFilter) -> Result<()> {
|
||||
self.handles
|
||||
.filters
|
||||
.send(Command::AddFilter(Box::new(filter)))
|
||||
}
|
||||
|
||||
/// Tests the given url to see if it issues a wildcard response
|
||||
///
|
||||
/// In the event that url returns a wildcard response, a
|
||||
/// [WildcardFilter](struct.WildcardFilter.html) is created and sent to the filters event
|
||||
/// handler.
|
||||
///
|
||||
/// Returns the number of times to increment the caller's progress bar
|
||||
pub async fn wildcard(&self, target_url: &str) -> Result<u64> {
|
||||
log::trace!("enter: wildcard_test({:?})", target_url);
|
||||
|
||||
if self.handles.config.dont_filter {
|
||||
// early return, dont_filter scans don't need tested
|
||||
log::trace!("exit: wildcard_test -> 0");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let data = match self.handles.config.data.is_empty() {
|
||||
true => None,
|
||||
false => Some(self.handles.config.data.as_slice()),
|
||||
};
|
||||
|
||||
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
|
||||
|
||||
for method in self.handles.config.methods.iter() {
|
||||
let ferox_response = self
|
||||
.make_wildcard_request(&ferox_url, method.as_str(), data, 1)
|
||||
.await?;
|
||||
|
||||
// found a wildcard response
|
||||
let mut wildcard = WildcardFilter::new(self.handles.config.dont_filter);
|
||||
|
||||
let wc_length = ferox_response.content_length();
|
||||
|
||||
if wc_length == 0 {
|
||||
log::trace!("exit: wildcard_test -> 1");
|
||||
print_dont_filter_message(
|
||||
WildcardType::Static(&wildcard),
|
||||
self.handles.config.output_level,
|
||||
);
|
||||
self.send_filter(wildcard)?;
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
// content length of wildcard is non-zero, perform additional tests:
|
||||
// make a second request, with a known-sized (64) longer request
|
||||
let resp_two = self
|
||||
.make_wildcard_request(&ferox_url, method.as_str(), data, 3)
|
||||
.await?;
|
||||
|
||||
let wc2_length = resp_two.content_length();
|
||||
|
||||
wildcard.method = resp_two.method().as_str().to_owned();
|
||||
|
||||
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
|
||||
wildcard.dynamic = wc_length - UUID_LENGTH;
|
||||
|
||||
print_dont_filter_message(
|
||||
WildcardType::Dynamic(&wildcard),
|
||||
self.handles.config.output_level,
|
||||
);
|
||||
} else if wc_length == wc2_length {
|
||||
wildcard.size = wc_length;
|
||||
|
||||
print_dont_filter_message(
|
||||
WildcardType::Static(&wildcard),
|
||||
self.handles.config.output_level,
|
||||
);
|
||||
}
|
||||
|
||||
self.send_filter(wildcard)?;
|
||||
}
|
||||
|
||||
log::trace!("exit: wildcard_test");
|
||||
Ok(2)
|
||||
}
|
||||
|
||||
/// Generates a uuid and appends it to the given target url. The reasoning is that the randomly
|
||||
/// generated unique string should not exist on and be served by the target web server.
|
||||
///
|
||||
/// Once the unique url is created, the request is sent to the server. If the server responds
|
||||
/// back with a valid status code, the response is considered to be a wildcard response. If that
|
||||
/// wildcard response has a 3xx status code, that redirection location is displayed to the user.
|
||||
async fn make_wildcard_request(
|
||||
&self,
|
||||
target: &FeroxUrl,
|
||||
method: &str,
|
||||
data: Option<&[u8]>,
|
||||
length: usize,
|
||||
) -> Result<FeroxResponse> {
|
||||
log::trace!("enter: make_wildcard_request({}, {})", target, length);
|
||||
|
||||
let unique_str = self.unique_string(length);
|
||||
|
||||
// To take care of slash when needed
|
||||
let slash = if self.handles.config.add_slash {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let nonexistent_url = target.format(&unique_str, slash)?;
|
||||
|
||||
let response = logged_request(
|
||||
&nonexistent_url.to_owned(),
|
||||
method,
|
||||
data,
|
||||
self.handles.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if self
|
||||
.handles
|
||||
.config
|
||||
.status_codes
|
||||
.contains(&response.status().as_u16())
|
||||
{
|
||||
// found a wildcard response
|
||||
|
||||
let mut ferox_response = FeroxResponse::from(
|
||||
response,
|
||||
&target.target,
|
||||
method,
|
||||
self.handles.config.output_level,
|
||||
)
|
||||
.await;
|
||||
ferox_response.set_wildcard(true);
|
||||
|
||||
if self
|
||||
.handles
|
||||
.filters
|
||||
.data
|
||||
.should_filter_response(&ferox_response, self.handles.stats.tx.clone())
|
||||
{
|
||||
bail!("filtered response")
|
||||
}
|
||||
|
||||
if matches!(
|
||||
self.handles.config.output_level,
|
||||
OutputLevel::Default | OutputLevel::Quiet
|
||||
) {
|
||||
let boxed = Box::new(ferox_response.clone());
|
||||
self.handles.output.send(Command::Report(boxed))?;
|
||||
}
|
||||
|
||||
log::trace!("exit: make_wildcard_request -> {}", ferox_response);
|
||||
return Ok(ferox_response);
|
||||
}
|
||||
|
||||
log::trace!("exit: make_wildcard_request -> Err");
|
||||
bail!("uninteresting status code")
|
||||
}
|
||||
|
||||
/// Simply tries to connect to all given sites before starting to scan
|
||||
///
|
||||
/// In the event that no sites can be reached, the program will exit.
|
||||
@@ -439,19 +241,17 @@ impl HeuristicTests {
|
||||
}
|
||||
|
||||
/// given a target's base url, attempt to automatically detect its 404 response
|
||||
/// pattern, and then set a filter that will exclude all but the first result
|
||||
pub async fn detect_404_response(&self, target_url: &str) -> Result<u64> {
|
||||
log::trace!("enter: detect_404_response({:?})", target_url);
|
||||
/// 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> {
|
||||
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_response -> dont_filter is true");
|
||||
log::trace!("exit: detect_404_like_responses -> dont_filter is true");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut size_sentry = true;
|
||||
let mut word_sentry = true;
|
||||
let mut line_sentry = true;
|
||||
let mut req_counter = 0;
|
||||
|
||||
let data = if self.handles.config.data.is_empty() {
|
||||
@@ -460,24 +260,33 @@ impl HeuristicTests {
|
||||
Some(self.handles.config.data.as_slice())
|
||||
};
|
||||
|
||||
// To take care of slash when needed
|
||||
let slash = if self.handles.config.add_slash {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// 4 is due to the array in the nested for loop below
|
||||
let mut responses = Vec::with_capacity(4);
|
||||
|
||||
// for every method, attempt to id its 404 response
|
||||
//
|
||||
// a good example of one where the GET/POST differ is on hackthebox:
|
||||
// - http://prd.m.rendering-api.interface.htb/api
|
||||
for method in self.handles.config.methods.iter() {
|
||||
for (prefix, length) in [("", 1), ("", 3), (".htaccess", 1), ("admin", 1)] {
|
||||
let path = format!("{prefix}{}", self.unique_string(length));
|
||||
|
||||
// To take care of slash when needed
|
||||
let slash = if self.handles.config.add_slash {
|
||||
Some("/")
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ferox_url = FeroxUrl::from_string(target_url, self.handles.clone());
|
||||
|
||||
let nonexistent_url = ferox_url.format(&path, slash)?;
|
||||
|
||||
// example requests:
|
||||
// - http://localhost/2fc1077836ad43ab98b7a31c2ca28fea
|
||||
// - http://localhost/92969beae6bf4beb855d1622406d87e395c87387a9ad432e8a11245002b709b03cf609d471004154b83bcc1c6ec49f6f
|
||||
// - http://localhost/.htaccessa005a2131e68449aa26e99029c914c09
|
||||
// - http://localhost/adminf1d2541e73c44dcb9d1fb7d93334b280
|
||||
let response =
|
||||
logged_request(&nonexistent_url, &method, data, self.handles.clone()).await;
|
||||
|
||||
@@ -512,74 +321,18 @@ impl HeuristicTests {
|
||||
|
||||
if responses.len() < 2 {
|
||||
// don't have enough responses to make a determination, continue to next method
|
||||
responses.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
// examine chars/words/lines for each response
|
||||
// if all responses respetive length match each other, we can assume
|
||||
// that will remain true for subsequent non-existent urls, and create
|
||||
// a filter for it.
|
||||
//
|
||||
// values are examined from most to least specific (content length, word count, line count)
|
||||
let content_length = responses[0].content_length();
|
||||
let word_count = responses[0].word_count();
|
||||
let line_count = responses[0].line_count();
|
||||
|
||||
for response in &responses[1..] {
|
||||
// if any of the responses differ in length, that particular
|
||||
// response length type is no longer a canidate for filtering
|
||||
if response.content_length() != content_length {
|
||||
size_sentry = false;
|
||||
}
|
||||
|
||||
if response.word_count() != word_count {
|
||||
word_sentry = false;
|
||||
}
|
||||
|
||||
if response.line_count() != line_count {
|
||||
line_sentry = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
let (command, filter_type, filter_length) = if size_sentry {
|
||||
log::info!(
|
||||
"[404-like] {target_url} => filtering future responses with {content_length} bytes"
|
||||
);
|
||||
(
|
||||
Command::AddFilter(Box::new(SizeFilter { content_length })),
|
||||
"bytes",
|
||||
content_length as usize,
|
||||
)
|
||||
} else if word_sentry {
|
||||
log::info!(
|
||||
"[404-like] {target_url} => filtering future responses with {word_count} words"
|
||||
);
|
||||
(
|
||||
Command::AddFilter(Box::new(WordsFilter { word_count })),
|
||||
"words",
|
||||
word_count,
|
||||
)
|
||||
} else if line_sentry {
|
||||
log::info!(
|
||||
"[404-like] {target_url} => filtering future responses with {line_count} lines"
|
||||
);
|
||||
(
|
||||
Command::AddFilter(Box::new(LinesFilter { line_count })),
|
||||
"lines",
|
||||
line_count,
|
||||
)
|
||||
} else {
|
||||
log::trace!("exit: detect_404_response -> no filter added");
|
||||
// no match was found; clear the vec and continue to the next
|
||||
// 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 {
|
||||
// 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() {
|
||||
@@ -590,10 +343,14 @@ impl HeuristicTests {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
|
||||
// if we're here, we've detected a 404-like response pattern, so we're already filtering for size/word/line
|
||||
// create the new filter
|
||||
self.handles.filters.send(command)?;
|
||||
|
||||
// if we're here, we've detected a 404-like response pattern, and we're already filtering for size/word/line
|
||||
//
|
||||
// in addition, we'll create a similarity filter as a fallback
|
||||
let hash = SIM_HASHER.create_signature(preprocess(responses[0].text()).iter());
|
||||
|
||||
@@ -602,14 +359,14 @@ impl HeuristicTests {
|
||||
original_url: responses[0].url().to_string(),
|
||||
};
|
||||
|
||||
self.handles.filters.send(command)?;
|
||||
self.handles
|
||||
.filters
|
||||
.send(Command::AddFilter(Box::new(sim_filter)))?;
|
||||
|
||||
// reset the responses for the next method, if it exists
|
||||
responses.clear();
|
||||
|
||||
self.handles
|
||||
.filters
|
||||
.send(Command::AddFilter(Box::new(sim_filter)))?;
|
||||
// report to the user, if appropriate
|
||||
if matches!(
|
||||
self.handles.config.output_level,
|
||||
OutputLevel::Default | OutputLevel::Quiet
|
||||
@@ -619,10 +376,75 @@ impl HeuristicTests {
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!("exit: detect_404_response");
|
||||
log::trace!("exit: detect_404_like_responses");
|
||||
|
||||
return Ok(req_counter);
|
||||
}
|
||||
|
||||
/// for all responses, examine chars/words/lines
|
||||
/// if all responses respective lengths match each other, we can assume
|
||||
/// that will remain true for subsequent non-existent urls
|
||||
///
|
||||
/// values are examined from most to least specific (content length, word count, line count)
|
||||
fn examine_404_like_responses(
|
||||
&self,
|
||||
responses: &[FeroxResponse],
|
||||
) -> Option<(Command, &'static str, usize)> {
|
||||
let mut size_sentry = true;
|
||||
let mut word_sentry = true;
|
||||
let mut line_sentry = true;
|
||||
|
||||
let content_length = responses[0].content_length();
|
||||
let word_count = responses[0].word_count();
|
||||
let line_count = responses[0].line_count();
|
||||
|
||||
for response in &responses[1..] {
|
||||
// if any of the responses differ in length, that particular
|
||||
// response length type is no longer a candidate for filtering
|
||||
if response.content_length() != content_length {
|
||||
size_sentry = false;
|
||||
}
|
||||
|
||||
if response.word_count() != word_count {
|
||||
word_sentry = false;
|
||||
}
|
||||
|
||||
if response.line_count() != line_count {
|
||||
line_sentry = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use super::*;
|
||||
use crate::event_handlers::Handles;
|
||||
use crate::filters::{
|
||||
EmptyFilter, LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WildcardFilter, WordsFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
use crate::traits::FeroxFilter;
|
||||
use crate::Command::AddFilter;
|
||||
@@ -182,10 +182,6 @@ 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())
|
||||
{
|
||||
|
||||
@@ -298,25 +298,12 @@ impl FeroxScanner {
|
||||
// the server is using to respond to resources that don't exist (could be a
|
||||
// traditional 404, or a custom response)
|
||||
//
|
||||
// `detect_404_response` will make the requests that the wildcard test used to
|
||||
// perform pre-2.8, and then hand those off to `wildcard`, so we're not
|
||||
// duplicating effort.
|
||||
//
|
||||
// the wildcard test will only add a filter in the event that the filter it
|
||||
// would create isn't already being filtered by the user or by the
|
||||
// auto-detected 404-like response
|
||||
let num_reqs_made = test.detect_404_response(&self.target_url).await?;
|
||||
// `detect_404_like_responses` will make the requests that the wildcard test used to
|
||||
// perform pre-2.8 in addition to new detection techniques, superseding the old
|
||||
// wildcard test
|
||||
let num_reqs_made = test.detect_404_like_responses(&self.target_url).await?;
|
||||
|
||||
|
||||
// let num_reqs_made = test.wildcard(detection_responses).await.unwrap_or_default();
|
||||
progress_bar.inc(num_reqs_made);
|
||||
|
||||
// if !detected {
|
||||
// // on error, we'll have 0, same for --dont-filter
|
||||
// // anything higher than 0 indicates a wildcard was found
|
||||
// let num_reqs_made = test.wildcard(&self.target_url).await.unwrap_or_default();
|
||||
// progress_bar.inc(num_reqs_made);
|
||||
// }
|
||||
}
|
||||
|
||||
// Arc clones to be passed around to the various scans
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! collection of all traits used
|
||||
use crate::filters::{
|
||||
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter, WildcardFilter,
|
||||
LinesFilter, RegexFilter, SimilarityFilter, SizeFilter, StatusCodeFilter,
|
||||
WordsFilter,
|
||||
};
|
||||
use crate::response::FeroxResponse;
|
||||
@@ -37,12 +37,6 @@ 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>() {
|
||||
if filter.dynamic != u64::MAX {
|
||||
write!(f, "Dynamic wildcard: {}", style(filter.dynamic).cyan())
|
||||
} else {
|
||||
write!(f, "Static wildcard: {}", style(filter.size).cyan())
|
||||
}
|
||||
} 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