From f8edf5b8437afd7303ec2bd209549e817e404237 Mon Sep 17 00:00:00 2001 From: phra Date: Wed, 19 Jun 2019 20:02:27 +0200 Subject: [PATCH 01/17] refactor: add subcommands --- src/main.rs | 511 ++++++++++++++++++++++++++-------------------------- 1 file changed, 259 insertions(+), 252 deletions(-) diff --git a/src/main.rs b/src/main.rs index 41e7d0b..69aa25c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate log; #[macro_use] extern crate clap; -use clap::{App, Arg}; +use clap::{App, Arg, SubCommand}; use indicatif::{ProgressBar, ProgressStyle}; use terminal_size::{terminal_size, Height, Width}; @@ -65,207 +65,78 @@ fn main() { --csrf-url \"http://localhost:3000/csrf\" \\ --csrf-regex '\\{\"csrf\":\"(\\w+)\"\\}' ") - .arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .multiple(true) - .help("Sets the level of verbosity"), - ) - .arg( - Arg::with_name("no-banner") - .long("no-banner") - .help("Skips initial banner"), - ) - .arg( - Arg::with_name("url") - .long("url") - .alias("domain") - .help("Sets the target URL") - .short("u") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name("wordlist") - .long("wordlist") - .help("Sets the wordlist") - .short("w") - .takes_value(true) - .multiple(true) - .required(true), - ) - .arg( - Arg::with_name("extensions") - .long("extensions") - .help("Sets the extensions") - .short("e") - .default_value("") - .use_delimiter(true), - ) - .arg( - Arg::with_name("mode") - .long("mode") - .help("Sets the mode of operation (dir, dns, fuzz)") - .short("m") - .takes_value(true) - .default_value("dir"), - ) - .arg( - Arg::with_name("threads") - .long("threads") - .alias("workers") - .help("Sets the amount of concurrent requests") - .short("t") - .default_value("10") - .takes_value(true), - ) - .arg( - Arg::with_name("ignore-certificate") - .long("ignore-certificate") - .alias("no-check-certificate") - .help("Disables TLS certificate validation") - .short("k"), - ) - .arg( - Arg::with_name("exit-on-error") - .long("exit-on-error") - .help("Exits on connection errors") - .short("K"), - ) - .arg( - Arg::with_name("include-status-codes") - .long("include-status-codes") - .help("Sets the list of status codes to include") - .short("s") - .default_value("") - .use_delimiter(true), - ) - .arg( - Arg::with_name("ignore-status-codes") - .long("ignore-status-codes") - .help("Sets the list of status codes to ignore") - .short("S") - .default_value("404") - .use_delimiter(true), - ) - .arg( - Arg::with_name("output") - .long("output") - .help("Saves the results in the specified file") - .short("o") - .default_value("") - .takes_value(true), - ) - .arg( - Arg::with_name("no-progress-bar") - .long("no-progress-bar") - .help("Disables the progress bar"), - ) - .arg( - Arg::with_name("http-method") - .long("http-method") - .help("Uses the specified HTTP method") - .short("X") - .default_value("GET") - .takes_value(true), - ) - .arg( - Arg::with_name("http-body") - .long("http-body") - .help("Uses the specified HTTP method") - .short("b") - .default_value("") - .takes_value(true), - ) - .arg( - Arg::with_name("http-header") - .long("http-header") - .help("Appends the specified HTTP header") - .short("H") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("user-agent") - .long("user-agent") - .help("Uses the specified User-Agent") - .short("a") - .default_value("rustbuster") - .takes_value(true), - ) - .arg( - Arg::with_name("domain") - .long("domain") - .help("Uses the specified domain") - .short("d") - .takes_value(true), - ) - .arg( - Arg::with_name("append-slash") - .long("append-slash") - .help("Tries to also append / to the base request") - .short("f"), - ) - .arg( - Arg::with_name("ignore-string") - .long("ignore-string") - .help("Ignores results with specified string in the HTTP Body") - .short("x") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("include-string") - .long("include-string") - .help("Includes results with specified string in the HTTP body") - .short("i") - .multiple(true) - .conflicts_with("ignore-string") - .takes_value(true), - ) - .arg( - Arg::with_name("csrf-url") - .long("csrf-url") - .help("Grabs the CSRF token via GET to csrf-url") - .requires("csrf-regex") - .takes_value(true), - ) - .arg( - Arg::with_name("csrf-regex") - .long("csrf-regex") - .help("Grabs the CSRF token applying the specified RegEx") - .requires("csrf-url") - .takes_value(true), - ) - .arg( - Arg::with_name("csrf-header") - .long("csrf-header") - .help("Adds the specified headers to CSRF GET request") - .requires("csrf-url") - .multiple(true) - .takes_value(true), - ) + .subcommand(set_common_args(SubCommand::with_name("dir")) + .about("Directories and files enumeration mode") + .arg( + Arg::with_name("extensions") + .long("extensions") + .help("Sets the extensions") + .short("e") + .default_value("") + .use_delimiter(true), + ) + .arg( + Arg::with_name("append-slash") + .long("append-slash") + .help("Tries to also append / to the base request") + .short("f"), + )) + .subcommand(set_common_args(SubCommand::with_name("dns")) + .about("A/AAAA entries enumeration mode")) + .subcommand(set_common_args(SubCommand::with_name("vhost")) + .about("Virtual hosts enumeration mode") + .arg( + Arg::with_name("domain") + .long("domain") + .help("Uses the specified domain to bruteforce") + .short("d") + .takes_value(true), + )) + .subcommand(set_common_args(SubCommand::with_name("fuzz")) + .about("Custom fuzzing enumeration mode") + .arg( + Arg::with_name("csrf-url") + .long("csrf-url") + .help("Grabs the CSRF token via GET to csrf-url") + .requires("csrf-regex") + .takes_value(true), + ) + .arg( + Arg::with_name("csrf-regex") + .long("csrf-regex") + .help("Grabs the CSRF token applying the specified RegEx") + .requires("csrf-url") + .takes_value(true), + ) + .arg( + Arg::with_name("csrf-header") + .long("csrf-header") + .help("Adds the specified headers to CSRF GET request") + .requires("csrf-url") + .multiple(true) + .takes_value(true), + )) .get_matches(); - let domain = matches.value_of("domain").unwrap_or(""); - let append_slash = matches.is_present("append-slash"); - let user_agent = matches.value_of("user-agent").unwrap(); - let http_method = matches.value_of("http-method").unwrap(); - let http_body = matches.value_of("http-body").unwrap(); - let url = matches.value_of("url").unwrap(); - let wordlist_paths = matches + let mode = matches.subcommand_name().unwrap_or("dir"); + let submatches = matches.subcommand_matches(mode).unwrap(); + let domain = submatches.value_of("domain").unwrap_or(""); + let append_slash = submatches.is_present("append-slash"); + let user_agent = submatches.value_of("user-agent").unwrap(); + let http_method = submatches.value_of("http-method").unwrap(); + let http_body = submatches.value_of("http-body").unwrap(); + let url = submatches.value_of("url").unwrap(); + let wordlist_paths = submatches .values_of("wordlist") .unwrap() .map(|w| w.to_owned()) .collect::>(); - let mode = matches.value_of("mode").unwrap(); - let ignore_certificate = matches.is_present("ignore-certificate"); - let mut no_banner = matches.is_present("no-banner"); - let mut no_progress_bar = matches.is_present("no-progress-bar"); - let exit_on_connection_errors = matches.is_present("exit-on-error"); - let http_headers: Vec<(String, String)> = if matches.is_present("http-header") { - matches + let ignore_certificate = submatches.is_present("ignore-certificate"); + let mut no_banner = submatches.is_present("no-banner"); + let mut no_progress_bar = submatches.is_present("no-progress-bar"); + let exit_on_connection_errors = submatches.is_present("exit-on-error"); + let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { + submatches .values_of("http-header") .unwrap() .map(|h| fuzzbuster::utils::split_http_headers(h)) @@ -273,8 +144,8 @@ fn main() { } else { Vec::new() }; - let ignore_strings: Vec = if matches.is_present("ignore-string") { - matches + let ignore_strings: Vec = if submatches.is_present("ignore-string") { + submatches .values_of("ignore-string") .unwrap() .map(|h| h.to_owned()) @@ -282,8 +153,8 @@ fn main() { } else { Vec::new() }; - let include_strings: Vec = if matches.is_present("include-string") { - matches + let include_strings: Vec = if submatches.is_present("include-string") { + submatches .values_of("include-string") .unwrap() .map(|h| h.to_owned()) @@ -291,17 +162,12 @@ fn main() { } else { Vec::new() }; - let n_threads = matches + let n_threads = submatches .value_of("threads") .unwrap() .parse::() .expect("threads is a number"); - let extensions = matches - .values_of("extensions") - .unwrap() - .filter(|e| !e.is_empty()) - .collect::>(); - let include_status_codes = matches + let include_status_codes = submatches .values_of("include-status-codes") .unwrap() .filter(|s| { @@ -316,7 +182,7 @@ fn main() { }) .map(|s| s.to_string()) .collect::>(); - let ignore_status_codes = matches + let ignore_status_codes = submatches .values_of("ignore-status-codes") .unwrap() .filter(|s| { @@ -331,49 +197,33 @@ fn main() { }) .map(|s| s.to_string()) .collect::>(); - let output = matches.value_of("output").unwrap(); - let csrf_url = match matches.value_of("csrf-url") { - Some(v) => Some(v.to_owned()), - None => None, - }; - let csrf_regex = match matches.value_of("csrf-regex") { - Some(v) => Some(v.to_owned()), - None => None, - }; - let csrf_headers: Option> = if matches.is_present("csrf-header") { - Some( - matches - .values_of("csrf-header") - .unwrap() - .map(|h| fuzzbuster::utils::split_http_headers(h)) - .collect(), - ) - } else { - None - }; - - match url.parse::() { - Err(e) => { - error!( - "Invalid URL: {}, consider adding a protocol like http:// or https://", - e - ); - return; - } - Ok(v) => match v.scheme_part() { - Some(s) => { - if s != "http" && s != "https" { - error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); - return; - } + let output = submatches.value_of("output").unwrap(); + + if mode != "dns" { + debug!("mode {}", mode); + match url.parse::() { + Err(e) => { + error!( + "Invalid URL: {}, consider adding a protocol like http:// or https://", + e + ); + return; } - None => { - if mode != "dns" { - error!("Invalid URL: missing protocol, consider adding http:// or https://"); - return; + Ok(v) => match v.scheme_part() { + Some(s) => { + if s != "http" && s != "https" { + error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); + return; + } } - } - }, + None => { + if mode != "dns" { + error!("Invalid URL: missing protocol, consider adding http:// or https://"); + return; + } + } + }, + } } let all_wordlists_exist = wordlist_paths @@ -396,7 +246,6 @@ fn main() { debug!("Using url: {:?}", url); debug!("Using wordlist: {:?}", wordlist_paths); debug!("Using mode: {:?}", mode); - debug!("Using extensions: {:?}", extensions); debug!("Using concurrent requests: {:?}", n_threads); debug!("Using certificate validation: {:?}", !ignore_certificate); debug!("Using HTTP headers: {:?}", http_headers); @@ -416,7 +265,7 @@ fn main() { // Vary the output based on how many times the user used the "verbose" flag // (i.e. 'myprog -v -v -v' or 'myprog -vvv' vs 'myprog -v' - match matches.occurrences_of("verbose") { + match submatches.occurrences_of("verbose") { 0 => trace!("No verbose info"), 1 => trace!("Some verbose info"), 2 => trace!("Tons of verbose info"), @@ -450,7 +299,7 @@ fn main() { banner::configuration( mode, url, - matches.value_of("threads").unwrap(), + submatches.value_of("threads").unwrap(), &wordlist_paths[0] ) ); @@ -461,6 +310,11 @@ fn main() { match mode { "dir" => { + let extensions = submatches.values_of("extensions") + .unwrap() + .filter(|e| !e.is_empty()) + .collect::>(); + debug!("Using extensions: {:?}", extensions); let urls = build_urls(&wordlist_paths[0], url, extensions, append_slash); let total_numbers_of_request = urls.len(); let (tx, rx) = channel::(); @@ -757,6 +611,25 @@ fn main() { } } "fuzz" => { + let csrf_url = match submatches.value_of("csrf-url") { + Some(v) => Some(v.to_owned()), + None => None, + }; + let csrf_regex = match submatches.value_of("csrf-regex") { + Some(v) => Some(v.to_owned()), + None => None, + }; + let csrf_headers: Option> = if submatches.is_present("csrf-header") { + Some( + submatches + .values_of("csrf-header") + .unwrap() + .map(|h| fuzzbuster::utils::split_http_headers(h)) + .collect(), + ) + } else { + None + }; let fuzzbuster = FuzzBuster { n_threads, ignore_certificate, @@ -785,3 +658,137 @@ fn main() { _ => (), } } + +fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("verbose") + .long("verbose") + .short("v") + .multiple(true) + .help("Sets the level of verbosity"), + ) + .arg( + Arg::with_name("no-banner") + .long("no-banner") + .help("Skips initial banner"), + ) + .arg( + Arg::with_name("url") + .long("url") + .alias("domain") + .help("Sets the target URL") + .short("u") + .takes_value(true) + .required(true), + ) + .arg( + Arg::with_name("wordlist") + .long("wordlist") + .help("Sets the wordlist") + .short("w") + .takes_value(true) + .multiple(true) + .use_delimiter(true) + .required(true), + ) + .arg( + Arg::with_name("ignore-string") + .long("ignore-string") + .help("Ignores results with specified string in the HTTP Body") + .short("x") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("include-string") + .long("include-string") + .help("Includes results with specified string in the HTTP body") + .short("i") + .multiple(true) + .conflicts_with("ignore-string") + .takes_value(true), + ) + .arg( + Arg::with_name("include-status-codes") + .long("include-status-codes") + .help("Sets the list of status codes to include") + .short("s") + .default_value("") + .use_delimiter(true), + ) + .arg( + Arg::with_name("ignore-status-codes") + .long("ignore-status-codes") + .help("Sets the list of status codes to ignore") + .short("S") + .default_value("404") + .use_delimiter(true), + ) + .arg( + Arg::with_name("threads") + .long("threads") + .alias("workers") + .help("Sets the amount of concurrent requests") + .short("t") + .default_value("10") + .takes_value(true), + ) + .arg( + Arg::with_name("ignore-certificate") + .long("ignore-certificate") + .alias("no-check-certificate") + .help("Disables TLS certificate validation") + .short("k"), + ) + .arg( + Arg::with_name("exit-on-error") + .long("exit-on-error") + .help("Exits on connection errors") + .short("K"), + ) + .arg( + Arg::with_name("output") + .long("output") + .help("Saves the results in the specified file") + .short("o") + .default_value("") + .takes_value(true), + ) + .arg( + Arg::with_name("no-progress-bar") + .long("no-progress-bar") + .help("Disables the progress bar"), + ) + .arg( + Arg::with_name("http-method") + .long("http-method") + .help("Uses the specified HTTP method") + .short("X") + .default_value("GET") + .takes_value(true), + ) + .arg( + Arg::with_name("http-body") + .long("http-body") + .help("Uses the specified HTTP method") + .short("b") + .default_value("") + .takes_value(true), + ) + .arg( + Arg::with_name("http-header") + .long("http-header") + .help("Appends the specified HTTP header") + .short("H") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("user-agent") + .long("user-agent") + .help("Uses the specified User-Agent") + .short("a") + .default_value("rustbuster") + .takes_value(true), + ) +} \ No newline at end of file From 0ee8a5426c1f80723265ce2518ae402f10a01d57 Mon Sep 17 00:00:00 2001 From: phra Date: Wed, 19 Jun 2019 23:50:48 +0200 Subject: [PATCH 02/17] refactor: add common_args --- src/main.rs | 363 +++++++++++++++++++++++++++++----------------------- 1 file changed, 203 insertions(+), 160 deletions(-) diff --git a/src/main.rs b/src/main.rs index 69aa25c..9ee1006 100644 --- a/src/main.rs +++ b/src/main.rs @@ -120,88 +120,11 @@ fn main() { let mode = matches.subcommand_name().unwrap_or("dir"); let submatches = matches.subcommand_matches(mode).unwrap(); - let domain = submatches.value_of("domain").unwrap_or(""); - let append_slash = submatches.is_present("append-slash"); - let user_agent = submatches.value_of("user-agent").unwrap(); - let http_method = submatches.value_of("http-method").unwrap(); - let http_body = submatches.value_of("http-body").unwrap(); - let url = submatches.value_of("url").unwrap(); - let wordlist_paths = submatches - .values_of("wordlist") - .unwrap() - .map(|w| w.to_owned()) - .collect::>(); - let ignore_certificate = submatches.is_present("ignore-certificate"); - let mut no_banner = submatches.is_present("no-banner"); - let mut no_progress_bar = submatches.is_present("no-progress-bar"); - let exit_on_connection_errors = submatches.is_present("exit-on-error"); - let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { - submatches - .values_of("http-header") - .unwrap() - .map(|h| fuzzbuster::utils::split_http_headers(h)) - .collect() - } else { - Vec::new() - }; - let ignore_strings: Vec = if submatches.is_present("ignore-string") { - submatches - .values_of("ignore-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - let include_strings: Vec = if submatches.is_present("include-string") { - submatches - .values_of("include-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - let n_threads = submatches - .value_of("threads") - .unwrap() - .parse::() - .expect("threads is a number"); - let include_status_codes = submatches - .values_of("include-status-codes") - .unwrap() - .filter(|s| { - if s.is_empty() { - return false; - } - let valid = hyper::StatusCode::from_str(s).is_ok(); - if !valid { - warn!("Ignoring invalid status code for '-s' param: {}", s); - } - valid - }) - .map(|s| s.to_string()) - .collect::>(); - let ignore_status_codes = submatches - .values_of("ignore-status-codes") - .unwrap() - .filter(|s| { - if s.is_empty() { - return false; - } - let valid = hyper::StatusCode::from_str(s).is_ok(); - if !valid { - warn!("Ignoring invalid status code for '-S' param: {}", s); - } - valid - }) - .map(|s| s.to_string()) - .collect::>(); - let output = submatches.value_of("output").unwrap(); + let common_args = extract_common_args(submatches); if mode != "dns" { debug!("mode {}", mode); - match url.parse::() { + match common_args.url.parse::() { Err(e) => { error!( "Invalid URL: {}, consider adding a protocol like http:// or https://", @@ -226,7 +149,7 @@ fn main() { } } - let all_wordlists_exist = wordlist_paths + let all_wordlists_exist = common_args.wordlist_paths .iter() .map(|wordlist_path| { if std::fs::metadata(wordlist_path).is_err() { @@ -243,25 +166,24 @@ fn main() { } debug!("Using mode: {:?}", mode); - debug!("Using url: {:?}", url); - debug!("Using wordlist: {:?}", wordlist_paths); - debug!("Using mode: {:?}", mode); - debug!("Using concurrent requests: {:?}", n_threads); - debug!("Using certificate validation: {:?}", !ignore_certificate); - debug!("Using HTTP headers: {:?}", http_headers); + debug!("Using url: {:?}", common_args.url); + debug!("Using wordlist: {:?}", common_args.wordlist_paths); + debug!("Using concurrent requests: {:?}", common_args.n_threads); + debug!("Using certificate validation: {:?}", !common_args.ignore_certificate); + debug!("Using HTTP headers: {:?}", common_args.http_headers); debug!( "Using exit on connection errors: {:?}", - exit_on_connection_errors + common_args.exit_on_connection_errors ); debug!( "Including status codes: {}", - if include_status_codes.is_empty() { + if common_args.include_status_codes.is_empty() { String::from("ALL") } else { - format!("{:?}", include_status_codes) + format!("{:?}", common_args.include_status_codes) } ); - debug!("Excluding status codes: {:?}", ignore_status_codes); + debug!("Excluding status codes: {:?}", common_args.ignore_status_codes); // Vary the output based on how many times the user used the "verbose" flag // (i.e. 'myprog -v -v -v' or 'myprog -vvv' vs 'myprog -v' @@ -274,23 +196,7 @@ fn main() { println!("{}", banner::copyright()); - if let Some((Width(w), Height(h))) = terminal_size() { - if w < 122 { - no_banner = true; - } - - if w < 104 { - warn!("Your terminal is {} cols wide and {} lines tall", w, h); - warn!("Disabling progress bar, minimum cols: 104"); - no_progress_bar = true; - } - } else { - warn!("Unable to get terminal size"); - no_banner = true; - no_progress_bar = true; - } - - if !no_banner { + if !common_args.no_banner { println!("{}", banner::generate()); } @@ -298,9 +204,9 @@ fn main() { "{}", banner::configuration( mode, - url, + &common_args.url, submatches.value_of("threads").unwrap(), - &wordlist_paths[0] + &common_args.wordlist_paths[0] ) ); println!("{}", banner::starting_time()); @@ -310,28 +216,29 @@ fn main() { match mode { "dir" => { + let append_slash = submatches.is_present("append-slash"); let extensions = submatches.values_of("extensions") .unwrap() .filter(|e| !e.is_empty()) .collect::>(); debug!("Using extensions: {:?}", extensions); - let urls = build_urls(&wordlist_paths[0], url, extensions, append_slash); + let urls = build_urls(&common_args.wordlist_paths[0], &common_args.url, extensions, append_slash); let total_numbers_of_request = urls.len(); let (tx, rx) = channel::(); let config = DirConfig { - n_threads, - ignore_certificate, - http_method: http_method.to_owned(), - http_body: http_body.to_owned(), - user_agent: user_agent.to_owned(), - http_headers, + n_threads: common_args.n_threads, + ignore_certificate: common_args.ignore_certificate, + http_method: common_args.http_method.to_owned(), + http_body: common_args.http_body.to_owned(), + user_agent: common_args.user_agent.to_owned(), + http_headers: common_args.http_headers.clone(), }; let rp_config = ResultProcessorConfig { - include: include_status_codes, - ignore: ignore_status_codes, + include: common_args.include_status_codes, + ignore: common_args.ignore_status_codes, }; let mut result_processor = ScanResult::new(rp_config); - let bar = if no_progress_bar { + let bar = if common_args.no_progress_bar { ProgressBar::hidden() } else { ProgressBar::new(total_numbers_of_request as u64) @@ -367,7 +274,7 @@ fn main() { match &msg.error { Some(e) => { error!("{:?}", e); - if current_numbers_of_request == 1 || exit_on_connection_errors { + if current_numbers_of_request == 1 || common_args.exit_on_connection_errors { warn!("Check connectivity to the target"); break; } @@ -391,7 +298,7 @@ fn main() { _ => 0, }; - if no_progress_bar { + if common_args.no_progress_bar { println!( "{}\t{}{}{}{}", msg.method, @@ -416,18 +323,18 @@ fn main() { bar.finish(); println!("{}", banner::ending_time()); - if !output.is_empty() { - save_dir_results(output, &result_processor.results); + if !common_args.output.is_empty() { + save_dir_results(&common_args.output, &result_processor.results); } } "dns" => { - let domains = build_domains(&wordlist_paths[0], url); + let domains = build_domains(&common_args.wordlist_paths[0], &common_args.url); let total_numbers_of_request = domains.len(); let (tx, rx) = channel::(); - let config = DnsConfig { n_threads }; + let config = DnsConfig { n_threads: common_args.n_threads }; let mut result_processor = DnsScanResult::new(); - let bar = if no_progress_bar { + let bar = if common_args.no_progress_bar { ProgressBar::hidden() } else { ProgressBar::new(total_numbers_of_request as u64) @@ -464,7 +371,7 @@ fn main() { result_processor.maybe_add_result(msg.clone()); match msg.status { true => { - if no_progress_bar { + if common_args.no_progress_bar { println!("OK\t{}", &msg.domain[..msg.domain.len() - 3]); } else { bar.println(format!("OK\t{}", &msg.domain[..msg.domain.len() - 3])); @@ -476,14 +383,14 @@ fn main() { let string_repr = addr.ip().to_string(); match addr.is_ipv4() { true => { - if no_progress_bar { + if common_args.no_progress_bar { println!("\t\tIPv4: {}", string_repr); } else { bar.println(format!("\t\tIPv4: {}", string_repr)); } } false => { - if no_progress_bar { + if common_args.no_progress_bar { println!("\t\tIPv6: {}", string_repr); } else { bar.println(format!("\t\tIPv6: {}", string_repr)); @@ -502,34 +409,34 @@ fn main() { bar.finish(); println!("{}", banner::ending_time()); - if !output.is_empty() { - save_dns_results(output, &result_processor.results); + if !common_args.output.is_empty() { + save_dns_results(&common_args.output, &result_processor.results); } } "vhost" => { - if domain.is_empty() { + if common_args.domain.is_empty() { error!("domain not specified (-d)"); return; } - if ignore_strings.is_empty() { + if common_args.ignore_strings.is_empty() { error!("ignore_strings not specified (-x)"); return; } - let vhosts = build_vhosts(&wordlist_paths[0], domain); + let vhosts = build_vhosts(&common_args.wordlist_paths[0], &common_args.domain); let total_numbers_of_request = vhosts.len(); let (tx, rx) = channel::(); let config = VhostConfig { - n_threads, - ignore_certificate, - http_method: http_method.to_owned(), - user_agent: user_agent.to_owned(), - ignore_strings, - original_url: url.to_owned(), + n_threads: common_args.n_threads, + ignore_certificate: common_args.ignore_certificate, + http_method: common_args.http_method.to_owned(), + user_agent: common_args.user_agent.to_owned(), + ignore_strings: common_args.ignore_strings, + original_url: common_args.url.to_owned(), }; let mut result_processor = VhostScanResult::new(); - let bar = if no_progress_bar { + let bar = if common_args.no_progress_bar { ProgressBar::hidden() } else { ProgressBar::new(total_numbers_of_request as u64) @@ -565,7 +472,7 @@ fn main() { match &msg.error { Some(e) => { error!("{:?}", e); - if current_numbers_of_request == 1 || exit_on_connection_errors { + if current_numbers_of_request == 1 || common_args.exit_on_connection_errors { warn!("Check connectivity to the target"); break; } @@ -583,7 +490,7 @@ fn main() { if !msg.ignored { result_processor.maybe_add_result(msg.clone()); - if no_progress_bar { + if common_args.no_progress_bar { println!( "{}\t{}{}{}", msg.method, @@ -606,8 +513,8 @@ fn main() { bar.finish(); println!("{}", banner::ending_time()); - if !output.is_empty() { - save_vhost_results(output, &result_processor.results); + if !common_args.output.is_empty() { + save_vhost_results(&common_args.output, &result_processor.results); } } "fuzz" => { @@ -631,21 +538,21 @@ fn main() { None }; let fuzzbuster = FuzzBuster { - n_threads, - ignore_certificate, - http_method: http_method.to_owned(), - http_body: http_body.to_owned(), - user_agent: user_agent.to_owned(), - http_headers, - wordlist_paths, - url: url.to_owned(), - ignore_status_codes, - include_status_codes, - no_progress_bar, - exit_on_connection_errors, - output: output.to_owned(), - include_body: include_strings, - ignore_body: ignore_strings, + n_threads: common_args.n_threads, + ignore_certificate: common_args.ignore_certificate, + http_method: common_args.http_method.to_owned(), + http_body: common_args.http_body.to_owned(), + user_agent: common_args.user_agent.to_owned(), + http_headers: common_args.http_headers, + wordlist_paths: common_args.wordlist_paths, + url: common_args.url.to_owned(), + ignore_status_codes: common_args.ignore_status_codes, + include_status_codes: common_args.include_status_codes, + no_progress_bar: common_args.no_progress_bar, + exit_on_connection_errors: common_args.exit_on_connection_errors, + output: common_args.output.to_owned(), + include_body: common_args.include_strings, + ignore_body: common_args.ignore_strings, csrf_url, csrf_regex, csrf_headers, @@ -791,4 +698,140 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .default_value("rustbuster") .takes_value(true), ) +} + +struct CommonArgs { + domain: String, + user_agent: String, + http_method: String, + http_body: String, + url: String, + wordlist_paths: Vec, + ignore_certificate: bool, + no_banner: bool, + no_progress_bar: bool, + exit_on_connection_errors: bool, + n_threads: usize, + http_headers: Vec<(String, String)>, + include_strings: Vec, + ignore_strings: Vec, + include_status_codes: Vec, + ignore_status_codes: Vec, + output: String, +} + +fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { + let domain = submatches.value_of("domain").unwrap_or(""); + let user_agent = submatches.value_of("user-agent").unwrap(); + let http_method = submatches.value_of("http-method").unwrap(); + let http_body = submatches.value_of("http-body").unwrap(); + let url = submatches.value_of("url").unwrap(); + let wordlist_paths = submatches + .values_of("wordlist") + .unwrap() + .map(|w| w.to_owned()) + .collect::>(); + let ignore_certificate = submatches.is_present("ignore-certificate"); + let mut no_banner = submatches.is_present("no-banner"); + let mut no_progress_bar = submatches.is_present("no-progress-bar"); + let exit_on_connection_errors = submatches.is_present("exit-on-error"); + let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { + submatches + .values_of("http-header") + .unwrap() + .map(|h| fuzzbuster::utils::split_http_headers(h)) + .collect() + } else { + Vec::new() + }; + let ignore_strings: Vec = if submatches.is_present("ignore-string") { + submatches + .values_of("ignore-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + let include_strings: Vec = if submatches.is_present("include-string") { + submatches + .values_of("include-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + let n_threads = submatches + .value_of("threads") + .unwrap() + .parse::() + .expect("threads is a number"); + let include_status_codes = submatches + .values_of("include-status-codes") + .unwrap() + .filter(|s| { + if s.is_empty() { + return false; + } + let valid = hyper::StatusCode::from_str(s).is_ok(); + if !valid { + warn!("Ignoring invalid status code for '-s' param: {}", s); + } + valid + }) + .map(|s| s.to_string()) + .collect::>(); + let ignore_status_codes = submatches + .values_of("ignore-status-codes") + .unwrap() + .filter(|s| { + if s.is_empty() { + return false; + } + let valid = hyper::StatusCode::from_str(s).is_ok(); + if !valid { + warn!("Ignoring invalid status code for '-S' param: {}", s); + } + valid + }) + .map(|s| s.to_string()) + .collect::>(); + let output = submatches.value_of("output").unwrap(); + + if let Some((Width(w), Height(h))) = terminal_size() { + if w < 122 { + no_banner = true; + } + + if w < 104 { + warn!("Your terminal is {} cols wide and {} lines tall", w, h); + warn!("Disabling progress bar, minimum cols: 104"); + no_progress_bar = true; + } + } else { + warn!("Unable to get terminal size"); + no_banner = true; + no_progress_bar = true; + } + + CommonArgs { + domain: domain.to_owned(), + user_agent: user_agent.to_owned(), + http_method: http_method.to_owned(), + http_body: http_body.to_owned(), + url: url.to_owned(), + wordlist_paths, + ignore_certificate, + no_banner, + no_progress_bar, + exit_on_connection_errors, + n_threads, + http_headers, + include_strings, + ignore_strings, + include_status_codes, + ignore_status_codes, + output: output.to_owned() + } } \ No newline at end of file From c8f91185e4152338f9dfb9cef1675f745f7e955d Mon Sep 17 00:00:00 2001 From: phra Date: Wed, 19 Jun 2019 23:51:03 +0200 Subject: [PATCH 03/17] style: cargo fmt --- src/main.rs | 84 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9ee1006..4ed6234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,24 +132,27 @@ fn main() { ); return; } - Ok(v) => match v.scheme_part() { - Some(s) => { - if s != "http" && s != "https" { - error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); - return; + Ok(v) => { + match v.scheme_part() { + Some(s) => { + if s != "http" && s != "https" { + error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); + return; + } } - } - None => { - if mode != "dns" { - error!("Invalid URL: missing protocol, consider adding http:// or https://"); - return; + None => { + if mode != "dns" { + error!("Invalid URL: missing protocol, consider adding http:// or https://"); + return; + } } } - }, + } } } - let all_wordlists_exist = common_args.wordlist_paths + let all_wordlists_exist = common_args + .wordlist_paths .iter() .map(|wordlist_path| { if std::fs::metadata(wordlist_path).is_err() { @@ -169,7 +172,10 @@ fn main() { debug!("Using url: {:?}", common_args.url); debug!("Using wordlist: {:?}", common_args.wordlist_paths); debug!("Using concurrent requests: {:?}", common_args.n_threads); - debug!("Using certificate validation: {:?}", !common_args.ignore_certificate); + debug!( + "Using certificate validation: {:?}", + !common_args.ignore_certificate + ); debug!("Using HTTP headers: {:?}", common_args.http_headers); debug!( "Using exit on connection errors: {:?}", @@ -183,7 +189,10 @@ fn main() { format!("{:?}", common_args.include_status_codes) } ); - debug!("Excluding status codes: {:?}", common_args.ignore_status_codes); + debug!( + "Excluding status codes: {:?}", + common_args.ignore_status_codes + ); // Vary the output based on how many times the user used the "verbose" flag // (i.e. 'myprog -v -v -v' or 'myprog -vvv' vs 'myprog -v' @@ -217,12 +226,18 @@ fn main() { match mode { "dir" => { let append_slash = submatches.is_present("append-slash"); - let extensions = submatches.values_of("extensions") + let extensions = submatches + .values_of("extensions") .unwrap() .filter(|e| !e.is_empty()) .collect::>(); debug!("Using extensions: {:?}", extensions); - let urls = build_urls(&common_args.wordlist_paths[0], &common_args.url, extensions, append_slash); + let urls = build_urls( + &common_args.wordlist_paths[0], + &common_args.url, + extensions, + append_slash, + ); let total_numbers_of_request = urls.len(); let (tx, rx) = channel::(); let config = DirConfig { @@ -274,7 +289,8 @@ fn main() { match &msg.error { Some(e) => { error!("{:?}", e); - if current_numbers_of_request == 1 || common_args.exit_on_connection_errors { + if current_numbers_of_request == 1 || common_args.exit_on_connection_errors + { warn!("Check connectivity to the target"); break; } @@ -331,7 +347,9 @@ fn main() { let domains = build_domains(&common_args.wordlist_paths[0], &common_args.url); let total_numbers_of_request = domains.len(); let (tx, rx) = channel::(); - let config = DnsConfig { n_threads: common_args.n_threads }; + let config = DnsConfig { + n_threads: common_args.n_threads, + }; let mut result_processor = DnsScanResult::new(); let bar = if common_args.no_progress_bar { @@ -472,7 +490,8 @@ fn main() { match &msg.error { Some(e) => { error!("{:?}", e); - if current_numbers_of_request == 1 || common_args.exit_on_connection_errors { + if current_numbers_of_request == 1 || common_args.exit_on_connection_errors + { warn!("Check connectivity to the target"); break; } @@ -526,17 +545,18 @@ fn main() { Some(v) => Some(v.to_owned()), None => None, }; - let csrf_headers: Option> = if submatches.is_present("csrf-header") { - Some( - submatches - .values_of("csrf-header") - .unwrap() - .map(|h| fuzzbuster::utils::split_http_headers(h)) - .collect(), - ) - } else { - None - }; + let csrf_headers: Option> = + if submatches.is_present("csrf-header") { + Some( + submatches + .values_of("csrf-header") + .unwrap() + .map(|h| fuzzbuster::utils::split_http_headers(h)) + .collect(), + ) + } else { + None + }; let fuzzbuster = FuzzBuster { n_threads: common_args.n_threads, ignore_certificate: common_args.ignore_certificate, @@ -832,6 +852,6 @@ fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { ignore_strings, include_status_codes, ignore_status_codes, - output: output.to_owned() + output: output.to_owned(), } -} \ No newline at end of file +} From 8f990c8179316c297c8fe88d970c16854e4a92da Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 11:37:37 +0200 Subject: [PATCH 04/17] fix: print usage if no command was provided --- src/main.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4ed6234..4e369bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -49,13 +49,13 @@ fn main() { .about("DirBuster for rust") .after_help("EXAMPLES: 1. Dir mode: - rustbuster -m dir -u http://localhost:3000/ -w examples/wordlist -e php + rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php 2. Dns mode: - rustbuster -m dns -u google.com -w examples/wordlist + rustbuster dns -u google.com -w examples/wordlist 3. Vhost mode: - rustbuster -m vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x \"Hello\" + rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x \"Hello\" 4. Fuzz mode: - rustbuster -m fuzz -u http://localhost:3000/login \\ + rustbuster fuzz -u http://localhost:3000/login \\ -X POST \\ -H \"Content-Type: application/json\" \\ -b '{\"user\":\"FUZZ\",\"password\":\"FUZZ\",\"csrf\":\"CSRFCSRF\"}' \\ @@ -90,6 +90,7 @@ fn main() { .long("domain") .help("Uses the specified domain to bruteforce") .short("d") + .required(true) .takes_value(true), )) .subcommand(set_common_args(SubCommand::with_name("fuzz")) @@ -119,7 +120,14 @@ fn main() { .get_matches(); let mode = matches.subcommand_name().unwrap_or("dir"); - let submatches = matches.subcommand_matches(mode).unwrap(); + let submatches = match matches.subcommand_matches(mode) { + Some(v) => v, + None => { + println!("{}", matches.usage()); + return; + } + }; + let common_args = extract_common_args(submatches); if mode != "dns" { From 6a5f49dddffb0f43bb172feffd8af05615fa9c59 Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 12:58:53 +0200 Subject: [PATCH 05/17] wip: update README with subcommands --- README.md | 83 ++++++++++++++++++++++++------------------------------- 1 file changed, 36 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index da88deb..0f89104 100644 --- a/README.md +++ b/README.md @@ -11,63 +11,32 @@ You can download prebuilt binaries from [here](https://github.com/phra/rustbuste ## Usage ```shell - - _ _ _ _ _ _ _ _ _ _ - /\ \ /\_\ / /\ /\ \ / /\ /\_\ / /\ /\ \ /\ \ /\ \ - / \ \/ / / _ / / \ \_\ \ / / \ / / / _ / / \ \_\ \ / \ \ / \ \ - / /\ \ \ \ \__ /\_\/ / /\ \__ /\__ \ / / /\ \ \ \ \__ /\_\/ / /\ \__ /\__ \ / /\ \ \ / /\ \ \ - / / /\ \_\ \___\ / / / / /\ \___\/ /_ \ \ / / /\ \ \ \ \___\ / / / / /\ \___\/ /_ \ \ / / /\ \_\ / / /\ \_\ - / / /_/ / /\__ / / / /\ \ \ \/___/ / /\ \ \/ / /\ \_\ \ \__ / / / /\ \ \ \/___/ / /\ \ \/ /_/_ \/_/ / / /_/ / / - / / /__\/ / / / / / / / \ \ \ / / / \/_/ / /\ \ \___\ / / / / / / \ \ \ / / / \/_/ /____/\ / / /__\/ / - / / /_____/ / / / / / _ \ \ \ / / / / / / \ \ \__// / / / / _ \ \ \ / / / / /\____\/ / / /_____/ - / / /\ \ \ / / /___/ / /_/\__/ / / / / / / / /____\_\ \ / / /___/ / /_/\__/ / / / / / / / /______ / / /\ \ \ -/ / / \ \ \/ / /____\/ /\ \/___/ / /_/ / / / /__________/ / /____\/ /\ \/___/ / /_/ / / / /_______/ / / \ \ \ -\/_/ \_\/\/_________/ \_____\/ \_\/ \/_____________\/_________/ \_____\/ \_\/ \/__________\/_/ \_\/ - -~ rustbuster v. 2.0.2 ~ by phra & ps1dr3x ~ +rustbuster 2.1.0 +DirBuster for rust USAGE: - rustbuster [FLAGS] [OPTIONS] --url --wordlist ... + rustbuster [SUBCOMMAND] FLAGS: - -f, --append-slash Tries to also append / to the base request - -K, --exit-on-error Exits on connection errors - -h, --help Prints help information - -k, --ignore-certificate Disables TLS certificate validation - --no-banner Skips initial banner - --no-progress-bar Disables the progress bar - -V, --version Prints version information - -v, --verbose Sets the level of verbosity - -OPTIONS: - --csrf-header ... Adds the specified headers to CSRF GET request - --csrf-regex Grabs the CSRF token applying the specified RegEx - --csrf-url Grabs the CSRF token via GET to csrf-url - -d, --domain Uses the specified domain - -e, --extensions Sets the extensions [default: ] - -b, --http-body Uses the specified HTTP method [default: ] - -H, --http-header ... Appends the specified HTTP header - -X, --http-method Uses the specified HTTP method [default: GET] - -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] - -x, --ignore-string ... Ignores results with specified string in the HTTP Body - -s, --include-status-codes Sets the list of status codes to include [default: ] - -i, --include-string ... Includes results with specified string in the HTTP body - -m, --mode Sets the mode of operation (dir, dns, fuzz) [default: dir] - -o, --output Saves the results in the specified file [default: ] - -t, --threads Sets the amount of concurrent requests [default: 10] - -u, --url Sets the target URL - -a, --user-agent Uses the specified User-Agent [default: rustbuster] - -w, --wordlist ... Sets the wordlist + -h, --help Prints help information + -V, --version Prints version information + +SUBCOMMANDS: + dir Directories and files enumeration mode + dns A/AAAA entries enumeration mode + fuzz Custom fuzzing enumeration mode + help Prints this message or the help of the given subcommand(s) + vhost Virtual hosts enumeration mode EXAMPLES: 1. Dir mode: - rustbuster -m dir -u http://localhost:3000/ -w examples/wordlist -e php + rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php 2. Dns mode: - rustbuster -m dns -u google.com -w examples/wordlist + rustbuster dns -u google.com -w examples/wordlist 3. Vhost mode: - rustbuster -m vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x "Hello" + rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x "Hello" 4. Fuzz mode: - rustbuster -m fuzz -u http://localhost:3000/login \ + rustbuster fuzz -u http://localhost:3000/login \ -X POST \ -H "Content-Type: application/json" \ -b '{"user":"FUZZ","password":"FUZZ","csrf":"CSRFCSRF"}' \ @@ -78,3 +47,23 @@ EXAMPLES: --csrf-regex '\{"csrf":"(\w+)"\}' ``` + +### `dir` usage + +```shell +``` + +### `dns` usage + +```shell +``` + +### `vhost` usage + +```shell +``` + +#### `fuzz` usage + +```shell +``` From 8c2206e8a25dd8088deb65036905334aea3c9365 Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 12:59:24 +0200 Subject: [PATCH 06/17] wip: refactor args parsing --- src/main.rs | 277 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 162 insertions(+), 115 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4e369bc..ac950c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -74,15 +74,19 @@ fn main() { .short("e") .default_value("") .use_delimiter(true), - ) - .arg( - Arg::with_name("append-slash") - .long("append-slash") - .help("Tries to also append / to the base request") - .short("f"), - )) + ) + .arg( + Arg::with_name("append-slash") + .long("append-slash") + .help("Tries to also append / to the base request") + .short("f"), + ) + .after_help("EXAMPLE: + rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php")) .subcommand(set_common_args(SubCommand::with_name("dns")) - .about("A/AAAA entries enumeration mode")) + .about("A/AAAA entries enumeration mode") + .after_help("EXAMPLE: + rustbuster dns -u google.com -w examples/wordlist")) .subcommand(set_common_args(SubCommand::with_name("vhost")) .about("Virtual hosts enumeration mode") .arg( @@ -92,7 +96,9 @@ fn main() { .short("d") .required(true) .takes_value(true), - )) + ) + .after_help("EXAMPLE: + rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x \"Hello\"")) .subcommand(set_common_args(SubCommand::with_name("fuzz")) .about("Custom fuzzing enumeration mode") .arg( @@ -116,7 +122,17 @@ fn main() { .requires("csrf-url") .multiple(true) .takes_value(true), - )) + ) + .after_help("EXAMPLE: + rustbuster fuzz -u http://localhost:3000/login \\ + -X POST \\ + -H \"Content-Type: application/json\" \\ + -b '{\"user\":\"FUZZ\",\"password\":\"FUZZ\",\"csrf\":\"CSRFCSRF\"}' \\ + -w examples/wordlist \\ + -w /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt \\ + -s 200 \\ + --csrf-url \"http://localhost:3000/csrf\" \\ + --csrf-regex '\\{\"csrf\":\"(\\w+)\"\\}'")) .get_matches(); let mode = matches.subcommand_name().unwrap_or("dir"); @@ -627,23 +643,37 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .required(true), ) .arg( - Arg::with_name("ignore-string") - .long("ignore-string") - .help("Ignores results with specified string in the HTTP Body") - .short("x") - .multiple(true) + Arg::with_name("threads") + .long("threads") + .alias("workers") + .help("Sets the amount of concurrent requests") + .short("t") + .default_value("10") .takes_value(true), ) .arg( - Arg::with_name("include-string") - .long("include-string") - .help("Includes results with specified string in the HTTP body") - .short("i") - .multiple(true) - .conflicts_with("ignore-string") + Arg::with_name("exit-on-error") + .long("exit-on-error") + .help("Exits on connection errors") + .short("K"), + ) + .arg( + Arg::with_name("output") + .long("output") + .help("Saves the results in the specified file") + .short("o") + .default_value("") .takes_value(true), ) .arg( + Arg::with_name("no-progress-bar") + .long("no-progress-bar") + .help("Disables the progress bar"), + ) +} + +fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( Arg::with_name("include-status-codes") .long("include-status-codes") .help("Sets the list of status codes to include") @@ -660,12 +690,11 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .use_delimiter(true), ) .arg( - Arg::with_name("threads") - .long("threads") - .alias("workers") - .help("Sets the amount of concurrent requests") - .short("t") - .default_value("10") + Arg::with_name("user-agent") + .long("user-agent") + .help("Uses the specified User-Agent") + .short("a") + .default_value("rustbuster") .takes_value(true), ) .arg( @@ -675,25 +704,6 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .help("Disables TLS certificate validation") .short("k"), ) - .arg( - Arg::with_name("exit-on-error") - .long("exit-on-error") - .help("Exits on connection errors") - .short("K"), - ) - .arg( - Arg::with_name("output") - .long("output") - .help("Saves the results in the specified file") - .short("o") - .default_value("") - .takes_value(true), - ) - .arg( - Arg::with_name("no-progress-bar") - .long("no-progress-bar") - .help("Disables the progress bar"), - ) .arg( Arg::with_name("http-method") .long("http-method") @@ -719,50 +729,102 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .takes_value(true), ) .arg( - Arg::with_name("user-agent") - .long("user-agent") - .help("Uses the specified User-Agent") - .short("a") - .default_value("rustbuster") + Arg::with_name("ignore-string") + .long("ignore-string") + .help("Ignores results with specified string in the HTTP Body") + .short("x") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("include-string") + .long("include-string") + .help("Includes results with specified string in the HTTP body") + .short("i") + .multiple(true) + .conflicts_with("ignore-string") .takes_value(true), ) } struct CommonArgs { + wordlist_paths: Vec, + no_banner: bool, + no_progress_bar: bool, + exit_on_connection_errors: bool, + n_threads: usize, + output: String, +} + +struct DNSArgs { domain: String, +} + +struct HTTPArgs { user_agent: String, http_method: String, http_body: String, url: String, - wordlist_paths: Vec, ignore_certificate: bool, - no_banner: bool, - no_progress_bar: bool, - exit_on_connection_errors: bool, - n_threads: usize, http_headers: Vec<(String, String)>, - include_strings: Vec, - ignore_strings: Vec, include_status_codes: Vec, ignore_status_codes: Vec, - output: String, +} + +struct BodyArgs { + include_strings: Vec, + ignore_strings: Vec, } fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { - let domain = submatches.value_of("domain").unwrap_or(""); - let user_agent = submatches.value_of("user-agent").unwrap(); - let http_method = submatches.value_of("http-method").unwrap(); - let http_body = submatches.value_of("http-body").unwrap(); - let url = submatches.value_of("url").unwrap(); let wordlist_paths = submatches .values_of("wordlist") .unwrap() .map(|w| w.to_owned()) .collect::>(); - let ignore_certificate = submatches.is_present("ignore-certificate"); let mut no_banner = submatches.is_present("no-banner"); let mut no_progress_bar = submatches.is_present("no-progress-bar"); let exit_on_connection_errors = submatches.is_present("exit-on-error"); + let n_threads = submatches + .value_of("threads") + .unwrap() + .parse::() + .expect("threads is a number"); + + let output = submatches.value_of("output").unwrap(); + + if let Some((Width(w), Height(h))) = terminal_size() { + if w < 122 { + no_banner = true; + } + + if w < 104 { + warn!("Your terminal is {} cols wide and {} lines tall", w, h); + warn!("Disabling progress bar, minimum cols: 104"); + no_progress_bar = true; + } + } else { + warn!("Unable to get terminal size"); + no_banner = true; + no_progress_bar = true; + } + + CommonArgs { + wordlist_paths, + no_banner, + no_progress_bar, + exit_on_connection_errors, + n_threads, + output: output.to_owned(), + } +} + +fn extract_http_args<'a>(submatches: &clap::ArgMatches<'a>) -> HTTPArgs { + let user_agent = submatches.value_of("user-agent").unwrap(); + let http_method = submatches.value_of("http-method").unwrap(); + let http_body = submatches.value_of("http-body").unwrap(); + let url = submatches.value_of("url").unwrap(); + let ignore_certificate = submatches.is_present("ignore-certificate"); let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { submatches .values_of("http-header") @@ -772,29 +834,6 @@ fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { } else { Vec::new() }; - let ignore_strings: Vec = if submatches.is_present("ignore-string") { - submatches - .values_of("ignore-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - let include_strings: Vec = if submatches.is_present("include-string") { - submatches - .values_of("include-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - let n_threads = submatches - .value_of("threads") - .unwrap() - .parse::() - .expect("threads is a number"); let include_status_codes = submatches .values_of("include-status-codes") .unwrap() @@ -825,41 +864,49 @@ fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { }) .map(|s| s.to_string()) .collect::>(); - let output = submatches.value_of("output").unwrap(); - - if let Some((Width(w), Height(h))) = terminal_size() { - if w < 122 { - no_banner = true; - } - - if w < 104 { - warn!("Your terminal is {} cols wide and {} lines tall", w, h); - warn!("Disabling progress bar, minimum cols: 104"); - no_progress_bar = true; - } - } else { - warn!("Unable to get terminal size"); - no_banner = true; - no_progress_bar = true; - } - CommonArgs { - domain: domain.to_owned(), + HTTPArgs { user_agent: user_agent.to_owned(), http_method: http_method.to_owned(), http_body: http_body.to_owned(), url: url.to_owned(), - wordlist_paths, ignore_certificate, - no_banner, - no_progress_bar, - exit_on_connection_errors, - n_threads, http_headers, - include_strings, - ignore_strings, include_status_codes, ignore_status_codes, - output: output.to_owned(), + } +} + +fn extract_dns_args<'a>(submatches: &clap::ArgMatches<'a>) -> DNSArgs { + let domain = submatches.value_of("domain").unwrap_or(""); + + DNSArgs { + domain: domain.to_owned(), + } +} + +fn extract_body_args<'a>(submatches: &clap::ArgMatches<'a>) -> BodyArgs { + let ignore_strings: Vec = if submatches.is_present("ignore-string") { + submatches + .values_of("ignore-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + let include_strings: Vec = if submatches.is_present("include-string") { + submatches + .values_of("include-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + + BodyArgs { + include_strings, + ignore_strings, } } From 3a92b9090ecf1242b790fdcbd3b20649538cce76 Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 12:59:43 +0200 Subject: [PATCH 07/17] build: add .vscode debug config --- .vscode/launch.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fcac372 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'rustbuster'", + "cargo": { + "args": [ + "build", + "--bin=rustbuster", + "--package=rustbuster" + ], + "filter": { + "name": "rustbuster", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug unit tests in executable 'rustbuster'", + "cargo": { + "args": [ + "test", + "--no-run", + "--bin=rustbuster", + "--package=rustbuster" + ], + "filter": { + "name": "rustbuster", + "kind": "bin" + } + }, + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file From 096af9e874b266dd2ec87b87db6c11a1f9810f19 Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 23:14:02 +0200 Subject: [PATCH 08/17] refactor: args parsing --- src/main.rs | 96 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/src/main.rs b/src/main.rs index ac950c0..c62d2db 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,7 @@ fn main() { --csrf-url \"http://localhost:3000/csrf\" \\ --csrf-regex '\\{\"csrf\":\"(\\w+)\"\\}' ") - .subcommand(set_common_args(SubCommand::with_name("dir")) + .subcommand(set_http_args(set_common_args(SubCommand::with_name("dir"))) .about("Directories and files enumeration mode") .arg( Arg::with_name("extensions") @@ -83,11 +83,11 @@ fn main() { ) .after_help("EXAMPLE: rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php")) - .subcommand(set_common_args(SubCommand::with_name("dns")) + .subcommand(set_dns_args(set_common_args(SubCommand::with_name("dns"))) .about("A/AAAA entries enumeration mode") .after_help("EXAMPLE: rustbuster dns -u google.com -w examples/wordlist")) - .subcommand(set_common_args(SubCommand::with_name("vhost")) + .subcommand(set_body_args(set_http_args(set_common_args(SubCommand::with_name("vhost")))) .about("Virtual hosts enumeration mode") .arg( Arg::with_name("domain") @@ -99,7 +99,7 @@ fn main() { ) .after_help("EXAMPLE: rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x \"Hello\"")) - .subcommand(set_common_args(SubCommand::with_name("fuzz")) + .subcommand(set_body_args(set_http_args(set_common_args(SubCommand::with_name("fuzz")))) .about("Custom fuzzing enumeration mode") .arg( Arg::with_name("csrf-url") @@ -145,10 +145,13 @@ fn main() { }; let common_args = extract_common_args(submatches); + let http_args = extract_http_args(submatches); + let dns_args = extract_dns_args(submatches); + let body_args = extract_body_args(submatches); if mode != "dns" { debug!("mode {}", mode); - match common_args.url.parse::() { + match http_args.url.parse::() { Err(e) => { error!( "Invalid URL: {}, consider adding a protocol like http:// or https://", @@ -193,29 +196,29 @@ fn main() { } debug!("Using mode: {:?}", mode); - debug!("Using url: {:?}", common_args.url); + debug!("Using url: {:?}", http_args.url); debug!("Using wordlist: {:?}", common_args.wordlist_paths); debug!("Using concurrent requests: {:?}", common_args.n_threads); debug!( "Using certificate validation: {:?}", - !common_args.ignore_certificate + !http_args.ignore_certificate ); - debug!("Using HTTP headers: {:?}", common_args.http_headers); + debug!("Using HTTP headers: {:?}", http_args.http_headers); debug!( "Using exit on connection errors: {:?}", common_args.exit_on_connection_errors ); debug!( "Including status codes: {}", - if common_args.include_status_codes.is_empty() { + if http_args.include_status_codes.is_empty() { String::from("ALL") } else { - format!("{:?}", common_args.include_status_codes) + format!("{:?}", http_args.include_status_codes) } ); debug!( "Excluding status codes: {:?}", - common_args.ignore_status_codes + http_args.ignore_status_codes ); // Vary the output based on how many times the user used the "verbose" flag @@ -237,7 +240,7 @@ fn main() { "{}", banner::configuration( mode, - &common_args.url, + &http_args.url, submatches.value_of("threads").unwrap(), &common_args.wordlist_paths[0] ) @@ -258,7 +261,7 @@ fn main() { debug!("Using extensions: {:?}", extensions); let urls = build_urls( &common_args.wordlist_paths[0], - &common_args.url, + &http_args.url, extensions, append_slash, ); @@ -266,15 +269,15 @@ fn main() { let (tx, rx) = channel::(); let config = DirConfig { n_threads: common_args.n_threads, - ignore_certificate: common_args.ignore_certificate, - http_method: common_args.http_method.to_owned(), - http_body: common_args.http_body.to_owned(), - user_agent: common_args.user_agent.to_owned(), - http_headers: common_args.http_headers.clone(), + ignore_certificate: http_args.ignore_certificate, + http_method: http_args.http_method.to_owned(), + http_body: http_args.http_body.to_owned(), + user_agent: http_args.user_agent.to_owned(), + http_headers: http_args.http_headers.clone(), }; let rp_config = ResultProcessorConfig { - include: common_args.include_status_codes, - ignore: common_args.ignore_status_codes, + include: http_args.include_status_codes, + ignore: http_args.ignore_status_codes, }; let mut result_processor = ScanResult::new(rp_config); let bar = if common_args.no_progress_bar { @@ -368,7 +371,7 @@ fn main() { } } "dns" => { - let domains = build_domains(&common_args.wordlist_paths[0], &common_args.url); + let domains = build_domains(&common_args.wordlist_paths[0], &http_args.url); let total_numbers_of_request = domains.len(); let (tx, rx) = channel::(); let config = DnsConfig { @@ -456,26 +459,26 @@ fn main() { } } "vhost" => { - if common_args.domain.is_empty() { + if dns_args.domain.is_empty() { error!("domain not specified (-d)"); return; } - if common_args.ignore_strings.is_empty() { + if body_args.ignore_strings.is_empty() { error!("ignore_strings not specified (-x)"); return; } - let vhosts = build_vhosts(&common_args.wordlist_paths[0], &common_args.domain); + let vhosts = build_vhosts(&common_args.wordlist_paths[0], &dns_args.domain); let total_numbers_of_request = vhosts.len(); let (tx, rx) = channel::(); let config = VhostConfig { n_threads: common_args.n_threads, - ignore_certificate: common_args.ignore_certificate, - http_method: common_args.http_method.to_owned(), - user_agent: common_args.user_agent.to_owned(), - ignore_strings: common_args.ignore_strings, - original_url: common_args.url.to_owned(), + ignore_certificate: http_args.ignore_certificate, + http_method: http_args.http_method.to_owned(), + user_agent: http_args.user_agent.to_owned(), + ignore_strings: body_args.ignore_strings, + original_url: http_args.url.to_owned(), }; let mut result_processor = VhostScanResult::new(); let bar = if common_args.no_progress_bar { @@ -583,20 +586,20 @@ fn main() { }; let fuzzbuster = FuzzBuster { n_threads: common_args.n_threads, - ignore_certificate: common_args.ignore_certificate, - http_method: common_args.http_method.to_owned(), - http_body: common_args.http_body.to_owned(), - user_agent: common_args.user_agent.to_owned(), - http_headers: common_args.http_headers, + ignore_certificate: http_args.ignore_certificate, + http_method: http_args.http_method.to_owned(), + http_body: http_args.http_body.to_owned(), + user_agent: http_args.user_agent.to_owned(), + http_headers: http_args.http_headers, wordlist_paths: common_args.wordlist_paths, - url: common_args.url.to_owned(), - ignore_status_codes: common_args.ignore_status_codes, - include_status_codes: common_args.include_status_codes, + url: http_args.url.to_owned(), + ignore_status_codes: http_args.ignore_status_codes, + include_status_codes: http_args.include_status_codes, no_progress_bar: common_args.no_progress_bar, exit_on_connection_errors: common_args.exit_on_connection_errors, output: common_args.output.to_owned(), - include_body: common_args.include_strings, - ignore_body: common_args.ignore_strings, + include_body: body_args.include_strings, + ignore_body: body_args.ignore_strings, csrf_url, csrf_regex, csrf_headers, @@ -728,7 +731,20 @@ fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .multiple(true) .takes_value(true), ) - .arg( +} + +fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("domain") + .long("domain") + .help("Uses the specified domain") + .short("d") + .takes_value(true), + ) +} + +fn set_body_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( Arg::with_name("ignore-string") .long("ignore-string") .help("Ignores results with specified string in the HTTP Body") From 643cbf806ca6837a2bb9026eee2fd50f813e281a Mon Sep 17 00:00:00 2001 From: phra Date: Thu, 20 Jun 2019 23:40:28 +0200 Subject: [PATCH 09/17] refactor: add dir args parsing --- src/dirbuster/utils.rs | 2 +- src/main.rs | 31 ++++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/dirbuster/utils.rs b/src/dirbuster/utils.rs index c46cdd6..5a0253b 100644 --- a/src/dirbuster/utils.rs +++ b/src/dirbuster/utils.rs @@ -5,7 +5,7 @@ use super::result_processor::SingleDirScanResult; pub fn build_urls( wordlist_path: &str, url: &str, - extensions: Vec<&str>, + extensions: Vec, append_slash: bool, ) -> Vec { debug!("building urls"); diff --git a/src/main.rs b/src/main.rs index c62d2db..0b498cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,6 +148,7 @@ fn main() { let http_args = extract_http_args(submatches); let dns_args = extract_dns_args(submatches); let body_args = extract_body_args(submatches); + let dir_args = extract_dir_args(submatches); if mode != "dns" { debug!("mode {}", mode); @@ -252,18 +253,11 @@ fn main() { match mode { "dir" => { - let append_slash = submatches.is_present("append-slash"); - let extensions = submatches - .values_of("extensions") - .unwrap() - .filter(|e| !e.is_empty()) - .collect::>(); - debug!("Using extensions: {:?}", extensions); let urls = build_urls( &common_args.wordlist_paths[0], &http_args.url, - extensions, - append_slash, + dir_args.extensions, + dir_args.append_slash, ); let total_numbers_of_request = urls.len(); let (tx, rx) = channel::(); @@ -792,6 +786,11 @@ struct BodyArgs { ignore_strings: Vec, } +struct DirArgs { + append_slash: bool, + extensions: Vec, +} + fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { let wordlist_paths = submatches .values_of("wordlist") @@ -926,3 +925,17 @@ fn extract_body_args<'a>(submatches: &clap::ArgMatches<'a>) -> BodyArgs { ignore_strings, } } + +fn extract_dir_args<'a>(submatches: &clap::ArgMatches<'a>) -> DirArgs { + let append_slash = submatches.is_present("append-slash"); + let extensions = submatches + .values_of("extensions") + .unwrap() + .filter(|e| !e.is_empty()) + .map(|s| s.to_owned()) + .collect::>(); + DirArgs { + append_slash, + extensions, + } +} From c159c4a1fbd069adada69a5a651d185b0657daa9 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 01:01:06 +0200 Subject: [PATCH 10/17] refactor: improve dir args parsing --- src/main.rs | 142 +++++++++++++++++++++------------------------------- 1 file changed, 58 insertions(+), 84 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0b498cb..932eb8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -145,39 +145,6 @@ fn main() { }; let common_args = extract_common_args(submatches); - let http_args = extract_http_args(submatches); - let dns_args = extract_dns_args(submatches); - let body_args = extract_body_args(submatches); - let dir_args = extract_dir_args(submatches); - - if mode != "dns" { - debug!("mode {}", mode); - match http_args.url.parse::() { - Err(e) => { - error!( - "Invalid URL: {}, consider adding a protocol like http:// or https://", - e - ); - return; - } - Ok(v) => { - match v.scheme_part() { - Some(s) => { - if s != "http" && s != "https" { - error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); - return; - } - } - None => { - if mode != "dns" { - error!("Invalid URL: missing protocol, consider adding http:// or https://"); - return; - } - } - } - } - } - } let all_wordlists_exist = common_args .wordlist_paths @@ -196,34 +163,6 @@ fn main() { return; } - debug!("Using mode: {:?}", mode); - debug!("Using url: {:?}", http_args.url); - debug!("Using wordlist: {:?}", common_args.wordlist_paths); - debug!("Using concurrent requests: {:?}", common_args.n_threads); - debug!( - "Using certificate validation: {:?}", - !http_args.ignore_certificate - ); - debug!("Using HTTP headers: {:?}", http_args.http_headers); - debug!( - "Using exit on connection errors: {:?}", - common_args.exit_on_connection_errors - ); - debug!( - "Including status codes: {}", - if http_args.include_status_codes.is_empty() { - String::from("ALL") - } else { - format!("{:?}", http_args.include_status_codes) - } - ); - debug!( - "Excluding status codes: {:?}", - http_args.ignore_status_codes - ); - - // Vary the output based on how many times the user used the "verbose" flag - // (i.e. 'myprog -v -v -v' or 'myprog -vvv' vs 'myprog -v' match submatches.occurrences_of("verbose") { 0 => trace!("No verbose info"), 1 => trace!("Some verbose info"), @@ -237,15 +176,6 @@ fn main() { println!("{}", banner::generate()); } - println!( - "{}", - banner::configuration( - mode, - &http_args.url, - submatches.value_of("threads").unwrap(), - &common_args.wordlist_paths[0] - ) - ); println!("{}", banner::starting_time()); let mut current_numbers_of_request = 0; @@ -253,6 +183,12 @@ fn main() { match mode { "dir" => { + let http_args = extract_http_args(submatches); + if !url_is_valid(&http_args.url) { + return; + } + + let dir_args = extract_dir_args(submatches); let urls = build_urls( &common_args.wordlist_paths[0], &http_args.url, @@ -365,7 +301,8 @@ fn main() { } } "dns" => { - let domains = build_domains(&common_args.wordlist_paths[0], &http_args.url); + let dns_args = extract_dns_args(submatches); + let domains = build_domains(&common_args.wordlist_paths[0], &dns_args.domain); let total_numbers_of_request = domains.len(); let (tx, rx) = channel::(); let config = DnsConfig { @@ -453,13 +390,15 @@ fn main() { } } "vhost" => { - if dns_args.domain.is_empty() { - error!("domain not specified (-d)"); + let dns_args = extract_dns_args(submatches); + let body_args = extract_body_args(submatches); + if body_args.ignore_strings.is_empty() { + error!("ignore_strings not specified (-x)"); return; } - if body_args.ignore_strings.is_empty() { - error!("ignore_strings not specified (-x)"); + let http_args = extract_http_args(submatches); + if !url_is_valid(&http_args.url) { return; } @@ -558,6 +497,12 @@ fn main() { } } "fuzz" => { + let http_args = extract_http_args(submatches); + if !url_is_valid(&http_args.url) { + return; + } + + let body_args = extract_body_args(submatches); let csrf_url = match submatches.value_of("csrf-url") { Some(v) => Some(v.to_owned()), None => None, @@ -620,15 +565,6 @@ fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .long("no-banner") .help("Skips initial banner"), ) - .arg( - Arg::with_name("url") - .long("url") - .alias("domain") - .help("Sets the target URL") - .short("u") - .takes_value(true) - .required(true), - ) .arg( Arg::with_name("wordlist") .long("wordlist") @@ -725,6 +661,15 @@ fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .multiple(true) .takes_value(true), ) + .arg( + Arg::with_name("url") + .long("url") + .alias("domain") + .help("Sets the target URL") + .short("u") + .takes_value(true) + .required(true), + ) } fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { @@ -733,6 +678,7 @@ fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .long("domain") .help("Uses the specified domain") .short("d") + .required(true) .takes_value(true), ) } @@ -939,3 +885,31 @@ fn extract_dir_args<'a>(submatches: &clap::ArgMatches<'a>) -> DirArgs { extensions, } } + +fn url_is_valid(url: &str) -> bool { + match url.parse::() { + Err(e) => { + error!( + "Invalid URL: {}, consider adding a protocol like http:// or https://", + e + ); + return false; + } + Ok(v) => { + match v.scheme_part() { + Some(s) => { + if s != "http" && s != "https" { + error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); + return false; + } else { + return true; + } + } + None => { + error!("Invalid URL: missing protocol, consider adding http:// or https://"); + return false; + } + } + } + } +} From b49093d0fcb29c947ea1d93234b0f92a8971e0c3 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 01:17:56 +0200 Subject: [PATCH 11/17] refactor: improve args parsing --- src/args.rs | 356 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 368 +--------------------------------------------------- 2 files changed, 359 insertions(+), 365 deletions(-) create mode 100644 src/args.rs diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..0b32f69 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,356 @@ +use clap::{App, Arg}; +use terminal_size::{terminal_size, Height, Width}; + +pub struct CommonArgs { + pub wordlist_paths: Vec, + pub no_banner: bool, + pub no_progress_bar: bool, + pub exit_on_connection_errors: bool, + pub n_threads: usize, + pub output: String, +} + +pub struct DNSArgs { + pub domain: String, +} + +pub struct HTTPArgs { + pub user_agent: String, + pub http_method: String, + pub http_body: String, + pub url: String, + pub ignore_certificate: bool, + pub http_headers: Vec<(String, String)>, + pub include_status_codes: Vec, + pub ignore_status_codes: Vec, +} + +pub struct BodyArgs { + pub include_strings: Vec, + pub ignore_strings: Vec, +} + +pub struct DirArgs { + pub append_slash: bool, + pub extensions: Vec, +} + +pub fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("verbose") + .long("verbose") + .short("v") + .multiple(true) + .help("Sets the level of verbosity"), + ) + .arg( + Arg::with_name("no-banner") + .long("no-banner") + .help("Skips initial banner"), + ) + .arg( + Arg::with_name("wordlist") + .long("wordlist") + .help("Sets the wordlist") + .short("w") + .takes_value(true) + .multiple(true) + .use_delimiter(true) + .required(true), + ) + .arg( + Arg::with_name("threads") + .long("threads") + .alias("workers") + .help("Sets the amount of concurrent requests") + .short("t") + .default_value("10") + .takes_value(true), + ) + .arg( + Arg::with_name("exit-on-error") + .long("exit-on-error") + .help("Exits on connection errors") + .short("K"), + ) + .arg( + Arg::with_name("output") + .long("output") + .help("Saves the results in the specified file") + .short("o") + .default_value("") + .takes_value(true), + ) + .arg( + Arg::with_name("no-progress-bar") + .long("no-progress-bar") + .help("Disables the progress bar"), + ) +} + +pub fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("include-status-codes") + .long("include-status-codes") + .help("Sets the list of status codes to include") + .short("s") + .default_value("") + .use_delimiter(true), + ) + .arg( + Arg::with_name("ignore-status-codes") + .long("ignore-status-codes") + .help("Sets the list of status codes to ignore") + .short("S") + .default_value("404") + .use_delimiter(true), + ) + .arg( + Arg::with_name("user-agent") + .long("user-agent") + .help("Uses the specified User-Agent") + .short("a") + .default_value("rustbuster") + .takes_value(true), + ) + .arg( + Arg::with_name("ignore-certificate") + .long("ignore-certificate") + .alias("no-check-certificate") + .help("Disables TLS certificate validation") + .short("k"), + ) + .arg( + Arg::with_name("http-method") + .long("http-method") + .help("Uses the specified HTTP method") + .short("X") + .default_value("GET") + .takes_value(true), + ) + .arg( + Arg::with_name("http-body") + .long("http-body") + .help("Uses the specified HTTP method") + .short("b") + .default_value("") + .takes_value(true), + ) + .arg( + Arg::with_name("http-header") + .long("http-header") + .help("Appends the specified HTTP header") + .short("H") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("url") + .long("url") + .alias("domain") + .help("Sets the target URL") + .short("u") + .takes_value(true) + .required(true), + ) +} + +pub fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("domain") + .long("domain") + .help("Uses the specified domain") + .short("d") + .required(true) + .takes_value(true), + ) +} + +pub fn set_body_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("ignore-string") + .long("ignore-string") + .help("Ignores results with specified string in the HTTP Body") + .short("x") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("include-string") + .long("include-string") + .help("Includes results with specified string in the HTTP body") + .short("i") + .multiple(true) + .conflicts_with("ignore-string") + .takes_value(true), + ) +} + +pub fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { + let wordlist_paths = submatches + .values_of("wordlist") + .unwrap() + .map(|w| w.to_owned()) + .collect::>(); + let mut no_banner = submatches.is_present("no-banner"); + let mut no_progress_bar = submatches.is_present("no-progress-bar"); + let exit_on_connection_errors = submatches.is_present("exit-on-error"); + let n_threads = submatches + .value_of("threads") + .unwrap() + .parse::() + .expect("threads is a number"); + + let output = submatches.value_of("output").unwrap(); + + if let Some((Width(w), Height(h))) = terminal_size() { + if w < 122 { + no_banner = true; + } + + if w < 104 { + warn!("Your terminal is {} cols wide and {} lines tall", w, h); + warn!("Disabling progress bar, minimum cols: 104"); + no_progress_bar = true; + } + } else { + warn!("Unable to get terminal size"); + no_banner = true; + no_progress_bar = true; + } + + CommonArgs { + wordlist_paths, + no_banner, + no_progress_bar, + exit_on_connection_errors, + n_threads, + output: output.to_owned(), + } +} + +pub fn extract_http_args<'a>(submatches: &clap::ArgMatches<'a>) -> HTTPArgs { + let user_agent = submatches.value_of("user-agent").unwrap(); + let http_method = submatches.value_of("http-method").unwrap(); + let http_body = submatches.value_of("http-body").unwrap(); + let url = submatches.value_of("url").unwrap(); + let ignore_certificate = submatches.is_present("ignore-certificate"); + let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { + submatches + .values_of("http-header") + .unwrap() + .map(|h| crate::fuzzbuster::utils::split_http_headers(h)) + .collect() + } else { + Vec::new() + }; + let include_status_codes = submatches + .values_of("include-status-codes") + .unwrap() + .filter(|s| { + if s.is_empty() { + return false; + } + s.parse::().is_ok() + }) + .map(|s| s.to_string()) + .collect::>(); + let ignore_status_codes = submatches + .values_of("ignore-status-codes") + .unwrap() + .filter(|s| { + if s.is_empty() { + return false; + } + s.parse::().is_ok() + }) + .map(|s| s.to_string()) + .collect::>(); + + HTTPArgs { + user_agent: user_agent.to_owned(), + http_method: http_method.to_owned(), + http_body: http_body.to_owned(), + url: url.to_owned(), + ignore_certificate, + http_headers, + include_status_codes, + ignore_status_codes, + } +} + +pub fn extract_dns_args<'a>(submatches: &clap::ArgMatches<'a>) -> DNSArgs { + let domain = submatches.value_of("domain").unwrap_or(""); + + DNSArgs { + domain: domain.to_owned(), + } +} + +pub fn extract_body_args<'a>(submatches: &clap::ArgMatches<'a>) -> BodyArgs { + let ignore_strings: Vec = if submatches.is_present("ignore-string") { + submatches + .values_of("ignore-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + let include_strings: Vec = if submatches.is_present("include-string") { + submatches + .values_of("include-string") + .unwrap() + .map(|h| h.to_owned()) + .collect() + } else { + Vec::new() + }; + + BodyArgs { + include_strings, + ignore_strings, + } +} + +pub fn extract_dir_args<'a>(submatches: &clap::ArgMatches<'a>) -> DirArgs { + let append_slash = submatches.is_present("append-slash"); + let extensions = submatches + .values_of("extensions") + .unwrap() + .filter(|e| !e.is_empty()) + .map(|s| s.to_owned()) + .collect::>(); + DirArgs { + append_slash, + extensions, + } +} + +pub fn url_is_valid(url: &str) -> bool { + match url.parse::() { + Err(e) => { + error!( + "Invalid URL: {}, consider adding a protocol like http:// or https://", + e + ); + return false; + } + Ok(v) => { + match v.scheme_part() { + Some(s) => { + if s != "http" && s != "https" { + error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); + return false; + } else { + return true; + } + } + None => { + error!("Invalid URL: missing protocol, consider adding http:// or https://"); + return false; + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 932eb8d..a1b96d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,10 +5,9 @@ extern crate clap; use clap::{App, Arg, SubCommand}; use indicatif::{ProgressBar, ProgressStyle}; -use terminal_size::{terminal_size, Height, Width}; - -use std::{str::FromStr, sync::mpsc::channel, thread, time::SystemTime}; +use std::{sync::mpsc::channel, thread, time::SystemTime}; +mod args; mod banner; mod dirbuster; mod dnsbuster; @@ -31,6 +30,7 @@ use vhostbuster::{ utils::*, VhostConfig, }; +use args::*; fn main() { if std::env::vars() @@ -551,365 +551,3 @@ fn main() { _ => (), } } - -fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - app.arg( - Arg::with_name("verbose") - .long("verbose") - .short("v") - .multiple(true) - .help("Sets the level of verbosity"), - ) - .arg( - Arg::with_name("no-banner") - .long("no-banner") - .help("Skips initial banner"), - ) - .arg( - Arg::with_name("wordlist") - .long("wordlist") - .help("Sets the wordlist") - .short("w") - .takes_value(true) - .multiple(true) - .use_delimiter(true) - .required(true), - ) - .arg( - Arg::with_name("threads") - .long("threads") - .alias("workers") - .help("Sets the amount of concurrent requests") - .short("t") - .default_value("10") - .takes_value(true), - ) - .arg( - Arg::with_name("exit-on-error") - .long("exit-on-error") - .help("Exits on connection errors") - .short("K"), - ) - .arg( - Arg::with_name("output") - .long("output") - .help("Saves the results in the specified file") - .short("o") - .default_value("") - .takes_value(true), - ) - .arg( - Arg::with_name("no-progress-bar") - .long("no-progress-bar") - .help("Disables the progress bar"), - ) -} - -fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - app.arg( - Arg::with_name("include-status-codes") - .long("include-status-codes") - .help("Sets the list of status codes to include") - .short("s") - .default_value("") - .use_delimiter(true), - ) - .arg( - Arg::with_name("ignore-status-codes") - .long("ignore-status-codes") - .help("Sets the list of status codes to ignore") - .short("S") - .default_value("404") - .use_delimiter(true), - ) - .arg( - Arg::with_name("user-agent") - .long("user-agent") - .help("Uses the specified User-Agent") - .short("a") - .default_value("rustbuster") - .takes_value(true), - ) - .arg( - Arg::with_name("ignore-certificate") - .long("ignore-certificate") - .alias("no-check-certificate") - .help("Disables TLS certificate validation") - .short("k"), - ) - .arg( - Arg::with_name("http-method") - .long("http-method") - .help("Uses the specified HTTP method") - .short("X") - .default_value("GET") - .takes_value(true), - ) - .arg( - Arg::with_name("http-body") - .long("http-body") - .help("Uses the specified HTTP method") - .short("b") - .default_value("") - .takes_value(true), - ) - .arg( - Arg::with_name("http-header") - .long("http-header") - .help("Appends the specified HTTP header") - .short("H") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("url") - .long("url") - .alias("domain") - .help("Sets the target URL") - .short("u") - .takes_value(true) - .required(true), - ) -} - -fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - app.arg( - Arg::with_name("domain") - .long("domain") - .help("Uses the specified domain") - .short("d") - .required(true) - .takes_value(true), - ) -} - -fn set_body_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { - app.arg( - Arg::with_name("ignore-string") - .long("ignore-string") - .help("Ignores results with specified string in the HTTP Body") - .short("x") - .multiple(true) - .takes_value(true), - ) - .arg( - Arg::with_name("include-string") - .long("include-string") - .help("Includes results with specified string in the HTTP body") - .short("i") - .multiple(true) - .conflicts_with("ignore-string") - .takes_value(true), - ) -} - -struct CommonArgs { - wordlist_paths: Vec, - no_banner: bool, - no_progress_bar: bool, - exit_on_connection_errors: bool, - n_threads: usize, - output: String, -} - -struct DNSArgs { - domain: String, -} - -struct HTTPArgs { - user_agent: String, - http_method: String, - http_body: String, - url: String, - ignore_certificate: bool, - http_headers: Vec<(String, String)>, - include_status_codes: Vec, - ignore_status_codes: Vec, -} - -struct BodyArgs { - include_strings: Vec, - ignore_strings: Vec, -} - -struct DirArgs { - append_slash: bool, - extensions: Vec, -} - -fn extract_common_args<'a>(submatches: &clap::ArgMatches<'a>) -> CommonArgs { - let wordlist_paths = submatches - .values_of("wordlist") - .unwrap() - .map(|w| w.to_owned()) - .collect::>(); - let mut no_banner = submatches.is_present("no-banner"); - let mut no_progress_bar = submatches.is_present("no-progress-bar"); - let exit_on_connection_errors = submatches.is_present("exit-on-error"); - let n_threads = submatches - .value_of("threads") - .unwrap() - .parse::() - .expect("threads is a number"); - - let output = submatches.value_of("output").unwrap(); - - if let Some((Width(w), Height(h))) = terminal_size() { - if w < 122 { - no_banner = true; - } - - if w < 104 { - warn!("Your terminal is {} cols wide and {} lines tall", w, h); - warn!("Disabling progress bar, minimum cols: 104"); - no_progress_bar = true; - } - } else { - warn!("Unable to get terminal size"); - no_banner = true; - no_progress_bar = true; - } - - CommonArgs { - wordlist_paths, - no_banner, - no_progress_bar, - exit_on_connection_errors, - n_threads, - output: output.to_owned(), - } -} - -fn extract_http_args<'a>(submatches: &clap::ArgMatches<'a>) -> HTTPArgs { - let user_agent = submatches.value_of("user-agent").unwrap(); - let http_method = submatches.value_of("http-method").unwrap(); - let http_body = submatches.value_of("http-body").unwrap(); - let url = submatches.value_of("url").unwrap(); - let ignore_certificate = submatches.is_present("ignore-certificate"); - let http_headers: Vec<(String, String)> = if submatches.is_present("http-header") { - submatches - .values_of("http-header") - .unwrap() - .map(|h| fuzzbuster::utils::split_http_headers(h)) - .collect() - } else { - Vec::new() - }; - let include_status_codes = submatches - .values_of("include-status-codes") - .unwrap() - .filter(|s| { - if s.is_empty() { - return false; - } - let valid = hyper::StatusCode::from_str(s).is_ok(); - if !valid { - warn!("Ignoring invalid status code for '-s' param: {}", s); - } - valid - }) - .map(|s| s.to_string()) - .collect::>(); - let ignore_status_codes = submatches - .values_of("ignore-status-codes") - .unwrap() - .filter(|s| { - if s.is_empty() { - return false; - } - let valid = hyper::StatusCode::from_str(s).is_ok(); - if !valid { - warn!("Ignoring invalid status code for '-S' param: {}", s); - } - valid - }) - .map(|s| s.to_string()) - .collect::>(); - - HTTPArgs { - user_agent: user_agent.to_owned(), - http_method: http_method.to_owned(), - http_body: http_body.to_owned(), - url: url.to_owned(), - ignore_certificate, - http_headers, - include_status_codes, - ignore_status_codes, - } -} - -fn extract_dns_args<'a>(submatches: &clap::ArgMatches<'a>) -> DNSArgs { - let domain = submatches.value_of("domain").unwrap_or(""); - - DNSArgs { - domain: domain.to_owned(), - } -} - -fn extract_body_args<'a>(submatches: &clap::ArgMatches<'a>) -> BodyArgs { - let ignore_strings: Vec = if submatches.is_present("ignore-string") { - submatches - .values_of("ignore-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - let include_strings: Vec = if submatches.is_present("include-string") { - submatches - .values_of("include-string") - .unwrap() - .map(|h| h.to_owned()) - .collect() - } else { - Vec::new() - }; - - BodyArgs { - include_strings, - ignore_strings, - } -} - -fn extract_dir_args<'a>(submatches: &clap::ArgMatches<'a>) -> DirArgs { - let append_slash = submatches.is_present("append-slash"); - let extensions = submatches - .values_of("extensions") - .unwrap() - .filter(|e| !e.is_empty()) - .map(|s| s.to_owned()) - .collect::>(); - DirArgs { - append_slash, - extensions, - } -} - -fn url_is_valid(url: &str) -> bool { - match url.parse::() { - Err(e) => { - error!( - "Invalid URL: {}, consider adding a protocol like http:// or https://", - e - ); - return false; - } - Ok(v) => { - match v.scheme_part() { - Some(s) => { - if s != "http" && s != "https" { - error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); - return false; - } else { - return true; - } - } - None => { - error!("Invalid URL: missing protocol, consider adding http:// or https://"); - return false; - } - } - } - } -} From 2b8240f7db86e0096e3692169d2595eb3e47a860 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 01:25:03 +0200 Subject: [PATCH 12/17] refactor: update readme --- README.md | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f89104..f3b1cc6 100644 --- a/README.md +++ b/README.md @@ -45,25 +45,151 @@ EXAMPLES: -s 200 \ --csrf-url "http://localhost:3000/csrf" \ --csrf-regex '\{"csrf":"(\w+)"\}' - ``` ### `dir` usage ```shell +rustbuster-dir +Directories and files enumeration mode + +USAGE: + rustbuster dir [FLAGS] [OPTIONS] --url --wordlist ... + +FLAGS: + -f, --append-slash Tries to also append / to the base request + -K, --exit-on-error Exits on connection errors + -h, --help Prints help information + -k, --ignore-certificate Disables TLS certificate validation + --no-banner Skips initial banner + --no-progress-bar Disables the progress bar + -V, --version Prints version information + -v, --verbose Sets the level of verbosity + +OPTIONS: + -e, --extensions Sets the extensions [default: ] + -b, --http-body Uses the specified HTTP method [default: ] + -H, --http-header ... Appends the specified HTTP header + -X, --http-method Uses the specified HTTP method [default: GET] + -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] + -s, --include-status-codes Sets the list of status codes to include [default: ] + -o, --output Saves the results in the specified file [default: ] + -t, --threads Sets the amount of concurrent requests [default: 10] + -u, --url Sets the target URL + -a, --user-agent Uses the specified User-Agent [default: rustbuster] + -w, --wordlist ... Sets the wordlist + +EXAMPLE: + rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php ``` ### `dns` usage ```shell +rustbuster-dns +A/AAAA entries enumeration mode + +USAGE: + rustbuster dns [FLAGS] [OPTIONS] --domain --wordlist ... + +FLAGS: + -K, --exit-on-error Exits on connection errors + -h, --help Prints help information + --no-banner Skips initial banner + --no-progress-bar Disables the progress bar + -V, --version Prints version information + -v, --verbose Sets the level of verbosity + +OPTIONS: + -d, --domain Uses the specified domain + -o, --output Saves the results in the specified file [default: ] + -t, --threads Sets the amount of concurrent requests [default: 10] + -w, --wordlist ... Sets the wordlist + +EXAMPLE: + rustbuster dns -u google.com -w examples/wordlist ``` ### `vhost` usage ```shell +rustbuster-vhost +Virtual hosts enumeration mode + +USAGE: + rustbuster vhost [FLAGS] [OPTIONS] --domain --url --wordlist ... + +FLAGS: + -K, --exit-on-error Exits on connection errors + -h, --help Prints help information + -k, --ignore-certificate Disables TLS certificate validation + --no-banner Skips initial banner + --no-progress-bar Disables the progress bar + -V, --version Prints version information + -v, --verbose Sets the level of verbosity + +OPTIONS: + -d, --domain Uses the specified domain to bruteforce + -b, --http-body Uses the specified HTTP method [default: ] + -H, --http-header ... Appends the specified HTTP header + -X, --http-method Uses the specified HTTP method [default: GET] + -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] + -x, --ignore-string ... Ignores results with specified string in the HTTP Body + -s, --include-status-codes Sets the list of status codes to include [default: ] + -i, --include-string ... Includes results with specified string in the HTTP body + -o, --output Saves the results in the specified file [default: ] + -t, --threads Sets the amount of concurrent requests [default: 10] + -u, --url Sets the target URL + -a, --user-agent Uses the specified User-Agent [default: rustbuster] + -w, --wordlist ... Sets the wordlist + +EXAMPLE: + rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x "Hello" ``` #### `fuzz` usage ```shell +rustbuster-fuzz +Custom fuzzing enumeration mode + +USAGE: + rustbuster fuzz [FLAGS] [OPTIONS] --url --wordlist ... + +FLAGS: + -K, --exit-on-error Exits on connection errors + -h, --help Prints help information + -k, --ignore-certificate Disables TLS certificate validation + --no-banner Skips initial banner + --no-progress-bar Disables the progress bar + -V, --version Prints version information + -v, --verbose Sets the level of verbosity + +OPTIONS: + --csrf-header ... Adds the specified headers to CSRF GET request + --csrf-regex Grabs the CSRF token applying the specified RegEx + --csrf-url Grabs the CSRF token via GET to csrf-url + -b, --http-body Uses the specified HTTP method [default: ] + -H, --http-header ... Appends the specified HTTP header + -X, --http-method Uses the specified HTTP method [default: GET] + -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] + -x, --ignore-string ... Ignores results with specified string in the HTTP Body + -s, --include-status-codes Sets the list of status codes to include [default: ] + -i, --include-string ... Includes results with specified string in the HTTP body + -o, --output Saves the results in the specified file [default: ] + -t, --threads Sets the amount of concurrent requests [default: 10] + -u, --url Sets the target URL + -a, --user-agent Uses the specified User-Agent [default: rustbuster] + -w, --wordlist ... Sets the wordlist + +EXAMPLE: + rustbuster fuzz -u http://localhost:3000/login \ + -X POST \ + -H "Content-Type: application/json" \ + -b '{"user":"FUZZ","password":"FUZZ","csrf":"CSRFCSRF"}' \ + -w examples/wordlist \ + -w /usr/share/seclists/Passwords/Common-Credentials/10-million-password-list-top-10000.txt \ + -s 200 \ + --csrf-url "http://localhost:3000/csrf" \ + --csrf-regex '\{"csrf":"(\w+)"\}' ``` From d920426b62657cfe4892dd4bcb91ae22112cf10e Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 01:26:31 +0200 Subject: [PATCH 13/17] refactor: update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3b1cc6..cfd6bdb 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ EXAMPLE: rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x "Hello" ``` -#### `fuzz` usage +### `fuzz` usage ```shell rustbuster-fuzz From 50dac7eb88cde83877ddd2c1867d85ee7b47c778 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 01:30:09 +0200 Subject: [PATCH 14/17] style: cargo fmt --- src/args.rs | 24 +++++++++++------------- src/main.rs | 2 +- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/args.rs b/src/args.rs index 0b32f69..7d842d4 100644 --- a/src/args.rs +++ b/src/args.rs @@ -336,21 +336,19 @@ pub fn url_is_valid(url: &str) -> bool { ); return false; } - Ok(v) => { - match v.scheme_part() { - Some(s) => { - if s != "http" && s != "https" { - error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); - return false; - } else { - return true; - } - } - None => { - error!("Invalid URL: missing protocol, consider adding http:// or https://"); + Ok(v) => match v.scheme_part() { + Some(s) => { + if s != "http" && s != "https" { + error!("Invalid URL: invalid protocol, only http:// or https:// are supported"); return false; + } else { + return true; } } - } + None => { + error!("Invalid URL: missing protocol, consider adding http:// or https://"); + return false; + } + }, } } diff --git a/src/main.rs b/src/main.rs index a1b96d9..288381b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ mod dnsbuster; mod fuzzbuster; mod vhostbuster; +use args::*; use dirbuster::{ result_processor::{ResultProcessorConfig, ScanResult, SingleDirScanResult}, utils::*, @@ -30,7 +31,6 @@ use vhostbuster::{ utils::*, VhostConfig, }; -use args::*; fn main() { if std::env::vars() From 01014679ec6deee0807d091867a54c4b6783c2c3 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 23:20:33 +0200 Subject: [PATCH 15/17] refactor: move more parsing in args.rs --- src/args.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/main.rs | 85 +++++---------------------------------- 2 files changed, 113 insertions(+), 84 deletions(-) diff --git a/src/args.rs b/src/args.rs index 7d842d4..6f5e46f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -35,6 +35,12 @@ pub struct DirArgs { pub extensions: Vec, } +pub struct FuzzArgs { + pub csrf_url: Option, + pub csrf_regex: Option, + pub csrf_headers: Option>, +} + pub fn set_common_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app.arg( Arg::with_name("verbose") @@ -131,7 +137,7 @@ pub fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { .arg( Arg::with_name("http-body") .long("http-body") - .help("Uses the specified HTTP method") + .help("Uses the specified HTTP body") .short("b") .default_value("") .takes_value(true), @@ -155,6 +161,43 @@ pub fn set_http_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { ) } +pub fn set_body_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("ignore-string") + .long("ignore-string") + .help("Ignores results with specified string in the HTTP body") + .short("x") + .multiple(true) + .takes_value(true), + ) + .arg( + Arg::with_name("include-string") + .long("include-string") + .help("Includes results with specified string in the HTTP body") + .short("i") + .multiple(true) + .conflicts_with("ignore-string") + .takes_value(true), + ) +} + +pub fn set_dir_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("extensions") + .long("extensions") + .help("Sets the extensions") + .short("e") + .default_value("") + .use_delimiter(true), + ) + .arg( + Arg::with_name("append-slash") + .long("append-slash") + .help("Tries to also append / to the base request") + .short("f"), + ) +} + pub fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app.arg( Arg::with_name("domain") @@ -166,22 +209,47 @@ pub fn set_dns_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { ) } -pub fn set_body_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { +pub fn set_vhost_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { app.arg( + Arg::with_name("domain") + .long("domain") + .help("Uses the specified domain to bruteforce") + .short("d") + .required(true) + .takes_value(true), + ) + .arg( Arg::with_name("ignore-string") .long("ignore-string") - .help("Ignores results with specified string in the HTTP Body") + .help("Ignores results with specified string in the HTTP body") .short("x") + .required(true) .multiple(true) .takes_value(true), ) +} + +pub fn set_fuzz_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> { + app.arg( + Arg::with_name("csrf-url") + .long("csrf-url") + .help("Grabs the CSRF token via GET to csrf-url") + .requires("csrf-regex") + .takes_value(true), + ) .arg( - Arg::with_name("include-string") - .long("include-string") - .help("Includes results with specified string in the HTTP body") - .short("i") + Arg::with_name("csrf-regex") + .long("csrf-regex") + .help("Grabs the CSRF token applying the specified RegEx") + .requires("csrf-url") + .takes_value(true), + ) + .arg( + Arg::with_name("csrf-header") + .long("csrf-header") + .help("Adds the specified headers to CSRF GET request") + .requires("csrf-url") .multiple(true) - .conflicts_with("ignore-string") .takes_value(true), ) } @@ -327,6 +395,34 @@ pub fn extract_dir_args<'a>(submatches: &clap::ArgMatches<'a>) -> DirArgs { } } +pub fn extract_fuzz_args<'a>(submatches: &clap::ArgMatches<'a>) -> FuzzArgs { + let csrf_url = match submatches.value_of("csrf-url") { + Some(v) => Some(v.to_owned()), + None => None, + }; + let csrf_regex = match submatches.value_of("csrf-regex") { + Some(v) => Some(v.to_owned()), + None => None, + }; + let csrf_headers: Option> = + if submatches.is_present("csrf-header") { + Some( + submatches + .values_of("csrf-header") + .unwrap() + .map(|h| crate::fuzzbuster::utils::split_http_headers(h)) + .collect(), + ) + } else { + None + }; + FuzzArgs { + csrf_url, + csrf_regex, + csrf_headers, + } +} + pub fn url_is_valid(url: &str) -> bool { match url.parse::() { Err(e) => { diff --git a/src/main.rs b/src/main.rs index 288381b..70b7f54 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate log; #[macro_use] extern crate clap; -use clap::{App, Arg, SubCommand}; +use clap::{App, SubCommand}; use indicatif::{ProgressBar, ProgressStyle}; use std::{sync::mpsc::channel, thread, time::SystemTime}; @@ -65,64 +65,20 @@ fn main() { --csrf-url \"http://localhost:3000/csrf\" \\ --csrf-regex '\\{\"csrf\":\"(\\w+)\"\\}' ") - .subcommand(set_http_args(set_common_args(SubCommand::with_name("dir"))) + .subcommand(set_dir_args(set_http_args(set_common_args(SubCommand::with_name("dir")))) .about("Directories and files enumeration mode") - .arg( - Arg::with_name("extensions") - .long("extensions") - .help("Sets the extensions") - .short("e") - .default_value("") - .use_delimiter(true), - ) - .arg( - Arg::with_name("append-slash") - .long("append-slash") - .help("Tries to also append / to the base request") - .short("f"), - ) .after_help("EXAMPLE: rustbuster dir -u http://localhost:3000/ -w examples/wordlist -e php")) .subcommand(set_dns_args(set_common_args(SubCommand::with_name("dns"))) .about("A/AAAA entries enumeration mode") .after_help("EXAMPLE: rustbuster dns -u google.com -w examples/wordlist")) - .subcommand(set_body_args(set_http_args(set_common_args(SubCommand::with_name("vhost")))) + .subcommand(set_vhost_args(set_http_args(set_common_args(SubCommand::with_name("vhost")))) .about("Virtual hosts enumeration mode") - .arg( - Arg::with_name("domain") - .long("domain") - .help("Uses the specified domain to bruteforce") - .short("d") - .required(true) - .takes_value(true), - ) .after_help("EXAMPLE: rustbuster vhost -u http://localhost:3000/ -w examples/wordlist -d test.local -x \"Hello\"")) - .subcommand(set_body_args(set_http_args(set_common_args(SubCommand::with_name("fuzz")))) + .subcommand(set_fuzz_args(set_body_args(set_http_args(set_common_args(SubCommand::with_name("fuzz"))))) .about("Custom fuzzing enumeration mode") - .arg( - Arg::with_name("csrf-url") - .long("csrf-url") - .help("Grabs the CSRF token via GET to csrf-url") - .requires("csrf-regex") - .takes_value(true), - ) - .arg( - Arg::with_name("csrf-regex") - .long("csrf-regex") - .help("Grabs the CSRF token applying the specified RegEx") - .requires("csrf-url") - .takes_value(true), - ) - .arg( - Arg::with_name("csrf-header") - .long("csrf-header") - .help("Adds the specified headers to CSRF GET request") - .requires("csrf-url") - .multiple(true) - .takes_value(true), - ) .after_help("EXAMPLE: rustbuster fuzz -u http://localhost:3000/login \\ -X POST \\ @@ -392,11 +348,6 @@ fn main() { "vhost" => { let dns_args = extract_dns_args(submatches); let body_args = extract_body_args(submatches); - if body_args.ignore_strings.is_empty() { - error!("ignore_strings not specified (-x)"); - return; - } - let http_args = extract_http_args(submatches); if !url_is_valid(&http_args.url) { return; @@ -503,26 +454,8 @@ fn main() { } let body_args = extract_body_args(submatches); - let csrf_url = match submatches.value_of("csrf-url") { - Some(v) => Some(v.to_owned()), - None => None, - }; - let csrf_regex = match submatches.value_of("csrf-regex") { - Some(v) => Some(v.to_owned()), - None => None, - }; - let csrf_headers: Option> = - if submatches.is_present("csrf-header") { - Some( - submatches - .values_of("csrf-header") - .unwrap() - .map(|h| fuzzbuster::utils::split_http_headers(h)) - .collect(), - ) - } else { - None - }; + let fuzz_args = extract_fuzz_args(submatches); + let fuzzbuster = FuzzBuster { n_threads: common_args.n_threads, ignore_certificate: http_args.ignore_certificate, @@ -539,9 +472,9 @@ fn main() { output: common_args.output.to_owned(), include_body: body_args.include_strings, ignore_body: body_args.ignore_strings, - csrf_url, - csrf_regex, - csrf_headers, + csrf_url: fuzz_args.csrf_url, + csrf_regex: fuzz_args.csrf_regex, + csrf_headers: fuzz_args.csrf_headers, }; debug!("FuzzBuster {:#?}", fuzzbuster); From 5ec249324b1dd8e877d7a702f3983d276eef3733 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 23:21:02 +0200 Subject: [PATCH 16/17] docs: update usage --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cfd6bdb..97e1852 100644 --- a/README.md +++ b/README.md @@ -113,11 +113,11 @@ EXAMPLE: ### `vhost` usage ```shell -rustbuster-vhost +rustbuster-vhost Virtual hosts enumeration mode USAGE: - rustbuster vhost [FLAGS] [OPTIONS] --domain --url --wordlist ... + rustbuster vhost [FLAGS] [OPTIONS] --domain --ignore-string ... --url --wordlist ... FLAGS: -K, --exit-on-error Exits on connection errors @@ -130,13 +130,12 @@ FLAGS: OPTIONS: -d, --domain Uses the specified domain to bruteforce - -b, --http-body Uses the specified HTTP method [default: ] + -b, --http-body Uses the specified HTTP body [default: ] -H, --http-header ... Appends the specified HTTP header -X, --http-method Uses the specified HTTP method [default: GET] -S, --ignore-status-codes Sets the list of status codes to ignore [default: 404] - -x, --ignore-string ... Ignores results with specified string in the HTTP Body + -x, --ignore-string ... Ignores results with specified string in the HTTP body -s, --include-status-codes Sets the list of status codes to include [default: ] - -i, --include-string ... Includes results with specified string in the HTTP body -o, --output Saves the results in the specified file [default: ] -t, --threads Sets the amount of concurrent requests [default: 10] -u, --url Sets the target URL From 8e2d2571819cf944ccc7a33d35aee45a04d05c57 Mon Sep 17 00:00:00 2001 From: phra Date: Fri, 21 Jun 2019 23:22:21 +0200 Subject: [PATCH 17/17] style: cargo fmt --- src/args.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/args.rs b/src/args.rs index 6f5e46f..8a969cf 100644 --- a/src/args.rs +++ b/src/args.rs @@ -404,18 +404,17 @@ pub fn extract_fuzz_args<'a>(submatches: &clap::ArgMatches<'a>) -> FuzzArgs { Some(v) => Some(v.to_owned()), None => None, }; - let csrf_headers: Option> = - if submatches.is_present("csrf-header") { - Some( - submatches - .values_of("csrf-header") - .unwrap() - .map(|h| crate::fuzzbuster::utils::split_http_headers(h)) - .collect(), - ) - } else { - None - }; + let csrf_headers: Option> = if submatches.is_present("csrf-header") { + Some( + submatches + .values_of("csrf-header") + .unwrap() + .map(|h| crate::fuzzbuster::utils::split_http_headers(h)) + .collect(), + ) + } else { + None + }; FuzzArgs { csrf_url, csrf_regex,