From ef36537055d52fee6d05da9fcf8d92b06cae157e Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Fri, 16 Oct 2020 20:12:12 +0200 Subject: [PATCH 1/2] Add --format and --output-file flags --- Cargo.lock | 1 + Cargo.toml | 1 + README.md | 45 +++++++++------- src/input.rs | 36 +++++++++++-- src/main.rs | 145 +++++++++++++++++++++++++++++++-------------------- 5 files changed, 147 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7c28f7818..19e60541c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1143,6 +1143,7 @@ dependencies = [ "rlimit", "serde", "serde_derive", + "serde_json", "shell-words", "structopt", "subprocess", diff --git a/Cargo.toml b/Cargo.toml index 80d747e52..3014bbc62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ ansi_term = "0.12.1" toml = "0.5.7" serde = "1.0.117" serde_derive = "1.0.116" +serde_json = "1.0.59" cidr-utils = "0.5.0" itertools = "0.9.0" trust-dns-resolver = { version = "0.19.5", features = ["dns-over-rustls"] } diff --git a/README.md b/README.md index 6957b8082..83dd96523 100644 --- a/README.md +++ b/README.md @@ -221,37 +221,44 @@ server may not be able to handle this many socket connections at once. - Discord https://github.com/RustScan/RustScan USAGE: - rustscan [FLAGS] [OPTIONS] [addresses]... [-- ...] + rustscan [FLAGS] [OPTIONS] [-- ...] FLAGS: --accessible Accessible mode. Turns off features which negatively affect screen readers -g, --greppable Greppable mode. Only output the ports. No Nmap. Useful for grep or outputting to a file -h, --help Prints help information -n, --no-config Whether to ignore the configuration file or not - --no-nmap Turns off Nmap --top Use the top 1000 ports -V, --version Prints version information OPTIONS: - -b, --batch-size The batch size for port scanning, it increases or decreases the speed of scanning. - Depends on the open file limit of your OS. If you use 65535 it will scan every port - at the same time. Although, your OS may not support this [default: 4500] - -p, --ports ... A list of comma separed ports to be scanned. Example: 80,443,8080 - -r, --range A range of ports with format start-end. Example: 1-1000 - --scan-order The order of scanning to be performed. The "serial" option will scan ports in - ascending order while the "random" option will scan ports randomly [default: - serial] [possible values: Serial, Random] - -t, --timeout The timeout in milliseconds before a port is assumed to be closed [default: 1500] - --tries The number of tries before a port is assumed to be closed. If set to 0, rustscan - will correct it to 1 [default: 1] - -u, --ulimit Automatically ups the ULIMIT with the value you provided + -a, --addresses ... A list of comma separated CIDRs, IPs, or hosts to be scanned + -b, --batch-size The batch size for port scanning, it increases or slows the speed of scanning. + Depends on the open file limit of your OS. If you do 65535 it will do every port + at the same time. Although, your OS may not support this [default: 4500] + --format Output format of the scan. Note that this only includes the port scan result that + is produced by rustscan, not the output of Nmap [default: text] [possible + values: Text, Json] + -o, --output-file Path to a file where the port scan result will be written to. The output will be + formatted according to the --format flag. If not specified, formatted output will + be printed to stdout + -p, --ports ... A list of comma separed ports to be scanned. Example: 80,443,8080 + -r, --range A range of ports with format start-end. Example: 1-1000 + --scan-order The order of scanning to be performed. The "serial" option will scan ports in + ascending order while the "random" option will scan ports randomly [default: + serial] [possible values: Serial, Random] + --scripts Level of scripting required for the run [default: default] [possible values: + None, Default, Custom] + -t, --timeout The timeout in milliseconds before a port is assumed to be closed [default: 1500] + --tries The number of tries before a port is assumed to be closed. If set to 0, rustscan + will correct it to 1 [default: 1] + -u, --ulimit Automatically ups the ULIMIT with the value you provided ARGS: - ... A list of comma separated CIDRs, IPs, or hosts to be scanned - ... The Nmap arguments to run. To use the argument -A, end RustScan's args with '-- -A'. Example: - 'rustscan -T 1500 127.0.0.1 -- -A -sC'. This command adds -Pn -vvv -p $PORTS automatically to - nmap. For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and - vuln)'\"") + ... The Script arguments to run. To use the argument -A, end RustScan's args with '-- -A'. Example: + 'rustscan -T 1500 127.0.0.1 -- -A -sC'. This command adds -Pn -vvv -p $PORTS automatically to + nmap. For things like --script '(safe and vuln)' enclose it in quotations marks \"'(safe and + vuln)'\"") ``` The format is `rustscan -b 500 -t 1500 192.168.0.1` to scan 192.168.0.1 with 500 batch size with a timeout of 1500ms. The timeout is how long RustScan waits for a response until it assumes the port is closed. diff --git a/src/input.rs b/src/input.rs index 276823e19..3bcaa424b 100644 --- a/src/input.rs +++ b/src/input.rs @@ -30,6 +30,15 @@ arg_enum! { } } +arg_enum! { + /// Specifies how to format the scan result output. + #[derive(Deserialize, Debug, StructOpt, Clone, Copy, PartialEq)] + pub enum OutputFormat { + Text, + Json, + } +} + /// Represents the range of ports to be scanned. #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct PortRange { @@ -94,8 +103,19 @@ pub struct Opts { #[structopt(long)] pub accessible: bool, + /// Output format of the scan. Note that this only includes the port scan result that is + /// produced by rustscan, not the output of Nmap. + #[structopt(long, default_value = "text", possible_values = &OutputFormat::variants(), case_insensitive = true)] + pub format: OutputFormat, + + /// Path to a file where the port scan result will be written to. The output will be formatted + /// according to the --format flag. If not specified, formatted output will be printed to + /// stdout. + #[structopt(short, long)] + pub output_file: Option, + /// The batch size for port scanning, it increases or slows the speed of - /// scanning. Depends on the open file limit of your OS. If you do 65535 + /// scanning. Depends on the open file limit of your OS. If you do 65535 /// it will do every port at the same time. Although, your OS may not /// support this. #[structopt(short, long, default_value = "4500")] @@ -173,8 +193,8 @@ impl Opts { } merge_required!( - addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts, - command + addresses, greppable, accessible, batch_size, timeout, tries, format, scan_order, + scripts, command ); } @@ -198,7 +218,7 @@ impl Opts { self.ports = Some(ports); } - merge_optional!(range, ulimit); + merge_optional!(range, output_file, ulimit); } } @@ -216,6 +236,8 @@ pub struct Config { batch_size: Option, timeout: Option, tries: Option, + format: Option, + output_file: Option, ulimit: Option, scan_order: Option, command: Option>, @@ -263,7 +285,7 @@ impl Config { #[cfg(test)] mod tests { - use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; + use super::{Config, Opts, OutputFormat, PortRange, ScanOrder, ScriptsRequired}; impl Config { fn default() -> Self { Self { @@ -275,6 +297,8 @@ mod tests { timeout: Some(1_000), tries: Some(1), ulimit: None, + format: None, + output_file: None, command: Some(vec!["-A".to_owned()]), accessible: Some(true), scan_order: Some(ScanOrder::Random), @@ -296,6 +320,8 @@ mod tests { ulimit: None, command: vec![], accessible: false, + format: OutputFormat::Text, + output_file: None, scan_order: ScanOrder::Serial, no_config: true, top: false, diff --git a/src/main.rs b/src/main.rs index 645db332a..7806cfc31 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ extern crate shell_words; mod tui; mod input; -use input::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; +use input::{Config, Opts, OutputFormat, PortRange, ScanOrder, ScriptsRequired}; mod scanner; use scanner::Scanner; @@ -24,10 +24,11 @@ use scripts::{init_scripts, Script, ScriptFile}; use cidr_utils::cidr::IpCidr; use colorful::{Color, Colorful}; use futures::executor::block_on; +use itertools::Itertools; use rlimit::{getrlimit, setrlimit, RawRlim, Resource, Rlim}; use std::collections::HashMap; use std::convert::TryInto; -use std::fs::File; +use std::fs::{self, File}; use std::io::{prelude::*, BufReader}; use std::net::{IpAddr, ToSocketAddrs}; use std::path::Path; @@ -128,67 +129,78 @@ fn main() { warning!(x, opts.greppable, opts.accessible); } - let mut script_bench = NamedTimer::start("Scripts"); - for (ip, ports) in &ports_per_ip { - let vec_str_ports: Vec = ports.iter().map(ToString::to_string).collect(); - - // nmap port style is 80,443. Comma separated with no spaces. - let ports_str = vec_str_ports.join(","); - - // if option scripts is none, no script will be spawned - if opts.greppable || opts.scripts == ScriptsRequired::None { - println!("{} -> [{}]", &ip, ports_str); - continue; + if opts.greppable + || opts.scripts == ScriptsRequired::None + || opts.format != OutputFormat::Text + || opts.output_file.is_some() + { + let output_string = format_summary(&ports_per_ip, opts.format); + + if let Some(ref path) = opts.output_file { + if let Err(e) = fs::write(path, &output_string) { + eprintln!("Failed to write output to file at '{}': {}", path, e); + println!("Have your output anyway:\n{}", output_string); + return; + } + } else { + println!("{}", output_string); } - detail!("Starting Script(s)", opts.greppable, opts.accessible); - - // Run all the scripts we found and parsed based on the script config file tags field. - for mut script_f in scripts_to_run.clone() { - output!( - format!("Script to be run {:?}\n", script_f.call_format,), - opts.greppable, - opts.accessible - ); + } - // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. - if !opts.command.is_empty() { - let user_extra_args: Vec = shell_words::split(&opts.command.join(" ")) - .expect("Failed to parse extra user commandline arguments"); - if script_f.call_format.is_some() { - let mut call_f = script_f.call_format.unwrap(); - call_f.push_str(&format!(" {}", &user_extra_args.join(" "))); - script_f.call_format = Some(call_f); + if !opts.greppable && opts.scripts != ScriptsRequired::None { + let mut script_bench = NamedTimer::start("Scripts"); + for (ip, ports) in &ports_per_ip { + detail!("Starting Script(s)", opts.greppable, opts.accessible); + + // Run all the scripts we found and parsed based on the script config file tags field. + for mut script_f in scripts_to_run.clone() { + output!( + format!("Script to be run {:?}\n", script_f.call_format,), + opts.greppable, + opts.accessible + ); + + // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command. + if !opts.command.is_empty() { + let user_extra_args: Vec = shell_words::split(&opts.command.join(" ")) + .expect("Failed to parse extra user commandline arguments"); + if script_f.call_format.is_some() { + let mut call_f = script_f.call_format.unwrap(); + call_f.push_str(&format!(" {}", &user_extra_args.join(" "))); + script_f.call_format = Some(call_f); + } } - } - // Building the script with the arguments from the ScriptFile, and ip-ports. - let script = Script::build( - script_f.path, - *ip, - ports.to_vec(), - script_f.port, - script_f.ports_separator, - script_f.tags, - script_f.call_format, - ); - match script.run() { - Ok(script_result) => { - detail!(script_result.to_string(), opts.greppable, opts.accessible); - } - Err(e) => { - warning!( - &format!("Error {}", e.to_string()), - opts.greppable, - opts.accessible - ); + // Building the script with the arguments from the ScriptFile, and ip-ports. + let script = Script::build( + script_f.path, + *ip, + ports.to_vec(), + script_f.port, + script_f.ports_separator, + script_f.tags, + script_f.call_format, + ); + match script.run() { + Ok(script_result) => { + detail!(script_result.to_string(), opts.greppable, opts.accessible); + } + Err(e) => { + warning!( + &format!("Error {}", e.to_string()), + opts.greppable, + opts.accessible + ); + } } } } + + // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan + script_bench.end(); + benchmarks.push(script_bench); } - // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan - script_bench.end(); - benchmarks.push(script_bench); rustscan_bench.end(); benchmarks.push(rustscan_bench); debug!("Benchmarks raw {:?}", benchmarks); @@ -267,6 +279,25 @@ fn parse_addresses(input: &Opts) -> Vec { ips } +/// Generates a summary in the given format. +fn format_summary(ports_per_ip: &HashMap>, out_format: OutputFormat) -> String { + match out_format { + OutputFormat::Text => ports_per_ip + .iter() + .map(|(ip, ports)| { + format!( + "{} -> [{}]", + ip, + ports.iter().map(ToString::to_string).join(",") + ) + }) + .join("\n"), + OutputFormat::Json => { + serde_json::to_string(&ports_per_ip).expect("Failed to serialize results as JSON.") + } + } +} + /// Given a string, parse it as an host, IP address, or CIDR. /// This allows us to pass files as hosts or cidr or IPs easily /// Call this everytime you have a possible IP_or_host @@ -343,8 +374,8 @@ fn infer_batch_size(opts: &Opts, ulimit: RawRlim) -> u16 { // Adjust the batch size when the ulimit value is lower than the desired batch size if ulimit < batch_size { warning!("File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers", - opts.greppable, opts.accessible - ); + opts.greppable, opts.accessible + ); // When the OS supports high file limits like 8000, but the user // selected a batch size higher than this we should reduce it to @@ -367,7 +398,7 @@ fn infer_batch_size(opts: &Opts, ulimit: RawRlim) -> u16 { // batch size can be increased unless they specified the ulimit themselves. else if ulimit + 2 > batch_size && (opts.ulimit.is_none()) { detail!(format!("File limit higher than batch size. Can increase speed by increasing batch size '-b {}'.", ulimit - 100), - opts.greppable, opts.accessible); + opts.greppable, opts.accessible); } batch_size From c10748c3b5df8d279e920d09ec9ef8e154584635 Mon Sep 17 00:00:00 2001 From: Niklas Mohrin Date: Wed, 21 Oct 2020 23:48:54 +0200 Subject: [PATCH 2/2] First try at proposed summary "mode" approach --- src/input.rs | 36 +++++++++++++----------------------- src/main.rs | 40 ++++++++++++++-------------------------- src/scanner/mod.rs | 4 ++-- src/tui.rs | 14 +++++++------- 4 files changed, 36 insertions(+), 58 deletions(-) diff --git a/src/input.rs b/src/input.rs index 3bcaa424b..38683cf3f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -31,9 +31,9 @@ arg_enum! { } arg_enum! { - /// Specifies how to format the scan result output. + /// Specifies how to format the summary. #[derive(Deserialize, Debug, StructOpt, Clone, Copy, PartialEq)] - pub enum OutputFormat { + pub enum SummaryFormat { Text, Json, } @@ -103,16 +103,9 @@ pub struct Opts { #[structopt(long)] pub accessible: bool, - /// Output format of the scan. Note that this only includes the port scan result that is - /// produced by rustscan, not the output of Nmap. - #[structopt(long, default_value = "text", possible_values = &OutputFormat::variants(), case_insensitive = true)] - pub format: OutputFormat, - - /// Path to a file where the port scan result will be written to. The output will be formatted - /// according to the --format flag. If not specified, formatted output will be printed to - /// stdout. - #[structopt(short, long)] - pub output_file: Option, + /// Just run the portscan and output its result in the given format. + #[structopt(long, possible_values = &SummaryFormat::variants(), case_insensitive = true)] + pub summary: Option, /// The batch size for port scanning, it increases or slows the speed of /// scanning. Depends on the open file limit of your OS. If you do 65535 @@ -193,8 +186,8 @@ impl Opts { } merge_required!( - addresses, greppable, accessible, batch_size, timeout, tries, format, scan_order, - scripts, command + addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts, + command ); } @@ -218,7 +211,7 @@ impl Opts { self.ports = Some(ports); } - merge_optional!(range, output_file, ulimit); + merge_optional!(range, summary, ulimit); } } @@ -236,8 +229,7 @@ pub struct Config { batch_size: Option, timeout: Option, tries: Option, - format: Option, - output_file: Option, + summary: Option, ulimit: Option, scan_order: Option, command: Option>, @@ -274,7 +266,7 @@ impl Config { let config: Config = match toml::from_str(&content) { Ok(config) => config, Err(e) => { - println!("Found {} in configuration file.\nAborting scan.\n", e); + eprintln!("Found {} in configuration file.\nAborting scan.\n", e); std::process::exit(1); } }; @@ -285,7 +277,7 @@ impl Config { #[cfg(test)] mod tests { - use super::{Config, Opts, OutputFormat, PortRange, ScanOrder, ScriptsRequired}; + use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired}; impl Config { fn default() -> Self { Self { @@ -297,8 +289,7 @@ mod tests { timeout: Some(1_000), tries: Some(1), ulimit: None, - format: None, - output_file: None, + summary: None, command: Some(vec!["-A".to_owned()]), accessible: Some(true), scan_order: Some(ScanOrder::Random), @@ -320,8 +311,7 @@ mod tests { ulimit: None, command: vec![], accessible: false, - format: OutputFormat::Text, - output_file: None, + summary: None, scan_order: ScanOrder::Serial, no_config: true, top: false, diff --git a/src/main.rs b/src/main.rs index 7806cfc31..71b5777f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ extern crate shell_words; mod tui; mod input; -use input::{Config, Opts, OutputFormat, PortRange, ScanOrder, ScriptsRequired}; +use input::{Config, Opts, PortRange, ScanOrder, SummaryFormat}; mod scanner; use scanner::Scanner; @@ -28,7 +28,7 @@ use itertools::Itertools; use rlimit::{getrlimit, setrlimit, RawRlim, Resource, Rlim}; use std::collections::HashMap; use std::convert::TryInto; -use std::fs::{self, File}; +use std::fs::File; use std::io::{prelude::*, BufReader}; use std::net::{IpAddr, ToSocketAddrs}; use std::path::Path; @@ -129,25 +129,13 @@ fn main() { warning!(x, opts.greppable, opts.accessible); } - if opts.greppable - || opts.scripts == ScriptsRequired::None - || opts.format != OutputFormat::Text - || opts.output_file.is_some() - { - let output_string = format_summary(&ports_per_ip, opts.format); - - if let Some(ref path) = opts.output_file { - if let Err(e) = fs::write(path, &output_string) { - eprintln!("Failed to write output to file at '{}': {}", path, e); - println!("Have your output anyway:\n{}", output_string); - return; - } - } else { - println!("{}", output_string); - } - } - - if !opts.greppable && opts.scripts != ScriptsRequired::None { + if let Some(summary_format) = opts.summary { + let summary = generate_summary(&ports_per_ip, summary_format); + std::io::stdout() + .lock() + .write_all(summary.as_bytes()) + .expect("Could not write summary to stdout."); + } else { let mut script_bench = NamedTimer::start("Scripts"); for (ip, ports) in &ports_per_ip { detail!("Starting Script(s)", opts.greppable, opts.accessible); @@ -215,12 +203,12 @@ fn print_opening(opts: &Opts) { | .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ | `-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-' The Modern Day Port Scanner."#; - println!("{}", s.gradient(Color::Green).bold()); + eprintln!("{}", s.gradient(Color::Green).bold()); let info = r#"________________________________________ : https://discord.gg/GFrQsGy : : https://github.com/RustScan/RustScan : --------------------------------------"#; - println!("{}", info.gradient(Color::Yellow).bold()); + eprintln!("{}", info.gradient(Color::Yellow).bold()); funny_opening!(); let config_path = dirs::home_dir() @@ -280,9 +268,9 @@ fn parse_addresses(input: &Opts) -> Vec { } /// Generates a summary in the given format. -fn format_summary(ports_per_ip: &HashMap>, out_format: OutputFormat) -> String { +fn generate_summary(ports_per_ip: &HashMap>, out_format: SummaryFormat) -> String { match out_format { - OutputFormat::Text => ports_per_ip + SummaryFormat::Text => ports_per_ip .iter() .map(|(ip, ports)| { format!( @@ -292,7 +280,7 @@ fn format_summary(ports_per_ip: &HashMap>, out_format: OutputFo ) }) .join("\n"), - OutputFormat::Json => { + SummaryFormat::Json => { serde_json::to_string(&ports_per_ip).expect("Failed to serialize results as JSON.") } } diff --git a/src/scanner/mod.rs b/src/scanner/mod.rs index 7fe2f9aae..3da54fd54 100644 --- a/src/scanner/mod.rs +++ b/src/scanner/mod.rs @@ -117,9 +117,9 @@ impl Scanner { } if !self.greppable { if self.accessible { - println!("Open {}", socket.to_string()); + eprintln!("Open {}", socket.to_string()); } else { - println!("Open {}", socket.to_string().purple()); + eprintln!("Open {}", socket.to_string().purple()); } } diff --git a/src/tui.rs b/src/tui.rs index e06129b61..56a56a2a5 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -5,7 +5,7 @@ macro_rules! warning { ($name:expr) => { use ansi_term::Colour::Red; - println!("{} {}", Red.bold().paint("[!]"), $name); + eprintln!("{} {}", Red.bold().paint("[!]"), $name); }; ($name:expr, $greppable:expr, $accessible:expr) => { use ansi_term::Colour::Red; @@ -13,9 +13,9 @@ macro_rules! warning { if !$greppable { if $accessible { // Don't print the ascii art - println!("{}", $name); + eprintln!("{}", $name); } else { - println!("{} {}", Red.bold().paint("[!]"), $name); + eprintln!("{} {}", Red.bold().paint("[!]"), $name); } } }; @@ -25,7 +25,7 @@ macro_rules! warning { macro_rules! detail { ($name:expr) => { use ansi_term::Colour::Blue; - println!("{} {}", Blue.bold().paint("[~]"), $name); + eprintln!("{} {}", Blue.bold().paint("[~]"), $name); }; ($name:expr, $greppable:expr, $accessible:expr) => { use ansi_term::Colour::Blue; @@ -33,9 +33,9 @@ macro_rules! detail { if !$greppable { if $accessible { // Don't print the ascii art - println!("{}", $name); + eprintln!("{}", $name); } else { - println!("{} {}", Blue.bold().paint("[~]"), $name); + eprintln!("{} {}", Blue.bold().paint("[~]"), $name); } } }; @@ -75,7 +75,7 @@ macro_rules! funny_opening { ]; let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap(); - println!("{}\n", random_quote); + eprintln!("{}\n", random_quote); // println!("{} {}", RGB(0, 255, 9).bold().paint("[>]"), $name); }; }