1use crate::core::module::{Module, ModuleError, ModuleResult};
2use async_trait::async_trait;
3use icebox_macro::module;
4use std::sync::Arc;
5use tokio::sync::Semaphore;
6
7use crate::modules::hex_encode;
8
9#[module(
10 name = "ping_sweep",
11 kind = "Scanner",
12 description = "Discover live hosts on the local network via ICMP ping sweep",
13 author = "ICEBOX"
14)]
15pub struct ArpScanner {
16 #[option(
17 required = true,
18 help = "Target range: CIDR (192.168.1.0/24), range (192.168.1.1-100), or list (192.168.1.1,192.168.1.2)"
19 )]
20 pub targets: String,
21 #[option(help = "Timeout per host in seconds (default 2)")]
22 pub timeout_secs: u64,
23 #[option(help = "Maximum concurrent probes (default 50)")]
24 pub concurrency: usize,
25}
26
27fn parse_target_list(spec: &str) -> Result<Vec<String>, ModuleError> {
28 let mut ips = Vec::new();
29 for part in spec.split(',') {
30 let part = part.trim();
31 if part.is_empty() {
32 continue;
33 }
34 if let Some((base, cidr)) = part.split_once('/') {
35 let prefix: u8 = cidr
36 .parse()
37 .map_err(|_| ModuleError::Parse(format!("bad CIDR: {cidr}")))?;
38 let base_ip: std::net::Ipv4Addr = base
39 .parse()
40 .map_err(|_| ModuleError::Parse(format!("bad IP: {base}")))?;
41 let bits = 32u32 - prefix as u32;
42 let count = 1u32 << bits;
43 let start = u32::from(base_ip) & (0xFFFFFFFFu32 << bits);
44 let network = std::net::Ipv4Addr::from(start);
45 let broadcast = std::net::Ipv4Addr::from(start | (count - 1));
46 for i in 1..count - 1 {
47 let ip = std::net::Ipv4Addr::from(start + i);
48 if ip != network && ip != broadcast {
49 ips.push(ip.to_string());
50 }
51 }
52 } else if let Some((lo, hi)) = part.split_once('-') {
53 let base_parts: Vec<&str> = lo.split('.').collect();
54 if base_parts.len() != 4 {
55 return Err(ModuleError::Parse("bad IP range format".into()));
56 }
57 let lo_octet: u8 = lo
58 .rsplit_once('.')
59 .map(|(_, o)| o)
60 .unwrap_or("")
61 .parse()
62 .map_err(|_| ModuleError::Parse("bad range".into()))?;
63 let hi_octet: u8 = hi
64 .trim()
65 .parse()
66 .map_err(|_| ModuleError::Parse("bad range".into()))?;
67 let prefix = lo.rsplit_once('.').map(|(p, _)| p).unwrap_or("");
68 for i in lo_octet..=hi_octet {
69 ips.push(format!("{prefix}.{i}"));
70 }
71 } else {
72 ips.push(part.to_string());
73 }
74 }
75 if ips.is_empty() {
76 return Err(ModuleError::Other("no targets specified".into()));
77 }
78 Ok(ips)
79}
80
81#[async_trait]
82impl Module for ArpScanner {
83 fn options_json(&self) -> serde_json::Value {
84 serde_json::to_value(&ArpScannerOptions {
85 targets: self.targets.clone(),
86 timeout_secs: self.timeout_secs,
87 concurrency: self.concurrency,
88 })
89 .unwrap_or(serde_json::Value::Null)
90 }
91
92 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
93 let mut o = ArpScannerOptions {
94 targets: self.targets.clone(),
95 timeout_secs: self.timeout_secs,
96 concurrency: self.concurrency,
97 };
98 o.set(name, value)?;
99 self.targets = o.targets;
100 self.timeout_secs = o.timeout_secs;
101 self.concurrency = o.concurrency;
102 Ok(())
103 }
104
105 fn validate(&self) -> Result<(), ModuleError> {
106 ArpScannerOptions {
107 targets: self.targets.clone(),
108 timeout_secs: self.timeout_secs,
109 concurrency: self.concurrency,
110 }
111 .validate()?;
112 parse_target_list(&self.targets).map(|_| ())
113 }
114
115 async fn run(&self) -> Result<ModuleResult, ModuleError> {
116 let ips = parse_target_list(&self.targets)?;
117 let timeout = if self.timeout_secs > 0 {
118 self.timeout_secs
119 } else {
120 2
121 };
122 let max_concurrency = if self.concurrency > 0 {
123 self.concurrency
124 } else {
125 50
126 };
127 let semaphore = Arc::new(Semaphore::new(max_concurrency));
128
129 let mut handles = Vec::new();
130 for ip in ips {
131 let permit = semaphore
132 .clone()
133 .acquire_owned()
134 .await
135 .map_err(|e| ModuleError::Other(e.to_string()))?;
136 handles.push(tokio::spawn(async move {
137 let _permit = permit;
138 let result = tokio::process::Command::new("ping")
139 .arg("-c")
140 .arg("1")
141 .arg("-W")
142 .arg(timeout.to_string())
143 .arg(&ip)
144 .output()
145 .await;
146 match result {
147 Ok(out) if out.status.success() => Some(ip),
148 _ => None,
149 }
150 }));
151 }
152
153 let mut live_hosts: Vec<String> = Vec::new();
154 for h in handles {
155 if let Ok(Some(ip)) = h.await {
156 live_hosts.push(ip);
157 }
158 }
159 live_hosts.sort();
160 live_hosts.dedup();
161
162 let finding = if live_hosts.is_empty() {
163 "No live hosts found".to_string()
164 } else {
165 format!(
166 "Found {} live host(s): {}",
167 live_hosts.len(),
168 live_hosts.join(", ")
169 )
170 };
171
172 Ok(ModuleResult {
173 success: true,
174 finding: Some(finding),
175 evidence: live_hosts.iter().map(|h| format!("host/{h}")).collect(),
176 data: serde_json::json!({
177 "targets": self.targets,
178 "live_hosts": live_hosts,
179 "count": live_hosts.len(),
180 }),
181 ..Default::default()
182 })
183 }
184}
185
186#[module(
187 name = "smb_scanner",
188 kind = "Scanner",
189 description = "SMB service scanner - version detection and null session check",
190 author = "ICEBOX"
191)]
192pub struct SmbScanner {
193 #[option(required = true, help = "Target IP or hostname")]
194 pub host: String,
195 #[option(help = "SMB port (default 445)")]
196 pub port: u16,
197 #[option(help = "Timeout in milliseconds (default 5000)")]
198 pub timeout_ms: u64,
199 #[option(help = "Check null session on IPC$ (default true)")]
200 pub check_null_session: bool,
201}
202
203fn build_smb_negotiate() -> Vec<u8> {
204 let mut pkt = Vec::new();
205 pkt.extend_from_slice(b"\xFFSMB");
206 pkt.push(0x72);
207 pkt.extend_from_slice(&[0x00; 4]);
208 pkt.push(0x18);
209 pkt.extend_from_slice(&[0x01, 0x48]);
210 pkt.extend_from_slice(&[0x00; 12]);
211 pkt.extend_from_slice(&[0x00; 2]);
212 pkt.extend_from_slice(&[0x00; 2]);
213 pkt.extend_from_slice(&[0x00; 2]);
214 pkt.extend_from_slice(&[0x00; 2]);
215 pkt.push(0x00);
216 let dialects = b"\x02\x0BNT LM 0.12\x02\x08SMB 2.002";
217 let bcc = dialects.len() as u16;
218 pkt.extend_from_slice(&bcc.to_le_bytes());
219 pkt.extend_from_slice(dialects);
220 pkt
221}
222
223fn parse_smb_negotiate_response(data: &[u8]) -> (Option<String>, Option<u16>) {
224 if data.len() < 36 || data[0..4] != [0xFF, b'S', b'M', b'B'] {
225 return (None, None);
226 }
227 let cmd = data[4];
228 if cmd != 0x72 {
229 return (None, None);
230 }
231 let mut status_bytes = [0u8; 4];
232 status_bytes.copy_from_slice(&data[5..9]);
233 let status = u32::from_le_bytes(status_bytes);
234 if status != 0 {
235 return (Some("SMB present (non-zero status)".into()), None);
236 }
237 let wct = data[32] as usize;
238 if wct == 0 || data.len() < 33 + wct * 2 + 2 {
239 return (Some("SMBv1 present".into()), None);
240 }
241 if wct >= 1 {
242 let dialect_idx = u16::from_le_bytes([data[33], data[34]]);
243 if dialect_idx == 0xFFFF {
244 return (Some("SMBv1 present (no dialect selected)".into()), None);
245 }
246 let bcc_start = 33 + wct * 2;
247 if data.len() > bcc_start + 1 {
248 let bcc = u16::from_le_bytes([data[bcc_start], data[bcc_start + 1]]);
249 let version = if wct == 1 {
250 if data.len() >= 33 + wct * 2 + 2 + 4 {
251 let dialect_rev =
252 u16::from_le_bytes([data[bcc_start + 2 + 32], data[bcc_start + 2 + 33]]);
253 Some(format!("SMBv2 (dialect 0x{dialect_rev:04x})"))
254 } else {
255 Some("SMBv2 present".into())
256 }
257 } else {
258 let sec_mode = data[35];
259 let caps_start = 33 + 2 + 4 + 2 + 2 + 4 + 4;
260 if data.len() > caps_start + 3 {
261 let caps = u32::from_le_bytes([
262 data[caps_start],
263 data[caps_start + 1],
264 data[caps_start + 2],
265 data[caps_start + 3],
266 ]);
267 Some(format!(
268 "SMBv1 (security=0x{sec_mode:02x}, caps=0x{caps:08x})"
269 ))
270 } else {
271 Some(format!("SMBv1 (security=0x{sec_mode:02x})"))
272 }
273 };
274 return (version, Some(bcc));
275 }
276 }
277 (Some("SMB present".into()), None)
278}
279
280#[async_trait]
281impl Module for SmbScanner {
282 fn options_json(&self) -> serde_json::Value {
283 serde_json::to_value(&SmbScannerOptions {
284 host: self.host.clone(),
285 port: self.port,
286 timeout_ms: self.timeout_ms,
287 check_null_session: self.check_null_session,
288 })
289 .unwrap_or(serde_json::Value::Null)
290 }
291
292 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
293 let mut o = SmbScannerOptions {
294 host: self.host.clone(),
295 port: self.port,
296 timeout_ms: self.timeout_ms,
297 check_null_session: self.check_null_session,
298 };
299 o.set(name, value)?;
300 self.host = o.host;
301 self.port = o.port;
302 self.timeout_ms = o.timeout_ms;
303 self.check_null_session = o.check_null_session;
304 Ok(())
305 }
306
307 fn validate(&self) -> Result<(), ModuleError> {
308 SmbScannerOptions {
309 host: self.host.clone(),
310 port: self.port,
311 timeout_ms: self.timeout_ms,
312 check_null_session: self.check_null_session,
313 }
314 .validate()
315 }
316
317 async fn run(&self) -> Result<ModuleResult, ModuleError> {
318 let port = if self.port > 0 { self.port } else { 445 };
319 let timeout = if self.timeout_ms > 0 {
320 std::time::Duration::from_millis(self.timeout_ms)
321 } else {
322 std::time::Duration::from_secs(5)
323 };
324 let addr = crate::core::proxy::resolve_dial(&self.host, port);
325
326 let stream =
327 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
328 Ok(Ok(s)) => s,
329 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
330 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
331 };
332
333 let req = build_smb_negotiate();
334 if tokio::time::timeout(timeout, stream.writable())
335 .await
336 .is_err()
337 {
338 return Err(ModuleError::Other("write timeout".into()));
339 }
340 let _ = stream.try_write(&req);
341
342 let mut buf = vec![0u8; 4096];
343 let n = match tokio::time::timeout(timeout, stream.readable()).await {
344 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
345 _ => 0,
346 };
347 if n < 36 {
348 return Ok(ModuleResult {
349 success: true,
350 finding: Some("SMB: no valid response received".into()),
351 data: serde_json::json!({"host": self.host, "port": port}),
352 ..Default::default()
353 });
354 }
355
356 let (version, _bcc) = parse_smb_negotiate_response(&buf[..n]);
357 let mut evidence = Vec::new();
358 let mut findings = Vec::new();
359
360 if let Some(ref ver) = version {
361 evidence.push(format!("smb/{ver}"));
362 findings.push(ver.clone());
363 } else {
364 evidence.push("smb/unrecognized".into());
365 findings.push("SMB service detected (unrecognized version)".into());
366 }
367
368 let null_session = if self.check_null_session {
369 let tcon_req = build_smb_treeconnect(&format!("\\\\{}\\IPC$", self.host), 0);
370 let _ = stream.try_write(&tcon_req);
371 let mut buf2 = vec![0u8; 1024];
372 let n2 = match tokio::time::timeout(timeout, stream.readable()).await {
373 Ok(Ok(_)) => stream.try_read(&mut buf2).unwrap_or(0),
374 _ => 0,
375 };
376 if n2 >= 36 && buf2[5..9] == [0x00, 0x00, 0x00, 0x00] {
377 findings.push("NULL session: IPC$ accessible (likely vulnerable)".into());
378 evidence.push("smb/null_session/ipc".into());
379 true
380 } else {
381 false
382 }
383 } else {
384 false
385 };
386
387 let mut finding_str = findings.join("; ");
388 if null_session {
389 finding_str.push_str(" - WARNING: Null session allowed!");
390 }
391
392 Ok(ModuleResult {
393 success: true,
394 finding: Some(finding_str),
395 evidence,
396 data: serde_json::json!({
397 "host": self.host,
398 "port": port,
399 "version": version,
400 "null_session": null_session,
401 }),
402 ..Default::default()
403 })
404 }
405}
406
407fn build_smb_treeconnect(path: &str, uid: u16) -> Vec<u8> {
408 let mut pkt = Vec::new();
409 pkt.extend_from_slice(b"\xFFSMB");
410 pkt.push(0x75);
411 pkt.extend_from_slice(&[0x00; 4]);
412 pkt.push(0x18);
413 pkt.extend_from_slice(&[0x01, 0x48]);
414 pkt.extend_from_slice(&[0x00; 12]);
415 pkt.extend_from_slice(&[0x00; 2]);
416 pkt.extend_from_slice(&[0x00; 2]);
417 pkt.extend_from_slice(&uid.to_le_bytes());
418 pkt.extend_from_slice(&[0x00; 2]);
419 pkt.push(0x04);
420 pkt.extend_from_slice(&[0x00; 2]);
421 pkt.extend_from_slice(&[0x00; 2]);
422 let path_bytes = path.as_bytes();
423 let bcc = 1 + 4 + path_bytes.len();
424 pkt.extend_from_slice(&(bcc as u16).to_le_bytes());
425 pkt.push(0x04);
426 pkt.extend_from_slice(&(path_bytes.len() as u16).to_le_bytes());
427 pkt.extend_from_slice(path_bytes);
428 pkt
429}
430
431#[module(
432 name = "ftp_scanner",
433 kind = "Scanner",
434 description = "FTP service scanner - anonymous login check and banner grab",
435 author = "ICEBOX"
436)]
437pub struct FtpScanner {
438 #[option(required = true, help = "Target IP or hostname")]
439 pub host: String,
440 #[option(help = "FTP port (default 21)")]
441 pub port: u16,
442 #[option(help = "Timeout in milliseconds (default 5000)")]
443 pub timeout_ms: u64,
444 #[option(help = "Try anonymous login (default true)")]
445 pub check_anonymous: bool,
446}
447
448#[async_trait]
449impl Module for FtpScanner {
450 fn options_json(&self) -> serde_json::Value {
451 serde_json::to_value(&FtpScannerOptions {
452 host: self.host.clone(),
453 port: self.port,
454 timeout_ms: self.timeout_ms,
455 check_anonymous: self.check_anonymous,
456 })
457 .unwrap_or(serde_json::Value::Null)
458 }
459
460 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
461 let mut o = FtpScannerOptions {
462 host: self.host.clone(),
463 port: self.port,
464 timeout_ms: self.timeout_ms,
465 check_anonymous: self.check_anonymous,
466 };
467 o.set(name, value)?;
468 self.host = o.host;
469 self.port = o.port;
470 self.timeout_ms = o.timeout_ms;
471 self.check_anonymous = o.check_anonymous;
472 Ok(())
473 }
474
475 fn validate(&self) -> Result<(), ModuleError> {
476 FtpScannerOptions {
477 host: self.host.clone(),
478 port: self.port,
479 timeout_ms: self.timeout_ms,
480 check_anonymous: self.check_anonymous,
481 }
482 .validate()
483 }
484
485 async fn run(&self) -> Result<ModuleResult, ModuleError> {
486 let port = if self.port > 0 { self.port } else { 21 };
487 let timeout = std::time::Duration::from_millis(if self.timeout_ms > 0 {
488 self.timeout_ms
489 } else {
490 5000
491 });
492 let addr = crate::core::proxy::resolve_dial(&self.host, port);
493
494 let stream =
495 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
496 Ok(Ok(s)) => s,
497 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
498 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
499 };
500
501 async fn read_response(
502 s: &tokio::net::TcpStream,
503 t: std::time::Duration,
504 ) -> Result<String, ModuleError> {
505 let mut buf = vec![0u8; 4096];
506 let n = match tokio::time::timeout(t, s.readable()).await {
507 Ok(Ok(_)) => s.try_read(&mut buf).unwrap_or(0),
508 _ => return Err(ModuleError::Other("read timeout".into())),
509 };
510 Ok(String::from_utf8_lossy(&buf[..n.min(4000)]).to_string())
511 }
512
513 async fn send_command(
514 s: &tokio::net::TcpStream,
515 cmd: &[u8],
516 t: std::time::Duration,
517 ) -> Result<(), ModuleError> {
518 match tokio::time::timeout(t, s.writable()).await {
519 Ok(Ok(_)) => {
520 s.try_write(cmd)
521 .map_err(|e| ModuleError::Other(format!("write: {e}")))?;
522 Ok(())
523 }
524 _ => Err(ModuleError::Other("write timeout".into())),
525 }
526 }
527
528 let banner = read_response(&stream, timeout).await.unwrap_or_default();
529 let banner_short = banner.lines().next().unwrap_or(&banner).trim().to_string();
530
531 let mut findings = Vec::new();
532 let mut evidence = Vec::new();
533 findings.push(format!("banner: {banner_short}"));
534
535 let anonymous_success = if self.check_anonymous {
536 send_command(&stream, b"USER anonymous\r\n", timeout)
537 .await
538 .ok();
539 let user_resp = read_response(&stream, timeout).await.unwrap_or_default();
540 let is_331 = user_resp.starts_with("331");
541 if is_331 {
542 send_command(&stream, b"PASS anonymous@\r\n", timeout)
543 .await
544 .ok();
545 let pass_resp = read_response(&stream, timeout).await.unwrap_or_default();
546 let logged_in = pass_resp.starts_with("230");
547 if logged_in {
548 send_command(&stream, b"SYST\r\n", timeout).await.ok();
549 let syst = read_response(&stream, timeout).await.unwrap_or_default();
550 let syst_line = syst.lines().next().unwrap_or("").trim().to_string();
551 findings.push(format!("anonymous login: SUCCESS (SYST: {syst_line})"));
552 evidence.push("ftp/anonymous_login".into());
553
554 send_command(&stream, b"PWD\r\n", timeout).await.ok();
555 let pwd = read_response(&stream, timeout).await.unwrap_or_default();
556 let pwd_line = pwd.lines().next().unwrap_or("").trim().to_string();
557 if !pwd_line.is_empty() {
558 evidence.push(format!("ftp/pwd: {pwd_line}"));
559 }
560 true
561 } else {
562 let err = pass_resp.lines().next().unwrap_or("").trim().to_string();
563 findings.push(format!("anonymous login: FAILED ({err})"));
564 false
565 }
566 } else {
567 findings.push("anonymous login: not supported (no 331)".into());
568 false
569 }
570 } else {
571 false
572 };
573
574 let _ = stream.try_write(b"QUIT\r\n");
575
576 let finding = if anonymous_success {
577 format!(
578 "FTP on tcp/{port}: {} - anonymous login enabled!",
579 banner_short
580 )
581 } else {
582 format!("FTP on tcp/{port}: {}", findings.join("; "))
583 };
584
585 Ok(ModuleResult {
586 success: true,
587 finding: Some(finding),
588 evidence,
589 data: serde_json::json!({
590 "host": self.host,
591 "port": port,
592 "banner": banner_short,
593 "anonymous_login": anonymous_success,
594 }),
595 ..Default::default()
596 })
597 }
598}
599
600#[module(
601 name = "ssh_scanner",
602 kind = "Scanner",
603 description = "SSH service scanner - version detection and auth method enumeration",
604 author = "ICEBOX"
605)]
606pub struct SshScanner {
607 #[option(required = true, help = "Target IP or hostname")]
608 pub host: String,
609 #[option(help = "SSH port (default 22)")]
610 pub port: u16,
611 #[option(help = "Timeout in milliseconds (default 5000)")]
612 pub timeout_ms: u64,
613}
614
615#[async_trait]
616impl Module for SshScanner {
617 fn options_json(&self) -> serde_json::Value {
618 serde_json::to_value(&SshScannerOptions {
619 host: self.host.clone(),
620 port: self.port,
621 timeout_ms: self.timeout_ms,
622 })
623 .unwrap_or(serde_json::Value::Null)
624 }
625
626 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
627 let mut o = SshScannerOptions {
628 host: self.host.clone(),
629 port: self.port,
630 timeout_ms: self.timeout_ms,
631 };
632 o.set(name, value)?;
633 self.host = o.host;
634 self.port = o.port;
635 self.timeout_ms = o.timeout_ms;
636 Ok(())
637 }
638
639 fn validate(&self) -> Result<(), ModuleError> {
640 SshScannerOptions {
641 host: self.host.clone(),
642 port: self.port,
643 timeout_ms: self.timeout_ms,
644 }
645 .validate()
646 }
647
648 async fn run(&self) -> Result<ModuleResult, ModuleError> {
649 let port = if self.port > 0 { self.port } else { 22 };
650 let timeout = std::time::Duration::from_millis(if self.timeout_ms > 0 {
651 self.timeout_ms
652 } else {
653 5000
654 });
655 let addr = crate::core::proxy::resolve_dial(&self.host, port);
656
657 let stream =
658 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
659 Ok(Ok(s)) => s,
660 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
661 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
662 };
663
664 let mut buf = vec![0u8; 4096];
665 let n = match tokio::time::timeout(timeout, stream.readable()).await {
666 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
667 _ => 0,
668 };
669
670 if n == 0 {
671 return Ok(ModuleResult {
672 success: true,
673 finding: Some(format!("SSH on tcp/{port}: no banner received")),
674 data: serde_json::json!({"host": self.host, "port": port}),
675 ..Default::default()
676 });
677 }
678
679 let raw = &buf[..n.min(1024)];
680 let resp = String::from_utf8_lossy(raw).to_string();
681 let banner = resp.lines().next().unwrap_or("").trim().to_string();
682
683 let has_kex = n > banner.len() + 1;
684
685 let mut evidence = Vec::new();
686 evidence.push(format!("ssh/banner: {banner}"));
687
688 let finding = if banner.starts_with("SSH-") {
689 format!("SSH on tcp/{port}: {banner}")
690 } else {
691 format!("SSH on tcp/{port}: unrecognized banner: {banner}")
692 };
693
694 Ok(ModuleResult {
695 success: true,
696 finding: Some(finding),
697 evidence,
698 data: serde_json::json!({
699 "host": self.host,
700 "port": port,
701 "banner": banner,
702 "has_kex_init": has_kex,
703 "raw_hex": hex_encode(&buf[..n.min(128)]),
704 }),
705 ..Default::default()
706 })
707 }
708}
709
710const SSH_COMMON_CREDS: &[(&str, &str)] = &[
711 ("root", "root"),
712 ("root", "admin"),
713 ("root", "toor"),
714 ("root", "password"),
715 ("root", "1234"),
716 ("root", "1"),
717 ("root", "!"),
718 ("root", "changeme"),
719 ("admin", "admin"),
720 ("admin", "password"),
721 ("admin", "1234"),
722 ("admin", "admin123"),
723 ("administrator", "administrator"),
724 ("administrator", "password"),
725 ("user", "user"),
726 ("user", "password"),
727 ("user", "1234"),
728 ("test", "test"),
729 ("guest", "guest"),
730 ("pi", "raspberry"),
731 ("ubuntu", "ubuntu"),
732 ("debian", "debian"),
733 ("oracle", "oracle"),
734 ("postgres", "postgres"),
735 ("nagios", "nagios"),
736 ("jenkins", "jenkins"),
737];
738
739#[module(
740 name = "ssh_bruteforce",
741 kind = "Scanner",
742 capabilities = "CredentialAccess",
743 intent = "Dump",
744 impact = "Critical",
745 description = "SSH credential bruteforcer - test username/password pairs via sshpass",
746 author = "ICEBOX"
747)]
748pub struct SshBruteforce {
749 #[option(required = true, help = "Target IP or hostname")]
750 pub host: String,
751 #[option(help = "SSH port (default 22)")]
752 pub port: u16,
753 #[option(
754 help = "Credentials in user:pass format, comma-separated (default: built-in wordlist)"
755 )]
756 pub wordlist: String,
757 #[option(help = "Timeout per attempt in seconds (default 5)")]
758 pub timeout_secs: u64,
759 #[option(help = "Maximum concurrent attempts (default 10)")]
760 pub concurrency: usize,
761}
762
763fn parse_creds(wordlist: &str) -> Vec<(String, String)> {
764 if wordlist.is_empty() {
765 return SSH_COMMON_CREDS
766 .iter()
767 .map(|(u, p)| (u.to_string(), p.to_string()))
768 .collect();
769 }
770 wordlist
771 .split(',')
772 .filter_map(|pair| {
773 let pair = pair.trim();
774 if let Some((u, p)) = pair.split_once(':') {
775 let u = u.trim();
776 let p = p.trim();
777 if !u.is_empty() {
778 Some((u.to_string(), p.to_string()))
779 } else {
780 None
781 }
782 } else {
783 None
784 }
785 })
786 .collect()
787}
788
789#[async_trait]
790impl Module for SshBruteforce {
791 fn options_json(&self) -> serde_json::Value {
792 serde_json::to_value(&SshBruteforceOptions {
793 host: self.host.clone(),
794 port: self.port,
795 wordlist: self.wordlist.clone(),
796 timeout_secs: self.timeout_secs,
797 concurrency: self.concurrency,
798 })
799 .unwrap_or(serde_json::Value::Null)
800 }
801
802 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
803 let mut o = SshBruteforceOptions {
804 host: self.host.clone(),
805 port: self.port,
806 wordlist: self.wordlist.clone(),
807 timeout_secs: self.timeout_secs,
808 concurrency: self.concurrency,
809 };
810 o.set(name, value)?;
811 self.host = o.host;
812 self.port = o.port;
813 self.wordlist = o.wordlist;
814 self.timeout_secs = o.timeout_secs;
815 self.concurrency = o.concurrency;
816 Ok(())
817 }
818
819 fn validate(&self) -> Result<(), ModuleError> {
820 SshBruteforceOptions {
821 host: self.host.clone(),
822 port: self.port,
823 wordlist: self.wordlist.clone(),
824 timeout_secs: self.timeout_secs,
825 concurrency: self.concurrency,
826 }
827 .validate()
828 }
829
830 async fn run(&self) -> Result<ModuleResult, ModuleError> {
831 let port = if self.port > 0 { self.port } else { 22 };
832 let timeout_secs = if self.timeout_secs > 0 {
833 self.timeout_secs
834 } else {
835 5
836 };
837 let max_concurrency = if self.concurrency > 0 {
838 self.concurrency
839 } else {
840 10
841 };
842 let creds = parse_creds(&self.wordlist);
843 let host = self.host.clone();
844
845 let has_sshpass = tokio::process::Command::new("which")
846 .arg("sshpass")
847 .output()
848 .await
849 .ok()
850 .is_some_and(|o| o.status.success());
851
852 if !has_sshpass {
853 return Err(ModuleError::Other(
854 "sshpass not found. Install with: brew install sshpass (macOS) or apt install sshpass (Linux)".into()
855 ));
856 }
857
858 let semaphore = Arc::new(Semaphore::new(max_concurrency));
859 let mut handles = Vec::new();
860
861 for (user, pass) in &creds {
862 let permit = match semaphore.clone().acquire_owned().await {
863 Ok(p) => p,
864 Err(_) => continue,
865 };
866 let user = user.clone();
867 let pass = pass.clone();
868 let host = host.clone();
869 handles.push(tokio::spawn(async move {
870 let _permit = permit;
871 let result = tokio::process::Command::new("sshpass")
872 .arg("-p")
873 .arg(&pass)
874 .arg("ssh")
875 .arg("-o")
876 .arg("StrictHostKeyChecking=no")
877 .arg("-o")
878 .arg("UserKnownHostsFile=/dev/null")
879 .arg("-o")
880 .arg(format!("ConnectTimeout={timeout_secs}"))
881 .arg("-p")
882 .arg(port.to_string())
883 .arg(format!("{user}@{host}"))
884 .arg("id")
885 .output()
886 .await;
887 match result {
888 Ok(out) if out.status.success() => {
889 let id_str = String::from_utf8_lossy(&out.stdout).trim().to_string();
890 Some((user, pass, id_str))
891 }
892 _ => None,
893 }
894 }));
895 }
896
897 let mut found: Vec<serde_json::Value> = Vec::new();
898 for h in handles {
899 if let Ok(Some((user, pass, id_str))) = h.await {
900 found.push(serde_json::json!({
901 "username": user,
902 "password": pass,
903 "id": id_str,
904 }));
905 }
906 }
907
908 let evidence: Vec<String> = found
909 .iter()
910 .map(|c| {
911 let u = c["username"].as_str().unwrap_or("");
912 let p = c["password"].as_str().unwrap_or("");
913 format!("ssh/cred:{u}:{p}")
914 })
915 .collect();
916
917 let finding = if found.is_empty() {
918 format!(
919 "SSH bruteforce on tcp/{port}: no valid credentials found (tried {} pairs)",
920 creds.len()
921 )
922 } else {
923 let details: Vec<String> = found
924 .iter()
925 .map(|c| {
926 let u = c["username"].as_str().unwrap_or("");
927 let p = c["password"].as_str().unwrap_or("");
928 format!("{u}:{p}")
929 })
930 .collect();
931 format!(
932 "SSH bruteforce on tcp/{port}: FOUND {} valid credential(s): {}",
933 found.len(),
934 details.join(", ")
935 )
936 };
937
938 Ok(ModuleResult {
939 success: !found.is_empty(),
940 finding: Some(finding),
941 evidence,
942 data: serde_json::json!({
943 "host": self.host,
944 "port": port,
945 "found_credentials": found,
946 "tried_count": creds.len(),
947 }),
948 ..Default::default()
949 })
950 }
951}
952
953#[module(
954 name = "ftp_bruteforce",
955 kind = "Scanner",
956 capabilities = "CredentialAccess",
957 intent = "Dump",
958 impact = "Critical",
959 description = "FTP credential bruteforcer - test username/password pairs via FTP protocol",
960 author = "ICEBOX"
961)]
962pub struct FtpBruteforce {
963 #[option(required = true, help = "Target IP or hostname")]
964 pub host: String,
965 #[option(help = "FTP port (default 21)")]
966 pub port: u16,
967 #[option(
968 help = "Credentials in user:pass format, comma-separated (default: built-in wordlist)"
969 )]
970 pub wordlist: String,
971 #[option(help = "Timeout per attempt in milliseconds (default 5000)")]
972 pub timeout_ms: u64,
973 #[option(help = "Maximum concurrent attempts (default 10)")]
974 pub concurrency: usize,
975}
976
977#[async_trait]
978impl Module for FtpBruteforce {
979 fn options_json(&self) -> serde_json::Value {
980 serde_json::to_value(&FtpBruteforceOptions {
981 host: self.host.clone(),
982 port: self.port,
983 wordlist: self.wordlist.clone(),
984 timeout_ms: self.timeout_ms,
985 concurrency: self.concurrency,
986 })
987 .unwrap_or(serde_json::Value::Null)
988 }
989
990 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
991 let mut o = FtpBruteforceOptions {
992 host: self.host.clone(),
993 port: self.port,
994 wordlist: self.wordlist.clone(),
995 timeout_ms: self.timeout_ms,
996 concurrency: self.concurrency,
997 };
998 o.set(name, value)?;
999 self.host = o.host;
1000 self.port = o.port;
1001 self.wordlist = o.wordlist;
1002 self.timeout_ms = o.timeout_ms;
1003 self.concurrency = o.concurrency;
1004 Ok(())
1005 }
1006
1007 fn validate(&self) -> Result<(), ModuleError> {
1008 FtpBruteforceOptions {
1009 host: self.host.clone(),
1010 port: self.port,
1011 wordlist: self.wordlist.clone(),
1012 timeout_ms: self.timeout_ms,
1013 concurrency: self.concurrency,
1014 }
1015 .validate()
1016 }
1017
1018 async fn run(&self) -> Result<ModuleResult, ModuleError> {
1019 let port = if self.port > 0 { self.port } else { 21 };
1020 let timeout = std::time::Duration::from_millis(if self.timeout_ms > 0 {
1021 self.timeout_ms
1022 } else {
1023 5000
1024 });
1025 let max_concurrency = if self.concurrency > 0 {
1026 self.concurrency
1027 } else {
1028 10
1029 };
1030 let creds = parse_creds(&self.wordlist);
1031 let host = self.host.clone();
1032
1033 async fn try_ftp_login(
1034 host: &str,
1035 port: u16,
1036 user: &str,
1037 pass: &str,
1038 timeout: std::time::Duration,
1039 ) -> Option<(String, String)> {
1040 let addr = crate::core::proxy::resolve_dial(host, port);
1041 let stream = tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr))
1042 .await
1043 .ok()?
1044 .ok()?;
1045 let mut buf = vec![0u8; 4096];
1046 let n = tokio::time::timeout(timeout, stream.readable())
1047 .await
1048 .ok()?
1049 .map(|_| stream.try_read(&mut buf).unwrap_or(0))
1050 .unwrap_or(0);
1051 if n == 0 {
1052 return None;
1053 }
1054 let _ = stream
1055 .try_write(format!("USER {user}\r\n").as_bytes())
1056 .ok()?;
1057 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1058 let mut buf2 = vec![0u8; 4096];
1059 let n2 = tokio::time::timeout(timeout, stream.readable())
1060 .await
1061 .ok()?
1062 .map(|_| stream.try_read(&mut buf2).unwrap_or(0))
1063 .unwrap_or(0);
1064 let user_resp = String::from_utf8_lossy(&buf2[..n2.min(4000)]);
1065 if !user_resp.starts_with("331") && !user_resp.starts_with("230") {
1066 return None;
1067 }
1068 let _ = stream
1069 .try_write(format!("PASS {pass}\r\n").as_bytes())
1070 .ok()?;
1071 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1072 let mut buf3 = vec![0u8; 4096];
1073 let n3 = tokio::time::timeout(timeout, stream.readable())
1074 .await
1075 .ok()?
1076 .map(|_| stream.try_read(&mut buf3).unwrap_or(0))
1077 .unwrap_or(0);
1078 let pass_resp = String::from_utf8_lossy(&buf3[..n3.min(4000)]);
1079 if pass_resp.starts_with("230") {
1080 Some((user.to_string(), pass.to_string()))
1081 } else {
1082 None
1083 }
1084 }
1085
1086 let semaphore = Arc::new(Semaphore::new(max_concurrency));
1087 let mut handles = Vec::new();
1088 for (user, pass) in &creds {
1089 let permit = semaphore
1090 .clone()
1091 .acquire_owned()
1092 .await
1093 .map_err(|e| ModuleError::Other(e.to_string()))?;
1094 let host = host.clone();
1095 let user = user.clone();
1096 let pass = pass.clone();
1097 handles.push(tokio::spawn(async move {
1098 let _permit = permit;
1099 try_ftp_login(&host, port, &user, &pass, timeout).await
1100 }));
1101 }
1102
1103 let mut found: Vec<(String, String)> = Vec::new();
1104 for h in handles {
1105 if let Ok(Some((u, p))) = h.await {
1106 found.push((u, p));
1107 }
1108 }
1109
1110 let evidence: Vec<String> = found
1111 .iter()
1112 .map(|(u, p)| format!("ftp/cred:{u}:{p}"))
1113 .collect();
1114 let finding = if found.is_empty() {
1115 format!(
1116 "FTP bruteforce on tcp/{port}: no valid credentials found (tried {} pairs)",
1117 creds.len()
1118 )
1119 } else {
1120 let details: Vec<String> = found.iter().map(|(u, p)| format!("{u}:{p}")).collect();
1121 format!(
1122 "FTP bruteforce on tcp/{port}: FOUND {} valid credential(s): {}",
1123 found.len(),
1124 details.join(", ")
1125 )
1126 };
1127
1128 Ok(ModuleResult {
1129 success: !found.is_empty(),
1130 finding: Some(finding),
1131 evidence,
1132 data: serde_json::json!({
1133 "host": self.host,
1134 "port": port,
1135 "found_credentials": found,
1136 "tried_count": creds.len(),
1137 }),
1138 ..Default::default()
1139 })
1140 }
1141}
1142
1143fn rdp_build_neg_req(requested: u32) -> Vec<u8> {
1144 let mut pkt = Vec::with_capacity(8);
1145 pkt.push(0x01);
1146 pkt.push(0x00);
1147 pkt.extend_from_slice(&[0x08, 0x00]);
1148 pkt.extend_from_slice(&requested.to_le_bytes());
1149 pkt
1150}
1151
1152fn rdp_parse_neg_rsp(data: &[u8]) -> Option<(u8, u32)> {
1153 if data.len() < 8 {
1154 return None;
1155 }
1156 if data[0] != 0x02 && data[0] != 0x03 {
1157 return None;
1158 }
1159 let r#type = data[0];
1160 let _flags = data[1];
1161 let len = u16::from_le_bytes([data[2], data[3]]);
1162 if len < 8 {
1163 return None;
1164 }
1165 let selected = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
1166 Some((r#type, selected))
1167}
1168
1169const PROTOCOL_RDP: u32 = 0x01;
1170const PROTOCOL_SSL: u32 = 0x02;
1171const PROTOCOL_NLA: u32 = 0x08;
1172
1173#[module(
1174 name = "rdp_scanner",
1175 kind = "Scanner",
1176 description = "RDP service scanner - version detection and NLA/SSL mode check",
1177 author = "ICEBOX"
1178)]
1179pub struct RdpScanner {
1180 #[option(required = true, help = "Target IP or hostname")]
1181 pub host: String,
1182 #[option(help = "RDP port (default 3389)")]
1183 pub port: u16,
1184 #[option(help = "Timeout in milliseconds (default 5000)")]
1185 pub timeout_ms: u64,
1186}
1187
1188#[async_trait]
1189impl Module for RdpScanner {
1190 fn options_json(&self) -> serde_json::Value {
1191 serde_json::to_value(&RdpScannerOptions {
1192 host: self.host.clone(),
1193 port: self.port,
1194 timeout_ms: self.timeout_ms,
1195 })
1196 .unwrap_or(serde_json::Value::Null)
1197 }
1198
1199 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
1200 let mut o = RdpScannerOptions {
1201 host: self.host.clone(),
1202 port: self.port,
1203 timeout_ms: self.timeout_ms,
1204 };
1205 o.set(name, value)?;
1206 self.host = o.host;
1207 self.port = o.port;
1208 self.timeout_ms = o.timeout_ms;
1209 Ok(())
1210 }
1211
1212 fn validate(&self) -> Result<(), ModuleError> {
1213 RdpScannerOptions {
1214 host: self.host.clone(),
1215 port: self.port,
1216 timeout_ms: self.timeout_ms,
1217 }
1218 .validate()
1219 }
1220
1221 async fn run(&self) -> Result<ModuleResult, ModuleError> {
1222 let port = if self.port > 0 { self.port } else { 3389 };
1223 let timeout_ms = if self.timeout_ms > 0 {
1224 self.timeout_ms
1225 } else {
1226 5000
1227 };
1228 let timeout = std::time::Duration::from_millis(timeout_ms);
1229 let addr = crate::core::proxy::resolve_dial(&self.host, port);
1230
1231 let stream =
1232 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
1233 Ok(Ok(s)) => s,
1234 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
1235 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
1236 };
1237
1238 let req = rdp_build_neg_req(PROTOCOL_RDP | PROTOCOL_SSL | PROTOCOL_NLA);
1239 if tokio::time::timeout(timeout, stream.writable())
1240 .await
1241 .is_err()
1242 {
1243 return Err(ModuleError::Other("write timeout".into()));
1244 }
1245 let _ = stream.try_write(&req);
1246
1247 let mut buf = vec![0u8; 1024];
1248 let n = match tokio::time::timeout(timeout, stream.readable()).await {
1249 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
1250 _ => 0,
1251 };
1252
1253 if n < 8 {
1254 return Ok(ModuleResult {
1255 success: true,
1256 finding: Some(format!("RDP on tcp/{port}: no response")),
1257 data: serde_json::json!({"host": self.host, "port": port}),
1258 ..Default::default()
1259 });
1260 }
1261
1262 let (rsp_type, selected) = rdp_parse_neg_rsp(&buf[..n]).unwrap_or((0, 0));
1263
1264 let proto_name = match selected {
1265 1 => "RDP (standard)",
1266 2 => "SSL/TLS",
1267 8 => "NLA (CredSSP)",
1268 0 => "FAILED",
1269 _ => "unknown",
1270 };
1271
1272 let finding = format!(
1273 "RDP on tcp/{port}: selected={proto_name} (0x{selected:x}), type=0x{rsp_type:x}"
1274 );
1275
1276 Ok(ModuleResult {
1277 success: true,
1278 finding: Some(finding),
1279 evidence: vec![format!("rdp/protocol:{proto_name}")],
1280 data: serde_json::json!({
1281 "host": self.host, "port": port,
1282 "selected_protocol": selected,
1283 "protocol_name": proto_name,
1284 "response_type": rsp_type,
1285 }),
1286 ..Default::default()
1287 })
1288 }
1289}
1290
1291#[module(
1292 name = "vnc_scanner",
1293 kind = "Scanner",
1294 description = "VNC service scanner - protocol version and authentication type detection",
1295 author = "ICEBOX"
1296)]
1297pub struct VncScanner {
1298 #[option(required = true, help = "Target IP or hostname")]
1299 pub host: String,
1300 #[option(help = "VNC port (default 5900)")]
1301 pub port: u16,
1302 #[option(help = "Timeout in milliseconds (default 5000)")]
1303 pub timeout_ms: u64,
1304 #[option(help = "Check VNC authentication support (default true)")]
1305 pub check_vnc_auth: bool,
1306}
1307
1308#[async_trait]
1309impl Module for VncScanner {
1310 fn options_json(&self) -> serde_json::Value {
1311 serde_json::to_value(&VncScannerOptions {
1312 host: self.host.clone(),
1313 port: self.port,
1314 timeout_ms: self.timeout_ms,
1315 check_vnc_auth: self.check_vnc_auth,
1316 })
1317 .unwrap_or(serde_json::Value::Null)
1318 }
1319
1320 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
1321 let mut o = VncScannerOptions {
1322 host: self.host.clone(),
1323 port: self.port,
1324 timeout_ms: self.timeout_ms,
1325 check_vnc_auth: self.check_vnc_auth,
1326 };
1327 o.set(name, value)?;
1328 self.host = o.host;
1329 self.port = o.port;
1330 self.timeout_ms = o.timeout_ms;
1331 self.check_vnc_auth = o.check_vnc_auth;
1332 Ok(())
1333 }
1334
1335 fn validate(&self) -> Result<(), ModuleError> {
1336 VncScannerOptions {
1337 host: self.host.clone(),
1338 port: self.port,
1339 timeout_ms: self.timeout_ms,
1340 check_vnc_auth: self.check_vnc_auth,
1341 }
1342 .validate()
1343 }
1344
1345 async fn run(&self) -> Result<ModuleResult, ModuleError> {
1346 let port = if self.port > 0 { self.port } else { 5900 };
1347 let timeout_ms = if self.timeout_ms > 0 {
1348 self.timeout_ms
1349 } else {
1350 5000
1351 };
1352 let timeout = std::time::Duration::from_millis(timeout_ms);
1353 let addr = crate::core::proxy::resolve_dial(&self.host, port);
1354
1355 let stream =
1356 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
1357 Ok(Ok(s)) => s,
1358 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
1359 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
1360 };
1361
1362 let mut buf = vec![0u8; 4096];
1363 let n = match tokio::time::timeout(timeout, stream.readable()).await {
1364 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
1365 _ => 0,
1366 };
1367 if n == 0 {
1368 return Ok(ModuleResult {
1369 success: true,
1370 finding: Some(format!("VNC on tcp/{port}: no protocol version received")),
1371 data: serde_json::json!({"host": self.host, "port": port}),
1372 ..Default::default()
1373 });
1374 }
1375
1376 let raw = String::from_utf8_lossy(&buf[..n.min(256)]);
1377 let banner = raw.lines().next().unwrap_or("").trim().to_string();
1378
1379 let mut findings = Vec::new();
1380 let mut evidence = Vec::new();
1381
1382 if banner.starts_with("RFB") {
1383 findings.push(format!("version: {banner}"));
1384 evidence.push(format!("vnc/version:{banner}"));
1385
1386 let ver_str = if banner == "RFB 003.008\n" || banner == "RFB 003.008" {
1387 "RFB 003.008\n"
1388 } else {
1389 "RFB 003.003\n"
1390 };
1391 if tokio::time::timeout(timeout, stream.writable())
1392 .await
1393 .is_ok()
1394 {
1395 let _ = stream.try_write(ver_str.as_bytes());
1396 }
1397
1398 let n2 = match tokio::time::timeout(timeout, stream.readable()).await {
1399 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
1400 _ => 0,
1401 };
1402 if n2 >= 1 {
1403 let sec_count = buf[0] as usize;
1404 if sec_count > 0 && n2 > sec_count {
1405 let types: Vec<u8> = buf[1..1 + sec_count].to_vec();
1406 let type_names: Vec<String> = types
1407 .iter()
1408 .map(|&t| match t {
1409 1 => "None".into(),
1410 2 => "VNC Auth".into(),
1411 5 => "RA2".into(),
1412 6 => "RA2ne".into(),
1413 16 => "Tight".into(),
1414 17 => "Ultra".into(),
1415 18 => "TLS".into(),
1416 19 => "VeNCrypt".into(),
1417 20 => "GTK-VNC SASL".into(),
1418 21 => "MD5".into(),
1419 22 => "Colin 64".into(),
1420 _ => format!("Unknown(0x{t:x})"),
1421 })
1422 .collect();
1423 findings.push(format!("auth types: {}", type_names.join(", ")));
1424 evidence.extend(types.iter().map(|&t| format!("vnc/auth:0x{t:x}")));
1425 if types.contains(&1) {
1426 findings.push("No authentication required!".into());
1427 evidence.push("vnc/no_auth".into());
1428 }
1429 } else if sec_count == 0 && n2 >= 4 {
1430 let auth_type = u32::from_le_bytes([buf[1], buf[2], buf[3], buf[4]]);
1431 let name = match auth_type {
1432 1 => "None",
1433 2 => "VNC Auth",
1434 _ => "unknown",
1435 };
1436 findings.push(format!("auth: {name} (0x{auth_type:x})"));
1437 evidence.push(format!("vnc/auth:0x{auth_type:x}"));
1438 }
1439 }
1440 } else {
1441 findings.push(format!("unrecognized banner: {banner}"));
1442 evidence.push(format!("vnc/banner:{}", hex_encode(&buf[..n.min(64)])));
1443 }
1444
1445 Ok(ModuleResult {
1446 success: true,
1447 finding: Some(format!("VNC on tcp/{port}: {}", findings.join("; "))),
1448 evidence,
1449 data: serde_json::json!({
1450 "host": self.host, "port": port,
1451 "banner": banner,
1452 "findings": findings,
1453 }),
1454 ..Default::default()
1455 })
1456 }
1457}
1458
1459const IAC: u8 = 0xFF;
1460const TN_SB: u8 = 0xFA;
1461const TN_SE: u8 = 0xF0;
1462
1463fn strip_telnet_negotiation(data: &[u8]) -> Vec<u8> {
1464 let mut out = Vec::new();
1465 let mut i = 0;
1466 while i < data.len() {
1467 if data[i] == IAC && i + 2 < data.len() {
1468 match data[i + 1] {
1469 TN_SB => {
1470 let mut j = i + 2;
1471 while j + 1 < data.len() {
1472 if data[j] == IAC && data[j + 1] == TN_SE {
1473 i = j + 2;
1474 break;
1475 }
1476 j += 1;
1477 }
1478 if j + 1 >= data.len() {
1479 i = data.len();
1480 }
1481 }
1482 _ => {
1483 i += 3;
1484 }
1485 }
1486 } else if data[i] >= 32 || data[i] == b'\n' || data[i] == b'\r' || data[i] == b'\t' {
1487 out.push(data[i]);
1488 i += 1;
1489 } else {
1490 i += 1;
1491 }
1492 }
1493 out
1494}
1495
1496#[module(
1497 name = "telnet_scanner",
1498 kind = "Scanner",
1499 description = "Telnet service scanner - banner grabbing and option negotiation detection",
1500 author = "ICEBOX"
1501)]
1502pub struct TelnetScanner {
1503 #[option(required = true, help = "Target IP or hostname")]
1504 pub host: String,
1505 #[option(help = "Telnet port (default 23)")]
1506 pub port: u16,
1507 #[option(help = "Timeout in milliseconds (default 5000)")]
1508 pub timeout_ms: u64,
1509}
1510
1511#[async_trait]
1512impl Module for TelnetScanner {
1513 fn options_json(&self) -> serde_json::Value {
1514 serde_json::to_value(&TelnetScannerOptions {
1515 host: self.host.clone(),
1516 port: self.port,
1517 timeout_ms: self.timeout_ms,
1518 })
1519 .unwrap_or(serde_json::Value::Null)
1520 }
1521
1522 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
1523 let mut o = TelnetScannerOptions {
1524 host: self.host.clone(),
1525 port: self.port,
1526 timeout_ms: self.timeout_ms,
1527 };
1528 o.set(name, value)?;
1529 self.host = o.host;
1530 self.port = o.port;
1531 self.timeout_ms = o.timeout_ms;
1532 Ok(())
1533 }
1534
1535 fn validate(&self) -> Result<(), ModuleError> {
1536 TelnetScannerOptions {
1537 host: self.host.clone(),
1538 port: self.port,
1539 timeout_ms: self.timeout_ms,
1540 }
1541 .validate()
1542 }
1543
1544 async fn run(&self) -> Result<ModuleResult, ModuleError> {
1545 let port = if self.port > 0 { self.port } else { 23 };
1546 let timeout_ms = if self.timeout_ms > 0 {
1547 self.timeout_ms
1548 } else {
1549 5000
1550 };
1551 let timeout = std::time::Duration::from_millis(timeout_ms);
1552 let addr = crate::core::proxy::resolve_dial(&self.host, port);
1553
1554 let stream =
1555 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
1556 Ok(Ok(s)) => s,
1557 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
1558 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
1559 };
1560
1561 let mut all_data = Vec::new();
1562 let mut buf = vec![0u8; 8192];
1563 loop {
1564 let n = match tokio::time::timeout(timeout, stream.readable()).await {
1565 Ok(Ok(_)) => match stream.try_read(&mut buf) {
1566 Ok(n) => n,
1567 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => 0,
1568 Err(_) => break,
1569 },
1570 _ => break,
1571 };
1572 if n == 0 {
1573 break;
1574 }
1575 all_data.extend_from_slice(&buf[..n]);
1576 if all_data.len() > 65536 {
1577 break;
1578 }
1579 }
1580
1581 if all_data.is_empty() {
1582 return Ok(ModuleResult {
1583 success: true,
1584 finding: Some(format!("Telnet on tcp/{port}: no banner received")),
1585 data: serde_json::json!({"host": self.host, "port": port}),
1586 ..Default::default()
1587 });
1588 }
1589
1590 let text = strip_telnet_negotiation(&all_data);
1591 let clean_text = String::from_utf8_lossy(&text).to_string();
1592
1593 let banner = clean_text
1594 .lines()
1595 .find(|l| !l.trim().is_empty())
1596 .unwrap_or("")
1597 .trim()
1598 .to_string();
1599
1600 let os_hint = if banner.contains("Linux")
1601 || banner.contains("ubuntu")
1602 || banner.contains("debian")
1603 || banner.contains("centos")
1604 {
1605 Some("Linux")
1606 } else if banner.contains("FreeBSD")
1607 || banner.contains("NetBSD")
1608 || banner.contains("OpenBSD")
1609 {
1610 Some("BSD")
1611 } else if banner.contains("SunOS") || banner.contains("Solaris") {
1612 Some("Solaris")
1613 } else if banner.contains("AIX") {
1614 Some("AIX")
1615 } else if banner.contains("HP-UX") {
1616 Some("HP-UX")
1617 } else if banner.to_lowercase().contains("cisco") || banner.to_lowercase().contains("ios") {
1618 Some("Cisco IOS")
1619 } else {
1620 None
1621 };
1622
1623 let mut evidence = vec![format!("telnet/banner:{banner}")];
1624 if let Some(os) = os_hint {
1625 evidence.push(format!("telnet/os:{os}"));
1626 }
1627
1628 Ok(ModuleResult {
1629 success: true,
1630 finding: Some(format!(
1631 "Telnet on tcp/{port}: {banner}{}",
1632 os_hint.map(|o| format!(" [{o}]")).unwrap_or_default()
1633 )),
1634 evidence,
1635 data: serde_json::json!({
1636 "host": self.host, "port": port,
1637 "banner": banner,
1638 "os_hint": os_hint,
1639 "raw_length": all_data.len(),
1640 "negotiation_count": all_data.iter().filter(|&&b| b == IAC).count(),
1641 }),
1642 ..Default::default()
1643 })
1644 }
1645}
1646
1647const REDIS_DEFAULT_PASSWORDS: &[&str] =
1648 &["", "redis", "default", "redislabs", "admin", "password"];
1649
1650#[module(
1651 name = "redis_scanner",
1652 kind = "Scanner",
1653 description = "Redis service scanner - version detection and unprotected instance check",
1654 author = "ICEBOX"
1655)]
1656pub struct RedisScanner {
1657 #[option(required = true, help = "Target IP or hostname")]
1658 pub host: String,
1659 #[option(help = "Redis port (default 6379)")]
1660 pub port: u16,
1661 #[option(help = "Timeout in milliseconds (default 5000)")]
1662 pub timeout_ms: u64,
1663 #[option(help = "Check default passwords when auth is required (default true)")]
1664 pub check_defaults: bool,
1665}
1666
1667async fn redis_read_line(
1668 stream: &tokio::net::TcpStream,
1669 timeout: std::time::Duration,
1670) -> Result<String, ModuleError> {
1671 let mut buf = vec![0u8; 4096];
1672 let n = match tokio::time::timeout(timeout, stream.readable()).await {
1673 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
1674 _ => return Err(ModuleError::Other("read timeout".into())),
1675 };
1676 let text = String::from_utf8_lossy(&buf[..n.min(4092)]);
1677 let line = text.lines().next().unwrap_or("").trim().to_string();
1678 Ok(line)
1679}
1680
1681async fn redis_send(
1682 stream: &tokio::net::TcpStream,
1683 cmd: &[u8],
1684 timeout: std::time::Duration,
1685) -> Result<(), ModuleError> {
1686 match tokio::time::timeout(timeout, stream.writable()).await {
1687 Ok(Ok(_)) => stream
1688 .try_write(cmd)
1689 .map_err(|e| ModuleError::Other(format!("write: {e}")))
1690 .map(|_| ()),
1691 _ => Err(ModuleError::Other("write timeout".into())),
1692 }
1693}
1694
1695#[async_trait]
1696impl Module for RedisScanner {
1697 fn options_json(&self) -> serde_json::Value {
1698 serde_json::to_value(&RedisScannerOptions {
1699 host: self.host.clone(),
1700 port: self.port,
1701 timeout_ms: self.timeout_ms,
1702 check_defaults: self.check_defaults,
1703 })
1704 .unwrap_or(serde_json::Value::Null)
1705 }
1706
1707 fn set_option(&mut self, name: &str, value: &str) -> Result<(), ModuleError> {
1708 let mut o = RedisScannerOptions {
1709 host: self.host.clone(),
1710 port: self.port,
1711 timeout_ms: self.timeout_ms,
1712 check_defaults: self.check_defaults,
1713 };
1714 o.set(name, value)?;
1715 self.host = o.host;
1716 self.port = o.port;
1717 self.timeout_ms = o.timeout_ms;
1718 self.check_defaults = o.check_defaults;
1719 Ok(())
1720 }
1721
1722 fn validate(&self) -> Result<(), ModuleError> {
1723 RedisScannerOptions {
1724 host: self.host.clone(),
1725 port: self.port,
1726 timeout_ms: self.timeout_ms,
1727 check_defaults: self.check_defaults,
1728 }
1729 .validate()
1730 }
1731
1732 async fn run(&self) -> Result<ModuleResult, ModuleError> {
1733 let port = if self.port > 0 { self.port } else { 6379 };
1734 let timeout_ms = if self.timeout_ms > 0 {
1735 self.timeout_ms
1736 } else {
1737 5000
1738 };
1739 let timeout = std::time::Duration::from_millis(timeout_ms);
1740 let addr = crate::core::proxy::resolve_dial(&self.host, port);
1741
1742 let stream =
1743 match tokio::time::timeout(timeout, tokio::net::TcpStream::connect(&addr)).await {
1744 Ok(Ok(s)) => s,
1745 Ok(Err(e)) => return Err(ModuleError::Other(format!("connection failed: {e}"))),
1746 Err(_) => return Err(ModuleError::Other("connection timed out".into())),
1747 };
1748
1749 redis_send(&stream, b"PING\r\n", timeout).await?;
1750 let pong = redis_read_line(&stream, timeout).await.unwrap_or_default();
1751
1752 let mut findings = Vec::new();
1753 let mut evidence = Vec::new();
1754 let mut authed = false;
1755 let mut used_password = String::new();
1756
1757 if pong == "+PONG" {
1758 findings.push("no authentication required".into());
1759 evidence.push("redis/no_auth".into());
1760 authed = true;
1761 } else if pong.starts_with("-NOAUTH") {
1762 findings.push("authentication required".into());
1763 if self.check_defaults {
1764 for pass in REDIS_DEFAULT_PASSWORDS {
1765 let auth_cmd = format!("AUTH {pass}\r\n");
1766 redis_send(&stream, auth_cmd.as_bytes(), timeout).await.ok();
1767 let resp = redis_read_line(&stream, timeout).await.unwrap_or_default();
1768 if resp == "+OK" {
1769 findings.push(format!("AUTH success with password: {:?}", pass));
1770 evidence.push(format!("redis/cred::{:?}", pass));
1771 authed = true;
1772 used_password = pass.to_string();
1773 break;
1774 }
1775 }
1776 if !authed {
1777 findings.push("default passwords failed".into());
1778 }
1779 }
1780 } else {
1781 findings.push(format!("unexpected response: {pong}"));
1782 }
1783
1784 if authed {
1785 redis_send(&stream, b"INFO\r\n", timeout).await.ok();
1786 let mut all_data = Vec::new();
1787 let mut buf = vec![0u8; 16384];
1788 let n = match tokio::time::timeout(timeout, stream.readable()).await {
1789 Ok(Ok(_)) => stream.try_read(&mut buf).unwrap_or(0),
1790 _ => 0,
1791 };
1792 if n > 0 {
1793 all_data.extend_from_slice(&buf[..n]);
1794 let info_text = String::from_utf8_lossy(&all_data);
1795
1796 let mut version = String::new();
1797 let mut os = String::new();
1798 for line in info_text.lines() {
1799 if let Some(v) = line.strip_prefix("redis_version:") {
1800 version = v.trim().to_string();
1801 }
1802 if let Some(o) = line.strip_prefix("os:") {
1803 os = o.trim().to_string();
1804 }
1805 }
1806 if !version.is_empty() {
1807 findings.push(format!("version: {version}"));
1808 evidence.push(format!("redis/version:{version}"));
1809 }
1810 if !os.is_empty() {
1811 evidence.push(format!("redis/os:{os}"));
1812 }
1813 }
1814 }
1815
1816 Ok(ModuleResult {
1817 success: true,
1818 finding: Some(format!("Redis on tcp/{port}: {}", findings.join("; "))),
1819 evidence,
1820 data: serde_json::json!({
1821 "host": self.host, "port": port,
1822 "authenticated": authed,
1823 "used_password": used_password,
1824 "findings": findings,
1825 }),
1826 ..Default::default()
1827 })
1828 }
1829}