1 #![deny(clippy::all)]
2 #![warn(clippy::pedantic)]
3 #![allow(clippy::doc_markdown, clippy::if_not_else, clippy::non_ascii_literal)]
4 
5 extern crate shell_words;
6 
7 mod tui;
8 
9 mod input;
10 use input::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
11 
12 mod scanner;
13 use scanner::Scanner;
14 
15 mod port_strategy;
16 use port_strategy::PortStrategy;
17 
18 mod benchmark;
19 use benchmark::{Benchmark, NamedTimer};
20 
21 mod scripts;
22 use scripts::{init_scripts, Script, ScriptFile};
23 
24 use cidr_utils::cidr::IpCidr;
25 use colorful::{Color, Colorful};
26 use futures::executor::block_on;
27 use rlimit::{getrlimit, setrlimit, RawRlim, Resource, Rlim};
28 use std::collections::HashMap;
29 use std::convert::TryInto;
30 use std::fs::File;
31 use std::io::{prelude::*, BufReader};
32 use std::net::{IpAddr, ToSocketAddrs};
33 use std::path::Path;
34 use std::string::ToString;
35 use std::time::Duration;
36 use trust_dns_resolver::{
37     config::{ResolverConfig, ResolverOpts},
38     Resolver,
39 };
40 
41 extern crate colorful;
42 extern crate dirs;
43 
44 // Average value for Ubuntu
45 const DEFAULT_FILE_DESCRIPTORS_LIMIT: RawRlim = 8000;
46 // Safest batch size based on experimentation
47 const AVERAGE_BATCH_SIZE: RawRlim = 3000;
48 
49 #[macro_use]
50 extern crate log;
51 
52 #[cfg(not(tarpaulin_include))]
53 #[allow(clippy::too_many_lines)]
54 /// Faster Nmap scanning with Rust
55 /// If you're looking for the actual scanning, check out the module Scanner
main()56 fn main() {
57     env_logger::init();
58     let mut benchmarks = Benchmark::init();
59     let mut rustscan_bench = NamedTimer::start("RustScan");
60 
61     let mut opts: Opts = Opts::read();
62     let config = Config::read();
63     opts.merge(&config);
64 
65     debug!("Main() `opts` arguments are {:?}", opts);
66 
67     let scripts_to_run: Vec<ScriptFile> = match init_scripts(opts.scripts) {
68         Ok(scripts_to_run) => scripts_to_run,
69         Err(e) => {
70             warning!(
71                 format!("Initiating scripts failed!\n{}", e.to_string()),
72                 opts.greppable,
73                 opts.accessible
74             );
75             std::process::exit(1);
76         }
77     };
78 
79     debug!("Scripts initialized {:?}", &scripts_to_run);
80 
81     if !opts.greppable && !opts.accessible {
82         print_opening(&opts);
83     }
84 
85     let ips: Vec<IpAddr> = parse_addresses(&opts);
86 
87     if ips.is_empty() {
88         warning!(
89             "No IPs could be resolved, aborting scan.",
90             opts.greppable,
91             opts.accessible
92         );
93         std::process::exit(1);
94     }
95 
96     let ulimit: RawRlim = adjust_ulimit_size(&opts);
97     let batch_size: u16 = infer_batch_size(&opts, ulimit);
98 
99     let scanner = Scanner::new(
100         &ips,
101         batch_size,
102         Duration::from_millis(opts.timeout.into()),
103         opts.tries,
104         opts.greppable,
105         PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
106         opts.accessible,
107     );
108     debug!("Scanner finished building: {:?}", scanner);
109 
110     let mut portscan_bench = NamedTimer::start("Portscan");
111     let scan_result = block_on(scanner.run());
112     portscan_bench.end();
113     benchmarks.push(portscan_bench);
114 
115     let mut ports_per_ip = HashMap::new();
116 
117     for socket in scan_result {
118         ports_per_ip
119             .entry(socket.ip())
120             .or_insert_with(Vec::new)
121             .push(socket.port());
122     }
123 
124     for ip in ips {
125         if ports_per_ip.contains_key(&ip) {
126             continue;
127         }
128 
129         // If we got here it means the IP was not found within the HashMap, this
130         // means the scan couldn't find any open ports for it.
131 
132         let x = format!("Looks like I didn't find any open ports for {:?}. This is usually caused by a high batch size.
133         \n*I used {} batch size, consider lowering it with {} or a comfortable number for your system.
134         \n Alternatively, increase the timeout if your ping is high. Rustscan -t 2000 for 2000 milliseconds (2s) timeout.\n",
135         ip,
136         opts.batch_size,
137         "'rustscan -b <batch_size> <ip address>'");
138         warning!(x, opts.greppable, opts.accessible);
139     }
140 
141     let mut script_bench = NamedTimer::start("Scripts");
142     for (ip, ports) in &ports_per_ip {
143         let vec_str_ports: Vec<String> = ports.iter().map(ToString::to_string).collect();
144 
145         // nmap port style is 80,443. Comma separated with no spaces.
146         let ports_str = vec_str_ports.join(",");
147 
148         // if option scripts is none, no script will be spawned
149         if opts.greppable || opts.scripts == ScriptsRequired::None {
150             println!("{} -> [{}]", &ip, ports_str);
151             continue;
152         }
153         detail!("Starting Script(s)", opts.greppable, opts.accessible);
154 
155         // Run all the scripts we found and parsed based on the script config file tags field.
156         for mut script_f in scripts_to_run.clone() {
157             output!(
158                 format!("Script to be run {:?}\n", script_f.call_format,),
159                 opts.greppable,
160                 opts.accessible
161             );
162 
163             // This part allows us to add commandline arguments to the Script call_format, appending them to the end of the command.
164             if !opts.command.is_empty() {
165                 let user_extra_args: Vec<String> = shell_words::split(&opts.command.join(" "))
166                     .expect("Failed to parse extra user commandline arguments");
167                 if script_f.call_format.is_some() {
168                     let mut call_f = script_f.call_format.unwrap();
169                     call_f.push_str(&format!(" {}", &user_extra_args.join(" ")));
170                     script_f.call_format = Some(call_f);
171                 }
172             }
173 
174             // Building the script with the arguments from the ScriptFile, and ip-ports.
175             let script = Script::build(
176                 script_f.path,
177                 *ip,
178                 ports.to_vec(),
179                 script_f.port,
180                 script_f.ports_separator,
181                 script_f.tags,
182                 script_f.call_format,
183             );
184             match script.run() {
185                 Ok(script_result) => {
186                     detail!(script_result.to_string(), opts.greppable, opts.accessible);
187                 }
188                 Err(e) => {
189                     warning!(
190                         &format!("Error {}", e.to_string()),
191                         opts.greppable,
192                         opts.accessible
193                     );
194                 }
195             }
196         }
197     }
198 
199     // To use the runtime benchmark, run the process as: RUST_LOG=info ./rustscan
200     script_bench.end();
201     benchmarks.push(script_bench);
202     rustscan_bench.end();
203     benchmarks.push(rustscan_bench);
204     debug!("Benchmarks raw {:?}", benchmarks);
205     info!("{}", benchmarks.summary());
206 }
207 
208 /// Prints the opening title of RustScan
print_opening(opts: &Opts)209 fn print_opening(opts: &Opts) {
210     debug!("Printing opening");
211     let s = r#".----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
212 | {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
213 | .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
214 `-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-'
215 The Modern Day Port Scanner."#;
216     println!("{}", s.gradient(Color::Green).bold());
217     let info = r#"________________________________________
218 : https://discord.gg/GFrQsGy           :
219 : https://github.com/RustScan/RustScan :
220  --------------------------------------"#;
221     println!("{}", info.gradient(Color::Yellow).bold());
222     funny_opening!();
223 
224     let config_path = dirs::home_dir()
225         .expect("Could not infer config file path.")
226         .join(".rustscan.toml");
227 
228     detail!(
229         format!("The config file is expected to be at {:?}", config_path),
230         opts.greppable,
231         opts.accessible
232     );
233 }
234 
235 /// Goes through all possible IP inputs (files or via argparsing)
236 /// Parses the string(s) into IPs
parse_addresses(input: &Opts) -> Vec<IpAddr>237 fn parse_addresses(input: &Opts) -> Vec<IpAddr> {
238     let mut ips: Vec<IpAddr> = Vec::new();
239     let mut unresolved_addresses: Vec<&str> = Vec::new();
240     let backup_resolver =
241         Resolver::new(ResolverConfig::cloudflare_tls(), ResolverOpts::default()).unwrap();
242 
243     for address in &input.addresses {
244         let parsed_ips = parse_address(address, &backup_resolver);
245         if !parsed_ips.is_empty() {
246             ips.extend(parsed_ips);
247         } else {
248             unresolved_addresses.push(address);
249         }
250     }
251 
252     // If we got to this point this can only be a file path or the wrong input.
253     for file_path in unresolved_addresses {
254         let file_path = Path::new(file_path);
255 
256         if !file_path.is_file() {
257             warning!(
258                 format!("Host {:?} could not be resolved.", file_path),
259                 input.greppable,
260                 input.accessible
261             );
262 
263             continue;
264         }
265 
266         if let Ok(x) = read_ips_from_file(file_path, &backup_resolver) {
267             ips.extend(x);
268         } else {
269             warning!(
270                 format!("Host {:?} could not be resolved.", file_path),
271                 input.greppable,
272                 input.accessible
273             );
274         }
275     }
276 
277     ips
278 }
279 
280 /// Given a string, parse it as an host, IP address, or CIDR.
281 /// This allows us to pass files as hosts or cidr or IPs easily
282 /// Call this everytime you have a possible IP_or_host
parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr>283 fn parse_address(address: &str, resolver: &Resolver) -> Vec<IpAddr> {
284     IpCidr::from_str(&address)
285         .map(|cidr| cidr.iter().collect())
286         .ok()
287         .or_else(|| {
288             format!("{}:{}", &address, 80)
289                 .to_socket_addrs()
290                 .ok()
291                 .map(|mut iter| vec![iter.next().unwrap().ip()])
292         })
293         .unwrap_or_else(|| resolve_ips_from_host(address, resolver))
294 }
295 
296 /// Uses DNS to get the IPS assiocated with host
resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr>297 fn resolve_ips_from_host(source: &str, backup_resolver: &Resolver) -> Vec<IpAddr> {
298     let mut ips: Vec<std::net::IpAddr> = Vec::new();
299 
300     if let Ok(addrs) = source.to_socket_addrs() {
301         for ip in addrs {
302             ips.push(ip.ip());
303         }
304     } else if let Ok(addrs) = backup_resolver.lookup_ip(&source) {
305         ips.extend(addrs.iter());
306     }
307 
308     ips
309 }
310 
311 #[cfg(not(tarpaulin_include))]
312 /// Parses an input file of IPs and uses those
read_ips_from_file( ips: &std::path::Path, backup_resolver: &Resolver, ) -> Result<Vec<std::net::IpAddr>, std::io::Error>313 fn read_ips_from_file(
314     ips: &std::path::Path,
315     backup_resolver: &Resolver,
316 ) -> Result<Vec<std::net::IpAddr>, std::io::Error> {
317     let file = File::open(ips)?;
318     let reader = BufReader::new(file);
319 
320     let mut ips: Vec<std::net::IpAddr> = Vec::new();
321 
322     for address_line in reader.lines() {
323         if let Ok(address) = address_line {
324             ips.extend(parse_address(&address, backup_resolver));
325         } else {
326             debug!("Line in file is not valid");
327         }
328     }
329 
330     Ok(ips)
331 }
332 
adjust_ulimit_size(opts: &Opts) -> RawRlim333 fn adjust_ulimit_size(opts: &Opts) -> RawRlim {
334     if opts.ulimit.is_some() {
335         let limit: Rlim = Rlim::from_raw(opts.ulimit.unwrap());
336 
337         if setrlimit(Resource::NOFILE, limit, limit).is_ok() {
338             detail!(
339                 format!("Automatically increasing ulimit value to {}.", limit),
340                 opts.greppable,
341                 opts.accessible
342             );
343         } else {
344             warning!(
345                 "ERROR. Failed to set ulimit value.",
346                 opts.greppable,
347                 opts.accessible
348             );
349         }
350     }
351 
352     let (rlim, _) = getrlimit(Resource::NOFILE).unwrap();
353 
354     rlim.as_raw()
355 }
356 
infer_batch_size(opts: &Opts, ulimit: RawRlim) -> u16357 fn infer_batch_size(opts: &Opts, ulimit: RawRlim) -> u16 {
358     let mut batch_size: RawRlim = opts.batch_size.into();
359 
360     // Adjust the batch size when the ulimit value is lower than the desired batch size
361     if ulimit < batch_size {
362         warning!("File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers",
363             opts.greppable, opts.accessible
364         );
365 
366         // When the OS supports high file limits like 8000, but the user
367         // selected a batch size higher than this we should reduce it to
368         // a lower number.
369         if ulimit < AVERAGE_BATCH_SIZE {
370             // ulimit is smaller than aveage batch size
371             // user must have very small ulimit
372             // decrease batch size to half of ulimit
373             warning!("Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'. ", opts.greppable, opts.accessible);
374             info!("Halving batch_size because ulimit is smaller than average batch size");
375             batch_size = ulimit / 2
376         } else if ulimit > DEFAULT_FILE_DESCRIPTORS_LIMIT {
377             info!("Batch size is now average batch size");
378             batch_size = AVERAGE_BATCH_SIZE
379         } else {
380             batch_size = ulimit - 100
381         }
382     }
383     // When the ulimit is higher than the batch size let the user know that the
384     // batch size can be increased unless they specified the ulimit themselves.
385     else if ulimit + 2 > batch_size && (opts.ulimit.is_none()) {
386         detail!(format!("File limit higher than batch size. Can increase speed by increasing batch size '-b {}'.", ulimit - 100),
387         opts.greppable, opts.accessible);
388     }
389 
390     batch_size
391         .try_into()
392         .expect("Couldn't fit the batch size into a u16.")
393 }
394 
395 #[cfg(test)]
396 mod tests {
397     use crate::{adjust_ulimit_size, infer_batch_size, parse_addresses, print_opening, Opts};
398     use std::net::Ipv4Addr;
399 
400     #[test]
batch_size_lowered()401     fn batch_size_lowered() {
402         let mut opts = Opts::default();
403         opts.batch_size = 50_000;
404         let batch_size = infer_batch_size(&opts, 120);
405 
406         assert!(batch_size < opts.batch_size);
407     }
408 
409     #[test]
batch_size_lowered_average_size()410     fn batch_size_lowered_average_size() {
411         let mut opts = Opts::default();
412         opts.batch_size = 50_000;
413         let batch_size = infer_batch_size(&opts, 9_000);
414 
415         assert!(batch_size == 3_000);
416     }
417     #[test]
batch_size_equals_ulimit_lowered()418     fn batch_size_equals_ulimit_lowered() {
419         // because ulimit and batch size are same size, batch size is lowered
420         // to ULIMIT - 100
421         let mut opts = Opts::default();
422         opts.batch_size = 50_000;
423         let batch_size = infer_batch_size(&opts, 5_000);
424 
425         assert!(batch_size == 4_900);
426     }
427     #[test]
batch_size_adjusted_2000()428     fn batch_size_adjusted_2000() {
429         // ulimit == batch_size
430         let mut opts = Opts::default();
431         opts.batch_size = 50_000;
432         opts.ulimit = Some(2_000);
433         let batch_size = adjust_ulimit_size(&opts);
434 
435         assert!(batch_size == 2_000);
436     }
437     #[test]
test_print_opening_no_panic()438     fn test_print_opening_no_panic() {
439         let mut opts = Opts::default();
440         opts.ulimit = Some(2_000);
441         // print opening should not panic
442         print_opening(&opts);
443     }
444 
445     #[test]
test_high_ulimit_no_greppable_mode()446     fn test_high_ulimit_no_greppable_mode() {
447         let mut opts = Opts::default();
448         opts.batch_size = 10;
449         opts.greppable = false;
450 
451         let batch_size = infer_batch_size(&opts, 1_000_000);
452 
453         assert!(batch_size == opts.batch_size);
454     }
455 
456     #[test]
parse_correct_addresses()457     fn parse_correct_addresses() {
458         let mut opts = Opts::default();
459         opts.addresses = vec!["127.0.0.1".to_owned(), "192.168.0.0/30".to_owned()];
460         let ips = parse_addresses(&opts);
461 
462         assert_eq!(
463             ips,
464             [
465                 Ipv4Addr::new(127, 0, 0, 1),
466                 Ipv4Addr::new(192, 168, 0, 0),
467                 Ipv4Addr::new(192, 168, 0, 1),
468                 Ipv4Addr::new(192, 168, 0, 2),
469                 Ipv4Addr::new(192, 168, 0, 3)
470             ]
471         );
472     }
473 
474     #[test]
parse_correct_host_addresses()475     fn parse_correct_host_addresses() {
476         let mut opts = Opts::default();
477         opts.addresses = vec!["google.com".to_owned()];
478         let ips = parse_addresses(&opts);
479 
480         assert_eq!(ips.len(), 1);
481     }
482 
483     #[test]
parse_correct_and_incorrect_addresses()484     fn parse_correct_and_incorrect_addresses() {
485         let mut opts = Opts::default();
486         opts.addresses = vec!["127.0.0.1".to_owned(), "im_wrong".to_owned()];
487         let ips = parse_addresses(&opts);
488 
489         assert_eq!(ips, [Ipv4Addr::new(127, 0, 0, 1),]);
490     }
491 
492     #[test]
parse_incorrect_addresses()493     fn parse_incorrect_addresses() {
494         let mut opts = Opts::default();
495         opts.addresses = vec!["im_wrong".to_owned(), "300.10.1.1".to_owned()];
496         let ips = parse_addresses(&opts);
497 
498         assert_eq!(ips.is_empty(), true);
499     }
500     #[test]
parse_hosts_file_and_incorrect_hosts()501     fn parse_hosts_file_and_incorrect_hosts() {
502         // Host file contains IP, Hosts, incorrect IPs, incorrect hosts
503         let mut opts = Opts::default();
504         opts.addresses = vec!["fixtures/hosts.txt".to_owned()];
505         let ips = parse_addresses(&opts);
506         assert_eq!(ips.len(), 3);
507     }
508 
509     #[test]
parse_empty_hosts_file()510     fn parse_empty_hosts_file() {
511         // Host file contains IP, Hosts, incorrect IPs, incorrect hosts
512         let mut opts = Opts::default();
513         opts.addresses = vec!["fixtures/empty_hosts.txt".to_owned()];
514         let ips = parse_addresses(&opts);
515         assert_eq!(ips.len(), 0);
516     }
517 
518     #[test]
parse_naughty_host_file()519     fn parse_naughty_host_file() {
520         // Host file contains IP, Hosts, incorrect IPs, incorrect hosts
521         let mut opts = Opts::default();
522         opts.addresses = vec!["fixtures/naughty_string.txt".to_owned()];
523         let ips = parse_addresses(&opts);
524         assert_eq!(ips.len(), 0);
525     }
526 }
527