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..38683cf3f 100644 --- a/src/input.rs +++ b/src/input.rs @@ -30,6 +30,15 @@ arg_enum! { } } +arg_enum! { + /// Specifies how to format the summary. + #[derive(Deserialize, Debug, StructOpt, Clone, Copy, PartialEq)] + pub enum SummaryFormat { + Text, + Json, + } +} + /// Represents the range of ports to be scanned. #[derive(Deserialize, Debug, Clone, PartialEq)] pub struct PortRange { @@ -94,8 +103,12 @@ pub struct Opts { #[structopt(long)] pub accessible: bool, + /// 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 + /// 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")] @@ -198,7 +211,7 @@ impl Opts { self.ports = Some(ports); } - merge_optional!(range, ulimit); + merge_optional!(range, summary, ulimit); } } @@ -216,6 +229,7 @@ pub struct Config { batch_size: Option, timeout: Option, tries: Option, + summary: Option, ulimit: Option, scan_order: Option, command: Option>, @@ -252,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); } }; @@ -275,6 +289,7 @@ mod tests { timeout: Some(1_000), tries: Some(1), ulimit: None, + summary: None, command: Some(vec!["-A".to_owned()]), accessible: Some(true), scan_order: Some(ScanOrder::Random), @@ -296,6 +311,7 @@ mod tests { ulimit: None, command: vec![], accessible: false, + summary: None, scan_order: ScanOrder::Serial, no_config: true, top: false, diff --git a/src/main.rs b/src/main.rs index 645db332a..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, PortRange, ScanOrder, ScriptsRequired}; +use input::{Config, Opts, PortRange, ScanOrder, SummaryFormat}; mod scanner; use scanner::Scanner; @@ -24,6 +24,7 @@ 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; @@ -128,67 +129,66 @@ 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; - } - 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 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); + + // 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); @@ -203,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() @@ -267,6 +267,25 @@ fn parse_addresses(input: &Opts) -> Vec { ips } +/// Generates a summary in the given format. +fn generate_summary(ports_per_ip: &HashMap>, out_format: SummaryFormat) -> String { + match out_format { + SummaryFormat::Text => ports_per_ip + .iter() + .map(|(ip, ports)| { + format!( + "{} -> [{}]", + ip, + ports.iter().map(ToString::to_string).join(",") + ) + }) + .join("\n"), + SummaryFormat::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 +362,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 +386,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 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); }; }