From 47d4edb219362b3b00edfcaafe669b30ab3e3bfc Mon Sep 17 00:00:00 2001 From: epi Date: Fri, 18 Sep 2020 17:24:37 -0500 Subject: [PATCH] added printer util, started heuristics, added -S option for size filtering --- Cargo.toml | 2 + src/banner.rs | 17 ++++-- src/brain.rs | 148 ++++++++++++++++++++++++++++++++++++++++++++++---- src/config.rs | 22 ++++++++ src/parser.rs | 19 ++++++- src/utils.rs | 14 +++++ 6 files changed, 204 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 48877e2..fdf12c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ clap = "2" lazy_static = "1.4" toml = "0.5" serde = { version = "1.0", features = ["derive"] } +uuid = { version = "0.8", features = ["v4"] } +ansi_term = "0.12" [dev-dependencies] tempfile = "3.1" diff --git a/src/banner.rs b/src/banner.rs index 0c0fbc6..0093af6 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -1,5 +1,4 @@ -use crate::{VERSION, config::CONFIGURATION}; -// use ansi_term::Colour::{Blue, Yellow}; +use crate::{VERSION, config::CONFIGURATION, utils::status_colorizer}; macro_rules! format_banner_entry_helper { // \u{0020} -> unicode space @@ -44,9 +43,15 @@ by Ben "epi" Risher {} ver: {}"#, '\u{1F913}', VERSION); println!("{}", format_banner_entry!("\u{1F3af}", "Target Url", target)); // 🎯 } + let mut codes = vec![]; + + for code in &CONFIGURATION.statuscodes { + codes.push(status_colorizer(&code.to_string())) + } + println!("{}", format_banner_entry!("\u{1F680}", "Threads", CONFIGURATION.threads)); // 🚀 println!("{}", format_banner_entry!("\u{1f4d6}", "Wordlist", CONFIGURATION.wordlist)); // 📖 - println!("{}", format_banner_entry!("\u{1F197}", "Status Codes", format!("{:?}", CONFIGURATION.statuscodes))); // 🆗 + println!("{}", format_banner_entry!("\u{1F197}", "Status Codes", format!("[{}]", codes.join(", ")))); // 🆗 println!("{}", format_banner_entry!("\u{1f4a5}", "Timeout (secs)", CONFIGURATION.timeout)); // 💥 println!("{}", format_banner_entry!("\u{1F9a1}", "User-Agent", CONFIGURATION.useragent)); // 🦡 @@ -61,12 +66,16 @@ by Ben "epi" Risher {} ver: {}"#, '\u{1F913}', VERSION); } } + if !CONFIGURATION.sizefilters.is_empty() { + println!("{}", format_banner_entry!("\u{1f4a2}", "Size Filters", format!("[{}]", CONFIGURATION.sizefilters.join(", ")))); // 💢 + } + if !CONFIGURATION.output.is_empty() { println!("{}", format_banner_entry!("\u{1f4be}", "Output File", CONFIGURATION.output)); // 💾 } if !CONFIGURATION.extensions.is_empty() { - println!("{}", format_banner_entry!("\u{1f4b2}", "Extensions", format!("{:?}", CONFIGURATION.extensions))); // 💲 + println!("{}", format_banner_entry!("\u{1f4b2}", "Extensions", format!("[{}]", CONFIGURATION.extensions.join(", ")))); // 💲 } if CONFIGURATION.insecure { diff --git a/src/brain.rs b/src/brain.rs index 7b555a9..90c8e81 100644 --- a/src/brain.rs +++ b/src/brain.rs @@ -1,28 +1,154 @@ use uuid::Uuid; use crate::scanner::{make_request, format_url}; use crate::config::CONFIGURATION; +use crate::utils::status_colorizer; +use std::process; +use reqwest::Response; + + +const UUID_LENGTH: u64 = 32; /// todo document pub async fn initialize(target_urls: &[String]) { - for target_url in target_urls { - let nonexistent = format_url(target_url, &unique_string(), None); - let response = make_request(&CONFIGURATION.client, nonexistent.unwrap()).await.unwrap(); - println!("{:?}", response); - if CONFIGURATION.statuscodes.contains(&response.status().as_u16()) { - println!("found wildcard response"); - } - } + log::trace!("enter: initialize({:?})", target_urls); + + let target_urls = connectivity_test(&target_urls).await; + smart_scan(&target_urls).await; + + log::trace!("exit: initialize"); } /// Simple helper to return a uuid, formatted as lowercase without hyphens -fn unique_string() -> String { - Uuid::new_v4().to_simple().to_string() +fn unique_string(length: usize) -> String { + log::trace!("enter: unique_string({})", length); + let mut ids = vec![]; + + for _ in 0..length { + ids.push(Uuid::new_v4().to_simple().to_string()); + } + + let unique_id = ids.join(""); + + log::trace!("exit: unique_string -> {}", unique_id); + unique_id } /// todo document pub async fn smart_scan(target_urls: &[String]) { - println!("{:?}", target_urls); + log::trace!("enter: smart_scan({:?})", target_urls); + for target_url in target_urls { + + if let Some(resp_one) = wildcard_request(&target_url, 1).await { + + let wc_length = resp_one.content_length().unwrap_or(0); + + if wc_length == 0 { + continue; + } + // content length of wildcard is non-zero + + if let Some(resp_two) = wildcard_request(&target_url, 3).await { + // make a second request, with a known-sized longer request + let wc2_length = resp_one.content_length().unwrap_or(0); + 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 + println!("[{}] - Url is being reflected in wildcard response", status_colorizer("WILDCARD")); + } else if wc_length == wc2_length { + println!("[{}] - Wildcard response is a static size; consider filtering by adding -S {} to your command", status_colorizer("WILDCARD"), wc_length); + } + } + } + + } + + log::trace!("exit: smart_scan"); } +/// todo doc +async fn wildcard_request(target_url: &str, length: usize) -> Option { + // todo trace + let unique_str = unique_string(length); + + let nonexistent = match format_url(target_url, &unique_str, None) { + Ok(url) => url, + Err(e) => { + log::error!("{}", e); + return None; + } + }; + + let wildcard = status_colorizer("WILDCARD"); + + match make_request(&CONFIGURATION.client, nonexistent.to_owned()).await { + Ok(response) => { + if CONFIGURATION.statuscodes.contains(&response.status().as_u16()) { + // found a wildcard response + println!("[{}] - Received [{}] for {} ({} bytes)", wildcard, status_colorizer(&response.status().to_string()), response.url(), response.content_length().unwrap_or(0)); + + if response.status().is_redirection() { + // show where it goes, if possible + if let Some(next_loc) = response.headers().get("Location") { + if let Ok(next_loc_str) = next_loc.to_str() { + println!("[{}] {} redirects to => {}", wildcard, response.url(), next_loc_str); + } else { + println!("[{}] {} redirects to => {:?}", wildcard, response.url(), next_loc); + } + } + } + return Some(response); + } + }, + Err(e) => { + log::warn!("{}", e); + return None; + } + } + None +} + +/// todo document +async fn connectivity_test(target_urls: &[String]) -> Vec { + log::trace!("enter: connectivity_test({:?})", target_urls); + + let mut good_urls = vec![]; + + for target_url in target_urls { + let request = match format_url(target_url, "", None) { + Ok(url) => url, + Err(e) => { + log::error!("{}", e); + continue; + } + }; + + match make_request(&CONFIGURATION.client, request).await { + Ok(_) => { + good_urls.push(target_url.to_owned()); + }, + Err(e) => { + println!("Could not connect to {}, skipping...", target_url); + log::error!("{}", e); + } + } + } + + if good_urls.is_empty() { + log::error!("Could not connect to any target provided, exiting."); + log::trace!("exit: connectivity_test"); + process::exit(1); + } + + log::trace!("exit: connectivity_test -> {:?}", good_urls); + + good_urls +} +// +// async fn connectivity_test(target_urls: &[String]) { +// +// +// +// +// } \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 6ff3cad..c4fd7fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,6 +65,9 @@ pub struct Configuration { pub stdin: bool, #[serde(default = "depth")] pub depth: usize, + #[serde(default)] + pub sizefilters: Vec, + } // functions timeout, threads, statuscodes, useragent, wordlist, and depth are used to provide @@ -113,6 +116,7 @@ impl Default for Configuration { output: String::new(), target_url: String::new(), extensions: Vec::new(), + sizefilters: Vec::new(), headers: HashMap::new(), threads: threads(), depth: depth(), @@ -139,6 +143,7 @@ impl Configuration { /// - **useragent**: `feroxer/VERSION` /// - **insecure**: `false` (don't be insecure, i.e. don't allow invalid certs) /// - **extensions**: `None` + /// - **sizefilters**: `None` /// - **headers**: `None` /// - **norecursion**: `false` (recursively scan enumerated sub-directories) /// - **addslash**: `false` @@ -184,6 +189,7 @@ impl Configuration { config.addslash = settings.addslash; config.stdin = settings.stdin; config.depth = settings.depth; + config.sizefilters = settings.sizefilters; } }; }; @@ -233,6 +239,14 @@ impl Configuration { .collect(); } + if args.values_of("sizefilters").is_some() { + config.sizefilters = args + .values_of("sizefilters") + .unwrap() + .map(|val| val.to_string()) + .collect(); + } + if args.is_present("quiet") { // the reason this is protected by an if statement: // consider a user specifying quiet = true in ferox-config.toml @@ -375,6 +389,7 @@ mod tests { addslash = true stdin = true depth = 1 + sizefilters = [4120] "#; let tmp_dir = TempDir::new().unwrap(); let file = tmp_dir.path().join(DEFAULT_CONFIG_NAME); @@ -400,6 +415,7 @@ mod tests { assert_eq!(config.redirects, false); assert_eq!(config.insecure, false); assert_eq!(config.extensions, Vec::::new()); + assert_eq!(config.sizefilters, Vec::::new()); assert_eq!(config.headers, HashMap::new()); } @@ -493,6 +509,12 @@ mod tests { assert_eq!(config.extensions, vec!["html", "php", "js"]); } + #[test] + fn config_reads_sizefilters() { + let config = setup_config_test(); + assert_eq!(config.sizefilters, vec!["4120"]); + } + #[test] fn config_reads_headers() { let config = setup_config_test(); diff --git a/src/parser.rs b/src/parser.rs index 1320d12..0f02658 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -21,7 +21,9 @@ pub fn initialize() -> App<'static, 'static> { .long("url") .required_unless("stdin") .value_name("URL") - .help("The target URL (required, unless --stdin used)"), + .multiple(true) + .use_delimiter(true) + .help("The target URL(s) (required, unless --stdin used)"), ) .arg( Arg::with_name("threads") @@ -156,12 +158,23 @@ pub fn initialize() -> App<'static, 'static> { ) .arg( Arg::with_name("stdin") - .short("S") .long("stdin") .takes_value(false) .help("Read url(s) from STDIN") .conflicts_with("url") ) + .arg( + Arg::with_name("sizefilters") + .short("S") + .long("sizefilter") + .value_name("SIZE") + .takes_value(true) + .multiple(true) + .use_delimiter(true) + .help( + "Filter out messages of a particular size (ex: -S 5120 -S 4927,1970)", + ), + ) .after_help(r#"NOTE: Options that take multiple values are very flexible. Consider the following ways of specifying @@ -171,7 +184,7 @@ pub fn initialize() -> App<'static, 'static> { The command above adds .pdf, .js, .html, .php, .txt, .json, and .docx to each url All of the methods above (multiple flags, space separated, comma separated, etc...) are valid - and interchangeable. The same goes for headers and status codes. + and interchangeable. The same goes for urls, headers, status codes, and size filters. EXAMPLES: Multiple headers: diff --git a/src/utils.rs b/src/utils.rs index f73b1ab..a325e89 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use reqwest::Url; +use ansi_term::Color::{Cyan, Red, Green, Blue, Yellow}; /// Helper function that determines the current depth of a given url /// @@ -51,6 +52,19 @@ pub fn get_current_depth(target: &str) -> usize { } } +/// todo: docs +pub fn status_colorizer(status: &str) -> String { + match status.chars().next() { + Some('1') => Blue.paint(status).to_string(), // informational + Some('2') => Green.paint(status).to_string(), // success + Some('3') => Yellow.paint(status).to_string(), // redirects + Some('4') => Red.paint(status).to_string(), // client error + Some('5') => Red.paint(status).to_string(), // server error + Some('W') => Cyan.paint(status).to_string(), // wildcard + _ => status.to_string() // ¯\_(ツ)_/¯ + } +} + #[cfg(test)] mod tests { use super::*;