implemented/tested logic for collecting backups

This commit is contained in:
epi
2022-02-16 17:16:12 -06:00
parent 88d451144c
commit d13bce2261
2 changed files with 312 additions and 50 deletions

View File

@@ -2,11 +2,13 @@ use super::Command::AddToUsizeField;
use super::*;
use anyhow::{Context, Result};
use futures::future::{BoxFuture, FutureExt};
use tokio::sync::{mpsc, oneshot};
use crate::{
config::Configuration,
progress::PROGRESS_PRINTER,
response::FeroxResponse,
scanner::RESPONSES,
send_command, skip_fail,
statistics::StatField::ResourcesDiscovered,
@@ -15,6 +17,7 @@ use crate::{
CommandReceiver, CommandSender, Joiner,
};
use std::sync::Arc;
use url::Url;
#[derive(Debug)]
/// Container for terminal output transmitter
@@ -185,56 +188,9 @@ impl TermOutHandler {
while let Some(command) = self.receiver.recv().await {
match command {
Command::Report(mut resp) => {
let contains_sentry =
self.config.status_codes.contains(&resp.status().as_u16());
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
let should_process_response = contains_sentry && unknown_sentry;
if should_process_response {
// print to stdout
ferox_print(&resp.as_str(), &PROGRESS_PRINTER);
send_command!(tx_stats, AddToUsizeField(ResourcesDiscovered, 1));
if self.file_task.is_some() {
// -o used, need to send the report to be written out to disk
self.tx_file
.send(Command::Report(resp.clone()))
.with_context(|| {
fmt_err(&format!("Could not send {} to file handler", resp))
})?;
}
}
log::trace!("report complete: {}", resp.url());
if self.config.replay_client.is_some() && should_process_response {
// replay proxy specified/client created and this response's status code is one that
// should be replayed; not using logged_request due to replay proxy client
make_request(
self.config.replay_client.as_ref().unwrap(),
resp.url(),
resp.method().as_str(),
None,
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await
.with_context(|| "Could not replay request through replay proxy")?;
}
if should_process_response {
// add response to RESPONSES for serialization in case of ctrl+c
// placed all by its lonesome like this so that RESPONSES can take ownership
// of the FeroxResponse
// before ownership is transferred, there's no real reason to keep the body anymore
// so we can free that piece of data, reducing memory usage
resp.drop_text();
RESPONSES.insert(*resp);
}
Command::Report(resp) => {
// todo add enum to replace bool
self.process_response(tx_stats.clone(), resp, false).await?;
}
Command::Sync(sender) => {
sender.send(true).unwrap_or_default();
@@ -251,6 +207,139 @@ impl TermOutHandler {
log::trace!("exit: start");
Ok(())
}
/// todo
fn process_response(
&self,
tx_stats: CommandSender,
mut resp: Box<FeroxResponse>,
recursive_call: bool,
) -> BoxFuture<'_, Result<()>> {
async move {
let contains_sentry = self.config.status_codes.contains(&resp.status().as_u16());
let unknown_sentry = !RESPONSES.contains(&resp); // !contains == unknown
let should_process_response = contains_sentry && unknown_sentry;
if should_process_response {
// print to stdout
ferox_print(&resp.as_str(), &PROGRESS_PRINTER);
send_command!(tx_stats, AddToUsizeField(ResourcesDiscovered, 1));
if self.file_task.is_some() {
// -o used, need to send the report to be written out to disk
self.tx_file
.send(Command::Report(resp.clone()))
.with_context(|| {
fmt_err(&format!("Could not send {} to file handler", resp))
})?;
}
}
log::trace!("report complete: {}", resp.url());
if self.config.replay_client.is_some() && should_process_response {
// replay proxy specified/client created and this response's status code is one that
// should be replayed; not using logged_request due to replay proxy client
make_request(
self.config.replay_client.as_ref().unwrap(),
resp.url(),
resp.method().as_str(),
None,
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await
.with_context(|| "Could not replay request through replay proxy")?;
}
// todo update if statement to include --collect-backups
if should_process_response && !recursive_call {
let backup_urls = self.generate_backup_urls(&resp).await;
for backup_url in &backup_urls {
let backup_response = make_request(
&self.config.client,
backup_url,
resp.method().as_str(),
None,
self.config.output_level,
&self.config,
tx_stats.clone(),
)
.await
.with_context(|| {
format!("Could not request backup of {}", resp.url().as_str())
})?;
let mut ferox_response = FeroxResponse::from(
backup_response,
resp.url().as_str(),
resp.method().as_str(),
resp.output_level,
)
.await;
self.process_response(tx_stats.clone(), Box::new(ferox_response), true)
.await?;
}
}
if should_process_response {
// add response to RESPONSES for serialization in case of ctrl+c
// placed all by its lonesome like this so that RESPONSES can take ownership
// of the FeroxResponse
// before ownership is transferred, there's no real reason to keep the body anymore
// so we can free that piece of data, reducing memory usage
resp.drop_text();
RESPONSES.insert(*resp);
}
log::trace!("exit: process_response");
Ok(())
}
.boxed()
}
/// internal helper to stay DRY
fn add_new_url_to_vec(&self, url: &Url, new_name: &str, urls: &mut Vec<Url>) {
let mut new_url = url.clone();
new_url.set_path(&new_name);
urls.push(new_url);
}
/// todo
async fn generate_backup_urls(&self, response: &FeroxResponse) -> Vec<Url> {
// todo
let mut urls = vec![];
let url = response.url();
// confirmed safe: see src/response.rs for comments
let filename = url.path_segments().unwrap().last().unwrap();
if !filename.is_empty() {
// append rules
for suffix in ["~", ".bak", ".bak2", ".old", ".1"] {
self.add_new_url_to_vec(&url, &format!("{}{}", filename, suffix), &mut urls);
}
// vim swap rule
self.add_new_url_to_vec(&url, &format!(".{}.swp", filename), &mut urls);
// replace original extension rule
let parts: Vec<_> = filename
.split('.')
// keep things like /.bash_history out of results
.filter(|part| !part.is_empty())
.collect();
if parts.len() > 1 {
// filename + at least one extension, i.e. whatever.js becomes ["whatever", "js"]
self.add_new_url_to_vec(url, &format!("{}.bak", parts.first().unwrap()), &mut urls);
}
}
// todo
urls
}
}
#[cfg(test)]
@@ -286,4 +375,89 @@ mod tests {
println!("{:?}", toh);
tx.send(Command::Exit).unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// when the feroxresponse's url contains an extension, there should be 7 urls returned
async fn generate_backup_urls_creates_correct_urls_when_extension_present() {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
};
let expected: Vec<_> = vec![
"derp.php~",
"derp.php.bak",
"derp.php.bak2",
"derp.php.old",
"derp.php.1",
".derp.php.swp",
"derp.bak",
];
let mut fr = FeroxResponse::default();
fr.set_url("http://localhost/derp.php");
let urls = toh.generate_backup_urls(&fr).await;
let paths: Vec<_> = urls
.iter()
.map(|url| url.path_segments().unwrap().last().unwrap())
.collect();
assert_eq!(urls.len(), 7);
for path in paths {
assert!(expected.contains(&path));
}
tx.send(Command::Exit).unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
/// when the feroxresponse's url doesn't contain an extension, there should be 6 urls returned
async fn generate_backup_urls_creates_correct_urls_when_extension_not_present() {
let (tx, rx) = mpsc::unbounded_channel::<Command>();
let (tx_file, _) = mpsc::unbounded_channel::<Command>();
let config = Arc::new(Configuration::new().unwrap());
let toh = TermOutHandler {
config,
file_task: None,
receiver: rx,
tx_file,
};
let expected: Vec<_> = vec![
"derp~",
"derp.bak",
"derp.bak2",
"derp.old",
"derp.1",
".derp.swp",
];
let mut fr = FeroxResponse::default();
fr.set_url("http://localhost/derp");
let urls = toh.generate_backup_urls(&fr).await;
let paths: Vec<_> = urls
.iter()
.map(|url| url.path_segments().unwrap().last().unwrap())
.collect();
assert_eq!(urls.len(), 6);
for path in paths {
assert!(expected.contains(&path));
}
tx.send(Command::Exit).unwrap();
}
}

View File

@@ -679,3 +679,91 @@ fn add_discovered_extension_updates_bars_and_stats() {
assert!(contents.contains("extensions_collected: 1"));
assert!(contents.contains("expected_per_scan: 6"));
}
#[test]
/// send a request to a 200 file, expect pre-configured backup collection rules to be applied
/// and then requested
fn collect_backups_makes_appropriate_requests() {
let srv = MockServer::start();
let (tmp_dir, file) = setup_tmp_directory(&["LICENSE.txt".to_string()], "wordlist").unwrap();
let mock = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt");
then.status(200).body("this is a test");
});
let tilde_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt~");
then.status(200);
});
let bak_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt.bak");
then.status(200);
});
let bak2_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt.bak2");
then.status(200);
});
let old_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt.old");
then.status(200);
});
let dot1_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt.1");
then.status(200);
});
let replaced_bak_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.bak");
then.status(200);
});
let vim_swap_backup = srv.mock(|when, then| {
when.method(GET).path("/.LICENSE.txt.swp");
then.status(200);
});
// todo add double backup style tests for all variants
let tilde_double_backup = srv.mock(|when, then| {
when.method(GET).path("/LICENSE.txt~~");
then.status(404);
});
// todo add --collect-backups flag when available
let cmd = Command::cargo_bin("feroxbuster")
.unwrap()
.arg("--url")
.arg(srv.url("/"))
.arg("--wordlist")
.arg(file.as_os_str())
.unwrap();
// todo maybe add in some stdout checks
cmd.assert().success().stdout(
predicate::str::contains("/LICENSE.txt").and(predicate::str::contains("/LICENSE.txt~")),
);
// .and(predicate::str::contains("403"))
// .and(predicate::str::contains("53c"))
// .and(predicate::str::contains("14c"))
// .and(predicate::str::contains("0c"))
// .and(predicate::str::contains("ignored").count(2))
// .and(predicate::str::contains("/ignored/LICENSE")),
// );
assert_eq!(mock.hits(), 1);
assert_eq!(tilde_backup.hits(), 1);
assert_eq!(tilde_double_backup.hits(), 0); // shouldn't request backups of backups
assert_eq!(bak_backup.hits(), 1);
assert_eq!(bak2_backup.hits(), 1);
assert_eq!(old_backup.hits(), 1);
assert_eq!(dot1_backup.hits(), 1);
assert_eq!(replaced_bak_backup.hits(), 1);
assert_eq!(vim_swap_backup.hits(), 1);
teardown_tmp_directory(tmp_dir);
}