mirror of
https://github.com/epi052/feroxbuster.git
synced 2026-05-29 10:31:12 -03:00
added printer util, started heuristics, added -S option for size filtering
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
148
src/brain.rs
148
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<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]) {
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// }
|
||||
@@ -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();
|
||||
|
||||
@@ -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:
|
||||
|
||||
14
src/utils.rs
14
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::*;
|
||||
|
||||
Reference in New Issue
Block a user