added printer util, started heuristics, added -S option for size filtering

This commit is contained in:
epi
2020-09-18 17:24:37 -05:00
parent a82098cf6b
commit 47d4edb219
6 changed files with 204 additions and 18 deletions

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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<Response> {
// 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<String> {
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]) {
//
//
//
//
// }

View File

@@ -65,6 +65,9 @@ pub struct Configuration {
pub stdin: bool,
#[serde(default = "depth")]
pub depth: usize,
#[serde(default)]
pub sizefilters: Vec<String>,
}
// 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::<String>::new());
assert_eq!(config.sizefilters, Vec::<String>::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();

View File

@@ -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:

View File

@@ -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::*;