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