diff --git a/Cargo.toml b/Cargo.toml
index 2190fc7..7a1c1c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,14 +17,14 @@ rustyline = "18.0"
# CLI & Async runtime
clap = { version = "4.6", features = ["derive"] }
-tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "net", "time", "io-util", "io-std", "fs", "process", "sync", "signal"] }
+tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
-reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form", "stream"] }
+reqwest = { version = "0.13", default-features = false, features = ["json", "cookies", "socks", "multipart", "form", "stream", "rustls-no-provider", "charset", "http2"] }
h2 = "0.4"
http = "1.4"
bytes = "1.11.1"
-tokio-rustls = "0.26" # used by exploit/scanner modules for target TLS connections
+tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] }
url = "2.5"
quick-xml = "0.39"
data-encoding = "2.10"
@@ -38,7 +38,7 @@ flate2 = "1.1"
base64 = "0.22"
# Networking & Protocols
-socket2 = "0.6"
+socket2 = { version = "0.6", features = ["all"] }
pnet_packet = "0.35"
ipnetwork = "0.21"
regex = "1.12" # newest listed
@@ -47,8 +47,10 @@ which = "8.0"
# FTP
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
native-tls = "0.2"
-rustls = "0.23" # used by exploit/scanner modules for target TLS connections
+rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
rustls-pemfile = "2" # used by exploit/scanner modules
+hyper = { version = "1", features = ["http1", "server"] }
+hyper-util = { version = "0.1", features = ["tokio", "service"] }
# Telnet
crossbeam-channel = "0.5"
@@ -64,9 +66,7 @@ rlimit = "0.11"
# Bluetooth
btleplug = "0.12"
-# TUI (WPair module)
-ratatui = "0.30"
-crossterm = "0.29"
+# WPair migrated from ratatui+crossterm TUI to rustyline REPL — deps removed.
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
@@ -76,6 +76,7 @@ tokio-tungstenite = "0.29"
# Futures
futures = "0.3"
+futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
@@ -83,7 +84,7 @@ serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
-axum = "0.8"
+axum = { version = "0.8", features = ["ws"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.23", features = ["v4", "serde"] }
@@ -95,18 +96,22 @@ hickory-proto = "0.25"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tracing-appender = "0.2"
# Misc utilities
+once_cell = "1.21"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.35"
des = { version = "0.8.1", features = ["zeroize"] }
-sha1 = "0.11"
+zeroize = { version = "1", features = ["derive"] }
+sha1 = "0.10"
strsim = "0.11"
ssh2 = "0.9.5"
num_cpus = "1.17.0"
+# Constant-time comparison for security (timing attack prevention)
+subtle = "2.6"
+aes-gcm = "0.10.3"
# Post-Quantum Encryption (PQXDH: X25519 + ML-KEM-768 hybrid, ChaCha20-Poly1305 AEAD)
ml-kem = "0.2.3"
@@ -114,8 +119,8 @@ kem = "=0.3.0-pre.0"
rand_core = { version = "0.6", features = ["getrandom"] }
x25519-dalek = { version = "2.0", features = ["static_secrets"] }
chacha20poly1305 = "0.10"
-hkdf = "0.13"
-sha2 = "0.11"
+hkdf = "0.12"
+sha2 = "0.10"
hex = "0.4"
[build-dependencies]
@@ -125,8 +130,7 @@ walkdir = "2.5"
# Dependency overrides to address security advisories in transitive dependencies
# RUSTSEC-2026-0009: time >=0.3.47 fixes DoS via stack exhaustion (used by reqwest via cookie/cookie_store)
time = "0.3.47"
-# Note: paste (RUSTSEC-2024-0436, unmaintained) and lru (RUSTSEC-2026-0002, unsound) are
-# transitive warnings from ratatui 0.29 internals — no upstream fix available yet.
+# (ratatui 0.29 transitive advisories cleared when the TUI was replaced with rustyline.)
# ============================================
# Development profile: Fast incremental builds
diff --git a/README.md b/README.md
index f4b71a3..4fe83c0 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,9 @@
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
-
-
+
+
+
---
@@ -16,7 +17,7 @@ Full documentation lives in the **[Rustsploit Wiki](docs/Home.md)**. Below is a
| [Getting Started](docs/Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](docs/Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](docs/CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
-| [API Server](docs/API-Server.md) | REST API startup, endpoints, auth, rate limiting, hardening |
+| [API Server](docs/API-Server.md) | REST + WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](docs/API-Usage-Examples.md) | Practical curl workflows, request/response samples |
| [Module Catalog](docs/Module-Catalog.md) | All modules by category — exploits, scanners, creds |
| [Module Development](docs/Module-Development.md) | How to author new modules, lifecycle, dispatcher |
@@ -44,36 +45,39 @@ Full documentation lives in the **[Rustsploit Wiki](docs/Home.md)**. Below is a
- **Background jobs:** Run modules asynchronously with `run -j`, manage with `jobs` commands
- **Export/reporting:** Export all engagement data to JSON, CSV, or human-readable summary reports
- **Console logging:** `spool` command captures all output to file for documentation
-- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, MQTT, Fortinet — with IPv6 and TLS support
-- **Exploit coverage:** CVEs for GNU inetutils-telnetd, Apache Tomcat, TP-Link, Ivanti, Zabbix, OpenSSH, Jenkins, PAN-OS, Heartbleed, and more
-- **Scanners & utilities:** Port scanner, ping sweep, SSDP, HTTP title grabber, DNS recursion tester, directory bruteforcer, sequential fuzzer
-- **REST API server:** 30+ endpoints — authentication, rate limiting, IP tracking, full CRUD for credentials, hosts, services, loot, jobs
+- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, IMAP, RDP, RTSP, SNMP, L2TP, MQTT, VNC, MySQL, PostgreSQL, Redis, CouchDB, Elasticsearch, Memcached, HTTP Basic, Proxy, Fortinet — with IPv6 and TLS support
+- **Exploit coverage:** CVEs for VNC (LibVNC, TigerVNC, TightVNC, x11vnc), honeypots (Cowrie, Dionaea, HoneyTrap, SNARE), WAFs (SafeLine), Apache Camel, Kubernetes ingress-nginx, Commvault, MISP, Zimbra, Next.js, Vite, and 100+ more
+- **Scanners & utilities:** Port scanner, ping sweep, SSDP, HTTP title grabber, DNS recursion tester, directory bruteforcer, sequential fuzzer, proxy scanner, reflect scanner, vulnerability checker
+- **API server:** PQ-encrypted WebSocket transport — post-quantum cryptography, full CRUD for credentials, hosts, services, loot, jobs
+- **MCP server:** 38-tool Model Context Protocol server for AI-assisted pentesting via stdio
- **Plugin system:** Third-party modules via `src/modules/plugins/` with build-time discovery and startup safety warnings
-- **Security hardened:** Input validation, path traversal protection, honeypot detection, memory-safe operations
+- **Security hardened:** Input validation, path traversal protection, honeypot detection, root privilege checks, spool symlink protection, memory-safe operations
- **IPv4/IPv6 ready:** Both address families work out-of-the-box across all modules
---
## Quick Start
+**One command** (Debian/Ubuntu/Kali):
+
```bash
-# Install dependencies (Debian/Ubuntu/Kali)
-sudo apt update
-sudo apt install pkg-config libssl-dev rustc libdbus-1-dev freerdp2-x11
-
-# Install Rust
-curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-source $HOME/.cargo/env
-
-# Clone & build
-git clone https://github.com/s-b-repo/rustsploit.git
-cd rustsploit
-cargo build
-
-# Run
-cargo run
+sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake && (command -v cargo > /dev/null 2>&1 || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . "$HOME/.cargo/env")) && git clone https://github.com/s-b-repo/rustsploit.git && cd rustsploit && cargo run
```
+
+What each dependency does
+
+| Package | Required by | Why |
+|---------|------------|-----|
+| `build-essential` | Native crate compilation | gcc, make, libc headers |
+| `pkg-config` | `native-tls`, `ssh2` | Finds system libraries at build time |
+| `libssl-dev` | `native-tls`, `ssh2` | OpenSSL headers for TLS and SSH |
+| `libdbus-1-dev` | `btleplug` | D-Bus IPC for Bluetooth scanning |
+| `cmake` | `ssh2` (libssh2-sys) | Builds libssh2 from source |
+
+
+
+
For other distros (Arch, Gentoo, Fedora), Docker deployment, and one-liner installs, see **[Getting Started](docs/Getting-Started.md)**.
---
diff --git a/build.rs b/build.rs
index bd4eeff..72e88eb 100644
--- a/build.rs
+++ b/build.rs
@@ -43,10 +43,13 @@ fn main() {
println!("cargo:rerun-if-changed=src/modules/{}", cat);
}
- // Compile regexes once, reuse across all categories
- let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)").unwrap();
- let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo").unwrap();
- let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult").unwrap();
+ // Compile regexes once, reuse across all categories.
+ let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
+ .expect("hardcoded regex must compile");
+ let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo")
+ .expect("hardcoded regex must compile");
+ let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult")
+ .expect("hardcoded regex must compile");
// Generate a dispatcher for each category
let mut registry_entries: Vec = Vec::new();
@@ -399,9 +402,16 @@ fn generate_registry(entries: &[RegistryEntry]) -> Result<(), Box Result, Box> {
+fn find_modules(
+ root: &Path,
+ run_re: &Regex,
+ info_re: &Regex,
+ check_re: &Regex,
+) -> Result, Box> {
let mut mappings = HashSet::new();
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(|e| e.ok()) {
@@ -415,7 +425,6 @@ fn find_modules(root: &Path, run_re: &Regex, info_re: &Regex, check_re: &Regex)
let mut content = String::new();
if File::open(path).and_then(|mut f| f.read_to_string(&mut content)).is_ok() {
- // Fast pre-filter: skip files that can't possibly match
if !content.contains("fn run") { continue; }
if run_re.is_match(&content) {
let caps = ModuleCapabilities {
diff --git a/changelogs/.changelog-latest.md.kate-swp b/changelogs/.changelog-latest.md.kate-swp
new file mode 100644
index 0000000..26a81fa
Binary files /dev/null and b/changelogs/.changelog-latest.md.kate-swp differ
diff --git a/changelogs/changelog-latest.md b/changelogs/changelog-latest.md
index 3133739..ec77172 100644
--- a/changelogs/changelog-latest.md
+++ b/changelogs/changelog-latest.md
@@ -1,45 +1,36 @@
-## v0.5.0 — Security & Isolation Hardening
+## April 2026 — Module Expansion & Infrastructure Hardening
-### Security Fixes (ArcticAlopex Frontend)
-- **API key hashing**: Replaced SHA256 with Argon2id via existing `hashApiKey()` function
-- **Permission checks**: Added ACL checks to 6 admin endpoints (GET/POST /api/users, GET/POST /api/roles, GET /api/audit, GET /api/activity) that previously only checked session existence
-- **Module restrictions**: Loaded `role_module_restrictions` from DB and passed to ACL `resolve()` engine (was always passing empty array)
-- **Export permission mismatch**: Fixed `export.data` → `export.json` permission name inconsistency
-- **PQ-aware health check**: Health worker now tests both plain HTTP and PQ-encrypted paths, reports healthy/degraded/unreachable
-- **Default-deny unmapped endpoints**: pathToPermission() now returns a sentinel for unmapped RSF endpoints instead of null (owner-only fallback)
-- **WebSocket error handling**: Separated JSON parse errors from session failures in WS message handler
-- **Credential redaction**: Recursive redaction across 15+ secret field names with nested object support
-- **TOTP encryption**: Secrets now encrypted with AES-256-GCM via MASTER_KEY before DB storage
-- **Per-account login lockout**: Added per-account rate limiting (10 attempts → 30 min lock) alongside existing IP-based limiter
+**Module totals: 182 exploits, 27 scanners, 29 creds, 1 plugin = 239 total**
-### Per-Workspace Isolation
-- **Credentials**: Each workspace now has its own credential store at `~/.rustsploit/workspaces/{name}_creds.json` (previously global `creds.json`)
-- **Options**: Each workspace now has its own options at `~/.rustsploit/workspaces/{name}_options.json` (previously global `global_options.json`)
-- **Auto-migration**: Legacy global files are automatically migrated to the default workspace on first run
-- **Workspace switch**: `workspace ` now switches credentials and options alongside hosts/services
+### 45 New Exploit Modules
-### Persistent File Logging
-- Added daily rolling log files at `~/.rustsploit/logs/rustsploit.YYYY-MM-DD.log`
-- Both console (stderr) and file logging run simultaneously via `tracing-appender`
-- Log files include timestamps, log levels, and target module information
+6 new categories: Cowrie (3), Dionaea (4), HoneyTrap (2), SafeLine (6), Snare (2), VNC (16). Plus 12 modules in existing categories: SSH (4), Frameworks (2), Network Infra (2), WebApps (4).
-### Per-User Isolation (ArcticAlopex GUI)
-- **Credential ownership**: Each user now has their own credential store. Non-admin users only see credentials they added. Owners/admins can toggle "Show All" to view all tenant credentials.
-- **User preferences**: New per-user key-value preferences API (`/api/preferences`) with theme, default target, concurrency, auto-refresh, and other settings. Managed via Account Settings page.
-- **Credential ownership enforcement**: Non-admin users can only delete their own credentials (403 for others' creds).
-- **New DB tables**: `credential_owners` (maps RSF cred IDs to users), `user_preferences` (per-user key-value settings).
+### 3 New Scanner Modules
-### Audit & Activity Log Export
-- Audit and Activity pages now have CSV and JSON export buttons
-- Export API: `GET /api/audit?export=csv|json` and `GET /api/activity?export=csv|json`
-- Exports up to 10,000 entries with `Content-Disposition: attachment` for file download
-- Respects current search/filter when exporting
+`proxy_scanner`, `reflect_scanner`, `vuln_checker`
-### Dashboard & UI Fixes
-- Fixed Instance Health showing "unreachable" despite backend being up (was checking `data.healthy` instead of `data.success`)
-- Dashboard now checks health endpoint directly + uses stored DB status
-- Fixed activity feed auto-refresh glitching (unstable callback deps causing interval reset every 5 seconds)
-- Fixed audit page loading flash on refresh (skeleton only shows on initial load)
+### 10 New Credential Modules
+
+`couchdb_bruteforce`, `elasticsearch_bruteforce`, `http_basic_bruteforce`, `imap_bruteforce`, `memcached_bruteforce`, `mysql_bruteforce`, `postgres_bruteforce`, `proxy_bruteforce`, `redis_bruteforce`, `vnc_bruteforce`
+
+### Infrastructure
+
+- WebSocket transport (`/pq/ws`) with PQ encryption, 100-connection cap, heartbeat
+- `require_root()` privilege helper for raw-socket modules
+- `build_http_client()` / `build_http_client_with()` standardized across 50+ modules
+- `tcp_connect_addr()`, `tcp_connect_str()` TCP helpers replacing raw TcpStream::connect
+- MCP server hardening: stdout isolation, 1 MiB line cap, binary-safe reads
+- Spool hardening: O_NOFOLLOW, parent symlink check, lock-first pattern
+- build.rs: `check_available()` dispatch, optimized regex compilation
+- `zeroize` crate for sensitive data cleanup
+
+### Module Audit
+
+- Replaced blocking `std::thread::sleep` with async alternatives
+- Migrated raw `TcpStream::connect` to framework `tcp_connect_addr()`
+- Standardized `reqwest::Client::builder` to `build_http_client()`
+- Added `require_root()` to all raw-socket modules
---
@@ -16907,1364 +16898,1790 @@ Built a complete separate web application (75 files, ~10K lines) in `arcticalope
● The GUI backend's key is registered. Now you need to restart the API server first (so it picks up the new key), then the backend, then the frontend. Run these in 3 terminals:
- Terminal 1 — API server (restart to load the new authorized key):
- cd ~/Pictures/Jenkins/rustsploit-thekiaboys && ./target/debug/rustsploit --api
-
- Terminal 2 — GUI Backend:
- cd ~/Pictures/Jenkins/rustsploit-thekiaboys/rustsploit\ gui/backend && npm run dev
-
- Terminal 3 — Frontend:
- cd ~/Pictures/Jenkins/rustsploit-thekiaboys/rustsploit\ gui/frontend && npm run dev
-
- The API server should now show Authorized clients: 1 with the gui-backend key, and the GUI backend should show a successful PQ connection instead of the API key error. No more plaintext — everything goes through the PQ-encrypted
- channel.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- the necromoonix fixes
-
- api.rs (3 fixes):
- - Rate limiter panic — lock().unwrap() replaced with poison-recovery via into_inner(). Added entry pruning when map exceeds 10K entries to prevent memory exhaustion.
- - Shell metacharacter check incomplete — Added (, ), {, } to the blocked characters to prevent subshell/brace injection.
-
- mcp/tools.rs (2 fixes):
- - u16/u32 silent truncation — n as u16 replaced with u16::try_from(n).ok() so values >65535 return None instead of silently wrapping (e.g., port 70000 was becoming 4464).
- - SSRF target filter bypass — prompts.remove("target") was case-sensitive. Now removes all case variations ("Target", "TARGET", etc.) to prevent prompt injection bypass.
-
- config.rs (1 fix):
- - Multi-target file path traversal — File paths in comma-separated target lists were stored raw without canonicalization. Now canonicalized (resolves .., symlinks) consistent with single-target handling.
-
-
-
-
- Bug Audit: 10 Framework Feature Modules
+Audit & Improve All 181 Exploit Modules
Context
- Comprehensive security and correctness audit of the 10 Metasploit-parity framework features added to Rustsploit. Each module was read in full, cross-referenced with shell.rs and api.rs integration points. Findings are organized by
- file, prioritized by severity.
+ The repo has 181 exploit modules under src/modules/exploits/. A verified grep-based audit surfaces several
+ systemic issues that break MCP/API/CLI compatibility, waste runtime, and leave the module set inconsistent:
+
+ - 14 blocking std::thread::sleep calls in async code (5 files) — freeze the tokio runtime.
+ - 23 files hand-roll TcpStream::connect instead of the framework's battle-tested
+ crate::utils::network::tcp_connect_addr (src/utils/network.rs:117).
+ - 50 files hand-roll reqwest::Client::builder instead of crate::utils::network::build_http_client
+ (src/utils/network.rs:257) — duplicated TLS/cookie/timeout config, inconsistent source-port handling.
+ - ~114 / 181 modules lack a standalone pub async fn check() — the framework expects a cheap vulnerability probe
+ separate from run() (used by check_module MCP tool; currently returns "does not support check" for most).
+ - ~73 modules hardcode port/path consts rather than using cfg_prompt_* — works for CLI-interactive only; breaks
+ silent MCP/API callers who want overrides.
+ - ~39 CVE-named modules have references: vec![] — links missing even though CVE ID is in the filename.
+ - Batch harness (/tmp/fire_all_modules.py → /tmp/fire_results.tsv, running in background) shows ~90% of modules
+ hit the 5 s per-module timeout when fired at a generic nc listener — they don't bail early when the target
+ clearly isn't speaking their protocol.
+
+ Fixing these raises MCP-run success rate, cuts timeout waste, eliminates duplicated networking logic, and makes
+ each exploit uniformly discoverable/probeable. The work splits cleanly into mechanical bulk fixes (quick wins)
+ and per-module feature work (bounded scope).
+
+ Critical Files & Utilities to Reuse
+
+ - src/utils/network.rs:117 — tcp_connect_addr(addr, timeout) (async, honours global source-port, handles
+ EINPROGRESS correctly)
+ - src/utils/network.rs:257 — build_http_client(timeout) (reqwest client with standard TLS + source-port
+ warning)
+ - src/utils/mod.rs — normalize_target, cfg_prompt_default, cfg_prompt_port, cfg_prompt_existing_file,
+ cfg_prompt_int_range, cfg_prompt_yes_no, cfg_prompt_output_file
+ - src/modules/creds/utils.rs — is_mass_scan_target, is_subnet_target, run_mass_scan, MassScanConfig,
+ load_credential_file, generate_combos_mode
+ - src/module_info.rs — ModuleInfo, ModuleRank, CheckResult
+
+ Phased Execution
+
+ Phase A — Mechanical bulk (~1 session)
+
+ A1. Replace std::thread::sleep with tokio::time::sleep(...).await in 5 files / 14 sites:
+ ssh/sshpwn_auth_passwd.rs, ssh/sshpwn_pam.rs, cowrie/llm_prompt_injection.rs, cowrie/ansi_log_injection.rs,
+ dos/null_syn_exhaustion.rs. Where the sleep sits inside an ssh2 blocking session (ssh2 crate is sync), keep the
+ sync sleep but wrap the surrounding block in tokio::task::spawn_blocking.
+
+ A2. Fill empty references: vec![] for the ~39 CVE-named modules. Default pair:
+ https://nvd.nist.gov/vuln/detail/CVE-{year}-{id} +
+ https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-{year}-{id}. Extract CVE from filename.
+
+ A3. Root-check preamble for raw-socket modules. Add a shared helper crate::utils::require_root("icmp raw
+ socket") at the top of every run() in the flood/amplification family (dos/icmp_flood, dos/syn_ack_flood,
+ dos/null_syn_exhaustion, dos/dns_amplification, dos/ntp_amplification, dos/ssdp_amplification,
+ dos/memcached_amplification, scanners/ping_sweep, scanners/stalkroute_full_traceroute). Returns a clean
+ anyhow::Error if geteuid() != 0 — no panic, no socket-bind crash. Helper lives in src/utils/privilege.rs (new
+ file, ~12 LOC using libc::geteuid()).
+
+ Gate: cargo build --release clean, cargo clippy -- -W clippy::all no new warnings.
+
+ Phase B — Utility-reuse migrations (~2 sessions)
+
+ B1. Migrate 23 raw TcpStream::connect sites → tcp_connect_addr. Transformation:
+
+ // before
+ let stream = tokio::time::timeout(
+ Duration::from_secs(t),
+ TcpStream::connect(&addr),
+ ).await??;
+ // after
+ use crate::utils::network::tcp_connect_addr;
+ let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
+ let stream = tcp_connect_addr(addr, Duration::from_secs(t)).await?;
+
+ File list (from grep): dionaea/{mysql_sqli, mssql_dos, mqtt_underflow}, honeytrap/{ftp_panic, docker_panic},
+ snare/cookie_dos, ssh/{openssh_regresshion_cve_2024_6387, opensshserver_9_8p1race_condition,
+ erlang_otp_ssh_rce_cve_2025_32433}, dos/{slowloris, rudy}, crypto/geth_dos_cve_2026_22862,
+ frameworks/http2/cve_2023_44487, frameworks/exim/exim_etrn_sqli_cve_2025_26794,
+ network_infra/trend_micro/cve_2025_692{58,59,60}, network_infra/fortinet/fortisiem_rce_cve_2025_64155,
+ webapps/dify/cve_2025_56157, tplink/{tapo_c200_vulns, tplink_vigi_c385_rce, tplink_wr740n_dos},
+ netgear/netgear_r6700v3_rce_cve_2022_27646.
+
+ B2. Extend build_http_client + migrate all 50 sites.
+ First, extend the helper (src/utils/network.rs:257) with an opts struct so every HTTP-using module can migrate
+ cleanly:
+
+ #[derive(Default)]
+ pub struct HttpClientOpts {
+ pub cookie_store: bool,
+ pub follow_redirects: bool, // default false → manual policy
+ pub user_agent: Option,
+ pub default_headers: Option,
+ }
+ pub fn build_http_client_with(timeout: Duration, opts: HttpClientOpts) -> Result { ... }
+ // existing build_http_client(timeout) stays as a thin wrapper over build_http_client_with(timeout,
+ Default::default()).
+
+ Then migrate all 50 modules — no diverging callers left in exploit code.
+
+ Gate: cargo build --release; re-run fire_all_modules.py, store diff vs Phase-A baseline. Timeout bucket should
+ shrink.
+
+ Phase C — Empirical bug triage (~1 session, after batch finishes)
+
+ Once /tmp/fire_results.tsv is complete, bucket by status:
+ 1. PANIC / unwrap()-in-net — fix all; replace with ? + anyhow::Context.
+ 2. IMMEDIATE_ERROR (<500 ms) — usually a hardcoded const or missing prompt default; fix inline.
+ 3. TIMEOUT_NO_READ — add protocol sniff + early-exit after first read:
+
+ let mut probe = [0u8; 8];
+ match tokio::time::timeout(Duration::from_millis(1500), stream.read(&mut probe)).await {
+ Ok(Ok(0)) | Ok(Err(_)) | Err(_) =>
+ return Ok(CheckResult::NotVulnerable("no banner".into())),
+ Ok(Ok(n)) if !looks_like_protocol(&probe[..n]) =>
+ return Ok(CheckResult::NotVulnerable("wrong protocol".into())),
+ _ => {}
+ }
+
+ Gate: PANIC bucket = 0, TIMEOUT bucket ≥50 % reduction.
+
+ Phase D — Feature additions (~2 sessions)
+
+ D1. Add pub async fn check() to the 114 missing modules. Priority = ~70 CVE-named ones. Template: reuse the
+ first TCP/HTTP probe from run(), return CheckResult::{Vulnerable|NotVulnerable|Unknown} without the payload.
+ For DoS floods where "check == exploit," omit with an inline comment.
+
+ D2. Replace hardcoded consts with cfg_prompt_* in ~73 modules. Keep current values as prompt defaults ⇒ zero
+ behavior change for existing callers; opens API/MCP overridability.
+
+ Gate: mcp__rustsploit__check_module returns a real result for every newly-checked module; list_options reflects
+ new knobs.
+
+ Phase E — Deep per-module audit of ALL 181 modules (rolling, multi-session)
+
+ User scope decision: full coverage. No deferral. Split into rolling session batches of ~20 modules each (≈9-10
+ sessions). Checklist tracked in src/modules/exploits/AUDIT.md (created once, updated per session) so progress
+ survives session boundaries.
+
+ Per-module checklist (applied uniformly):
+ 1. Payload correctness vs. upstream CVE PoC / advisory (link in doc-comment).
+ 2. Auth fallbacks where applicable (retry on 401/403 with alt credentials).
+ 3. Exponential backoff around retryable transient errors.
+ 4. Loot capture via crate::db::add_loot (shell output, extracted creds, files).
+ 5. Host/service registration via crate::db::add_host + add_service on successful fingerprint.
+ 6. Error context via anyhow::Context — every ? with human-readable .context(...).
+ 7. Doc block at top: CVE, vendor, affected versions, payload summary, references.
+ 8. ModuleRank honest w.r.t. reliability observed during testing.
+ 9. check() present (Phase D1 adds one if missing).
+ 10. cfg_prompt_* for every user-configurable knob (Phase D2 migrates consts).
+
+ Session batching order (priority first):
+ - E1 (session 1): highest-profile CVEs in network_infra/ (fortinet, ivanti, citrix, palo_alto, sonicwall) — 30
+ modules.
+ - E2: webapps RCE modules (craftcms, flowise, n8n, xwiki, roundcube, sharepoint, wordpress, sap, misp, mcpjam,
+ dify, langflow) — 25 modules.
+ - E3: frameworks (apache_tomcat, apache_camel, jenkins, nginx, php, wsus, http2) — 15 modules.
+ - E4: ssh module family (libssh_auth_bypass, asyncssh, paramiko, erlang_otp, sshpwn_*, libssh2_rogue_server,
+ openssh_regresshion, opensshserver_9_8p1race) — 15 modules.
+ - E5: router CVEs (tplink, ruijie, netgear, dlink, zte, zyxel, tenda, ubiquiti) — 25 modules.
+ - E6: vnc modules (13) + telnet (1) + voip (1) + cameras (6) — 21 modules.
+ - E7: dos modules (12 flood/amplification) — apply root-check + verify they behave cleanly.
+ - E8: honeytrap / snare / cowrie / dionaea / safeline — 15 modules.
+ - E9: crypto / ftp / ipmi / windows / bluetooth / payloadgens — 12 modules.
+ - E10: remaining + catch-all review.
+
+ Completion gate for each session: audit notes + diff committed, cargo build --release green, MCP smoke re-run.
+
+ Verification (cross-phase)
+
+ 1. cargo build --release must stay green after every phase.
+ 2. cargo clippy --all-targets -- -W clippy::all — no new warnings in touched files.
+ 3. python3 /tmp/fire_all_modules.py smoke run between phases; /tmp/fire_results.tsv compared against the
+ pre-phase snapshot for regressions.
+ 4. MCP smoke: mcp__rustsploit__list_modules, module_info on every touched module, check_module on all Phase-D1
+ additions, run_module on 3 representative modules (ssh, HTTP RCE, DoS).
+ 5. CLI smoke: run the shell (cargo run -- -s), use , check, verify prompts are satisfied via set
+ and no io::stdin blocks.
+
+ Decisions Locked
+
+ - Phase E scope → all 181 modules, batched across rolling sessions (E1–E10, ~20 modules each), checklist
+ tracked in src/modules/exploits/AUDIT.md.
+ - build_http_client extension → new HttpClientOpts struct + build_http_client_with(timeout, opts); existing
+ build_http_client(timeout) stays as wrapper. All 50 exploit modules migrate; no diverging callers.
+ - Raw-socket modules → shared crate::utils::require_root(&str) helper (new src/utils/privilege.rs); every
+ flood/amplification/raw-sniff module gates at top of run() returning anyhow::Error, no crash.
+
+
+ Completed
+ - ✅ Phase A1: thread::sleep → tokio::time::sleep (2 files — other sites were already correct inside
+ spawn_blocking/native threads)
+ - ✅ Phase A2: Populated all 39 empty references: vec![] with category-appropriate refs
+ - ✅ Phase A3: New require_root() helper in src/utils/privilege.rs wired into 9 raw-socket modules
+ - ✅ Phase B2a: HttpClientOpts + build_http_client_with() added to src/utils/network.rs; old build_http_client()
+ now wraps it
+
+
+
+ ┌─────────────────────────────────────┬───────────┬──────────────────────────────────────────────────────────┐
+ │ Phase │ Status │ Impact │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A1 — thread::sleep → tokio::sleep │ ✅ done │ 2 SSH modules (14 sites) │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A2 — Populate empty references │ ✅ done │ All 39 modules │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ A3 — require_root helper │ ✅ done │ New src/utils/privilege.rs + 9 raw-socket modules │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B2a — Extend build_http_client │ ✅ done │ HttpClientOpts + build_http_client_with │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B1 — TcpStream::connect migration │ ✅ │ ~14 sites / 10 files migrated; complex patterns deferred │
+ │ │ partial │ to E │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ B2b — reqwest migration │ ✅ done │ 47 sites / 41 files unified │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ C — Empirical batch triage │ ⏳ │ Needs fresh /tmp/fire_results.tsv │
+ │ │ pending │ │
+][
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ D2 — cfg_prompt_* for hardcoded │ ⏳ │ 73 modules │
+ │ consts │ pending │ │
+ ├─────────────────────────────────────┼───────────┼──────────────────────────────────────────────────────────┤
+ │ E — Deep audit all 181 modules │ ⏳ │ rolling E1–E10 │
+ │ │ pending │ │
+ └─────────────────────────────────────┴───────────┴──────────────────────────────────────────────────────────┘
+
+ MCP Server Protocol Safety (src/mcp/server.rs):
+ - Added isolate_protocol_stdout() — redirects fd 1 to /dev/null so stray println! from modules can't corrupt the
+ JSON-RPC stream
+ - Added MAX_LINE_BYTES (1 MiB) cap to prevent memory exhaustion from malicious input
+ - Switched to byte-level read_until for binary safety (vs read_line which requires valid UTF-8)
+ - Explicit non-UTF-8 error handling with proper JSON-RPC error response
+ - Made write_response accept dyn AsyncWrite for flexibility
+ - Fixed MCP spec compliance: resources/read now wraps response in { "contents": [...] }
+
+ Spool Security (src/spool.rs):
+ - Added O_NOFOLLOW flag to prevent TOCTOU race condition on symlinks
+ - Lock-first pattern: acquires the write lock before creating the file to prevent orphaned files
+ - Parent directory symlink check (Bug #96): blocks spool through symlinked parent dirs
+ - write_line() now returns Result<(), std::io::Error> with flush after every write (Bug #11)
+ - Proper error logging in start_at_path() and stop()
+
+ Output Error Handling (src/output.rs):
+ - Added SPOOL_WARNED pattern for spool write error suppression (logs once, then suppresses)
+ - _mprint_line and _mprint_newline now properly handle write_line Result
+
+ Shell (src/shell.rs):
+ - Proper error handling for spool write_line call
+
+ Build System (build.rs):
+ - Regex caching: compiles 3 regexes once in main() and reuses across all category dispatchers
+ - Fast pre-filter: content.contains("fn run") skip before regex matching
+ - Added check_available() function generation per category
+ - Added check_available_by_category() in unified registry
+
+ API SSRF Protection (src/api.rs):
+ - Restored comprehensive SSRF blocking: cloud metadata endpoints, link-local ranges, IPv6 metadata, wildcard DNS
+ services
+ - Added is_blocked_ip() function for IP-level blocking
+ - Made is_blocked_target() pub(crate) for MCP tools access
+ - Added () and {} to shell metacharacter checks
+
+ MCP Tools Input Validation (src/mcp/tools.rs):
+ - Added SSRF checks to handle_run_module and handle_check_module
+ - Added field length limits (4096 chars) and control character validation to handle_add_cred
+ - Added IP/hostname validation (256 chars) to handle_add_host
+ - Added port range validation (port 0 blocked) and field validation to handle_add_service
+
+
+
+ Warnings fixed (10 → 0):
+ - Removed unused extract_ip_from_target re-export from utils/mod.rs
+ - Wired categories() and has_check() into commands/mod.rs, called from ws.rs rpc_list_modules_enriched — this uses the generated CATEGORIES, check_available_by_category, and all 4 per-dispatch check_available functions
+ - Removed 9 unused ModuleConfig fields (port, username_wordlist, password_wordlist, concurrency, stop_on_success, save_results, output_file, verbose, combo_mode) — only custom_prompts and api_mode were ever read
+ - Wired results_dir() from config.rs into ws.rs instead of hardcoded "results" path
+
+ Slop audit result: Only 4 .expect() calls in the entire codebase, all on infallible operations (hardcoded regex, fixed-size byte slice, in-memory zlib). No .unwrap() calls on fallible paths.
+
+
+ Completed fixes:
+ - P0 (SSRF): Fixes #1-3 — async DNS resolution checks in ws.rs and mcp/tools.rs
+ - P1 (Race/Validation): Fix #4 (port truncation), #5 (WS connection limit TOCTOU), #6 (job spawn TOCTOU), #7
+ (session expiry/cleanup), #8 (file permission TOCTOU)
+ - P2 (Hardening): Fix #9 (health info disclosure), #10 (secret zeroization — Zeroizing> on all chain keys,
+ root keys, IKM), #11 (subscription limit), #12 (serialization error propagation), #13 (per-IP handshake rate
+ limiting — 10/min/IP with 429 response), #14 (job ID collision check)
+ - P3 (Low): Fix #15 (cred_type validation), #16 (removed is_locked() stub)
+
+First task to need a prompt (e.g., "Port Range") locks the shared cache, prompts the user once, caches the answer
+All other tasks find the cached answer instantly — zero stdin reads
+Single-target mode is completely unaffected (no cache active)
+Files changed
+File Change
+
+context.rs
+Added PromptCache type + prompt_cache field on RunContext
+
+prompt.rs
+Added cached_prompt() helper, updated all 7 cfg_prompt_* functions
+
+commands/mod.rs
+Wrapped all 3 concurrent dispatch paths (mass scan, file, CIDR) with shared cache via RUN_CONTEXT.scope()
+You can test it with cargo run and try a mass scan target like 0.0.0.0/0 — each prompt should now appear exactly once.
+ Fixed (7 changes across 8 files):
+
+ 1. TOCTOU file permissions (cred_store.rs, loot.rs, global_options.rs, pq_channel.rs): Files now created with
+ 0o600 atomically via OpenOptions::mode() instead of write-then-chmod.
+ 2. Path traversal (creds/utils.rs): mark_ip_checked and is_ip_checked now reject absolute paths, paths with
+ separators, and null bytes - not just ...
+ 3. SSH timeout hardening (ssh_bruteforce.rs): Added set_read_timeout, set_write_timeout, and sess.set_timeout() so
+ handshake/auth can't hang a blocking thread forever.
+ 4. Combo overflow protection (creds/utils.rs): Vec::with_capacity now caps pre-allocation at 10M entries to avoid
+ OOM on huge cross products; Vec still grows organically beyond that.
+ 5. Atomic ordering (creds/utils.rs): print_progress and print_final now use Ordering::Acquire instead of Relaxed
+ for consistent stats.
+ 6. Streaming wordlist support (utils/modules.rs, creds/utils.rs): Added load_lines_uncapped, load_lines_batched,
+ file_size, STREAMING_THRESHOLD (250 MB), and run_bruteforce_streaming - processes huge wordlists in 500K-line
+ batches without materializing the full cross product.
+
+
+ 11 files updated:
+
+ 1. docs/Module-Catalog.md — Added 57 new module entries (45 exploits, 3 scanners, 10 creds), 6 new category
+ sections (Cowrie, Dionaea, HoneyTrap, SafeLine, Snare, VNC), updated totals to 239
+ 2. docs/Changelog.md — New April 2026 section with all changes, updated module totals
+ 3. README.md — Updated counts, highlights (credential protocols, exploit categories, MCP server, WebSocket),
+ scanner list
+ 4. docs/Home.md — Updated module counts in wiki index (239 total)
+ 5. docs/Module-Development.md — Added mcp/, ws.rs, privilege.rs to layout tree, updated module counts
+ 6. docs/Utilities-Helpers.md — Added network utils and privilege check sections with full API docs
+ 7. docs/API-Server.md — Added WebSocket endpoint /pq/ws, transport documentation, updated endpoint count
+ 8. docs/CLI-Reference.md — Added --mcp flag
+ 9. docs/Security-Validation.md — Added MCP, spool, privilege check security patterns
+ 10. bugs.txt — Cleaned up debug logs, replaced with resolved bug list
+ 11. changelogs/changelog-latest.md — Prepended full session changelog
+
+ Mass scan prompt spam fix (context.rs, utils/mod.rs, utils/prompt.rs, commands/mod.rs):
+ - Added a global BATCH_ACTIVE flag and BATCH_CACHE that don't rely on tokio::task_local propagation into spawned
+ tasks
+ - is_batch_mode() now checks the global flag first, falling back to the task-local
+ - cached_prompt() now uses the global batch cache when active, ensuring only one prompt per key even across
+ tokio::spawn boundaries
+ - All three concurrent dispatch paths (random, file, CIDR) set set_batch_mode(true) before spawning and
+ set_batch_mode(false) after completion
+
+ hex.rs (src/native/hex.rs):
+ - Added decode() function — accepts upper/lowercase hex, returns Vec
+ - Added DecodeError enum with OddLength and InvalidChar variants, implementing Display and Error
+ - No test code — production functions only
+ v
+
+ New module: src/modules/exploits/dos/telnet_iac_flood.rs
+
+ Four attack modes that exploit the unbounded IAC parsing bug:
+
+ 1. WILL/DO Storm — Rapid-fire WILL/DO/WONT/DONT for 14 options, forcing the target to generate and process response packets for each
+ 2. Subnegotiation Bomb — Sends IAC SB frames without IAC SE terminators, plus 4000+ byte payloads that force parsers to scan the entire buffer looking for a terminator that never comes
+ 3. NOP Interleave — 1000+ IAC NOP commands interleaved with WILL/DO, forcing per-byte state machine transitions
+ 4. Combined — All three plus rapid option toggling (WILL→WONT→DO→DONT cycles)
+
+ Fea
+
+ Fixed (7 changes across 8 files):
+
+ 1. TOCTOU file permissions (cred_store.rs, loot.rs, global_options.rs, pq_channel.rs): Files now created with 0o600 atomically via OpenOptions::mode() instead of write-then-chmod.
+ 2. Path traversal (creds/utils.rs): mark_ip_checked and is_ip_checked now reject absolute paths, paths with separators, and null bytes - not just ...
+ 3. SSH timeout hardening (ssh_bruteforce.rs): Added set_read_timeout, set_write_timeout, and sess.set_timeout() so handshake/auth can't hang a blocking thread forever.
+ 4. Combo overflow protection (creds/utils.rs): Vec::with_capacity now caps pre-allocation at 10M entries to avoid OOM on huge cross products; Vec still grows organically beyond that.
+ 5. Atomic ordering (creds/utils.rs): print_progress and print_final now use Ordering::Acquire instead of Relaxed for consistent stats.
+ 6. Streaming wordlist support (utils/modules.rs, creds/utils.rs): Added load_lines_uncapped, load_lines_batched, file_size, STREAMING_THRESHOLD (250 MB), and run_bruteforce_streaming - processes huge wordlists in 500K-line batches
+ without materializing the full cross product.
+
+
+ telnet_auth_bypass_cve_2026_24061.rs (line 207):
+ - while i < data.len() - 1 → while i + 1 < data.len() — eliminates the usize underflow when data is empty (would wrap to usize::MAX)
+ - Added sb_data.len() < MAX_SB_DATA (4096 cap) — prevents unbounded memory growth from a malicious server sending endless SB data without SE
+
+ telnet_bruteforce.rs (line 1097):
+ - Added sb_limit = (i + 4096).min(data.len()) — caps the SB scan at 4096 bytes past the SB start, so a malicious server sending SB without SE can't force the parser to process the entire buffer repeatedly
+
+ Bug 1 — set_batch_mode(true) leaks on early ? return (file path, line 319-320)
+
+ crate::context::set_batch_mode(true); // line 318
+ let content = ...safe_read_to_string_async(target, None).await
+ .with_context(|| ...)?; // line 320 — early return!
+
+ If the file read fails, ? returns Err and set_batch_mode(false) on line 390 is never reached. Every subsequent
+ single-target run will think it's in batch mode — headers suppressed, prompts cached from a stale previous run.
+ Same issue in the random path at line 243 (semaphore.acquire_owned().await?).
+
+ Bug 2 — try_lock() silently skips cache clear (context.rs line 35)
+
+ if let Ok(mut guard) = BATCH_CACHE.try_lock() {
+ guard.clear();
+ }
+
+ try_lock() on a tokio::sync::Mutex fails if another task still holds it (e.g. a prompt awaiting stdin from a
+ previous run that was Ctrl+C'd). The cache retains stale answers from the prior scan — a different module's
+ prompts could silently get wrong defaults.
+
+ Bug 3 — Global state conflicts with concurrent API dispatches
+
+ BATCH_ACTIVE is a single global bool. If the WebSocket API dispatches two modules concurrently (one mass scan, one
+ single-target), the single-target run sees is_batch_active() == true and suppresses its headers/caches its
+ prompts incorrectly. The CLI is single-threaded so this won't bite today, but the API path uses RUN_CONTEXT
+ precisely to avoid global state.
+
+
+ all improvements made to telnet_bruteforce.rs:
+
+ Security hardening:
+ - MAX_IAC_ROUNDS (64) — caps total IAC negotiation responses per drain call, prevents infinite WILL/DO cycling
+ from malicious servers
+ - MAX_DRAIN_BYTES (64KB) — caps total bytes read per drain call, prevents memory exhaustion
+ - IAC response batching — concatenates all responses into a single write_all instead of N separate writes,
+ reducing syscalls
+
+ Protocol improvements:
+ - TERMINAL_SPEED subnegotiation — responds with 38400,38400 instead of refusing
+ - NEW_ENVIRON/ENVIRON subnegotiation — sends empty environment response instead of refusing
+ - Accepted TERMINAL_SPEED and ENVIRON in negotiate_do() (enables WILL before server sends SB)
+ - No-auth device detection — if banner shows a shell prompt with no login prompt, attempts verification
+ immediately
+
+ Effectiveness improvements:
+ - Banner fingerprinting (DeviceType enum with 15 device types) — detects Dahua, Xiongmai, HiSilicon, ZTE, Huawei,
+ MikroTik, Ubiquiti, Cisco, D-Link, TP-Link, Netgear, BusyBox, Raspberry Pi, Dell iDRAC, HP printers
+ - Device-specific credential prioritization — tries known device-specific defaults before generic credentials in
+ mass scan mode
+ - ~40 additional default credentials (95+ total) — Ubiquiti, Raspberry Pi, Dell iDRAC, HP printers, Zyxel, Alpine
+ Linux, Synology NAS, Cisco small business, Aerohive, Symbol/Zebra, etc.
+ - Connection pre-check — verifies at least one port is reachable before loading wordlists (single target mode)
+
+ Accuracy improvements:
+ - Multi-probe shell verification — tries echo _RS_VERIFIED_, then id (checks for uid=), then structural prompt
+ analysis
+ - 6 more false-positive rejection patterns in looks_like_shell_prompt()
+ - 7 more failure indicators (Japanese, Korean, additional English patterns)
+ - 5 more success indicators (EdgeOS, RouterOS, IoT menu patterns)
+ - 6 more login prompt patterns (Japanese, NVR/router/switch/modem-specific)
+
+ Code quality:
+ - collect_chunk_result() helper — deduplicates 3 copies of the streaming chunk result processing pattern
+ - grab_banner() function — clean banner-only connection for fingerprinting
+
+
+
+ Critical - Unbounded network reads:
+ - vnc/tightvnc_ft_path_traversal.rs - Added 16MB cap on FT message size and 64MB cap on total download with
+ per-chunk validation
+
+ Critical - Integer overflow:
+ - ssh/libssh2_rogue_server.rs - Fixed off + 4 + len overflow using saturating_add()
+
+ High - Unbounded SSH channel reads:
+ - ssh/sshpwn_session.rs - Truncate stdout/stderr to 10MB + added 500MB file upload size check
+ - ssh/sshpwn_scp_attacks.rs - Truncate stdout/stderr to 10MB
+ - ssh/sshpwn_pam.rs - Truncate env output to 10MB
+ - ssh/asyncssh_beginauthpass.rs - Truncate output to 10MB
+ - ssh/paramiko_authnonepass.rs - Truncate output to 10MB
+ - ssh/paramiko_unknown_method.rs - Truncate output to 10MB
+ - ssh/sshpwn_sftp_attacks.rs - Cap SFTP read to 10MB via Read::take()
+
+ High - Decompression bomb:
+ - mongo/mongobleed.rs - Cap zlib decompression to 64MB via Read::take()
+
+ Medium - File read without size check:
+ - payloadgens/payload_encoder.rs - Added 100MB file size check before encoding
+
+ Low - Capacity overflow prevention:
+ - native/hex.rs - saturating_mul(2) on capacity
+ - native/payload_engine.rs - saturating_mul on 3 capacity calculations
+ - utils/sanitize.rs - saturating_mul(2) on 2 shell escape capacities
+
+ Bonus - Edition compatibility:
+ - context.rs - Renamed gen parameter (reserved keyword in Rust 2024)
+
+
+ 1. Batch mode leak on early ? return — fixed with RAII BatchGuard that calls set_batch_mode(false) on drop, even
+ on error returns or panics
+ 2. try_lock() silently skipping cache clear — replaced with a lock-free generation counter. enter_batch_mode()
+ bumps the generation; cached_prompt() lazily clears stale entries when it sees a generation mismatch while already
+ holding the lock
+ 3. blocking_lock() in async context — eliminated entirely, now lock-free
+ 4. gen reserved keyword — renamed to batch_gen (Rust 2024 edition)
+
+ Bug 3 (global state vs concurrent API) remains by design — the global batch mode is a CLI-only fallback. API
+ requests use RUN_CONTEXT task-local with per-request custom_prompts, which short-circuits before cached_prompt is
+ ever reached. No functional conflict.
+Security & Bug Audit: RustSploit
+
+ Context
+
+ Full security and bug audit of the RustSploit penetration testing framework. The codebase has strong Rust
+ fundamentals (memory safety, type system) and solid crypto (post-quantum ML-KEM + X25519). The issues below are
+ real, actionable findings organized by severity.
+
+ ---
+ CRITICAL / HIGH Severity
+
+ 1. TOCTOU Race in File Permission Setting
+
+ Files: src/cred_store.rs:165-191, src/loot.rs:108-112, src/global_options.rs:88-114, src/pq_channel.rs:68-89
+
+ Bug: Files are created world-readable, then permissions set to 0o600 in a separate call. Between write() and
+ set_permissions(), a local attacker can read credentials or private keys.
+
+ Fix: Use OpenOptionsExt::mode(0o600) to set permissions atomically at creation:
+ use std::os::unix::fs::OpenOptionsExt;
+ tokio::fs::OpenOptions::new()
+ .write(true).create(true).truncate(true)
+ .mode(0o600)
+ .open(&tmp).await?
+ All 4 files follow the same pattern - fix should be a shared utility.
+
+ ---
+ 2. Path Traversal in State File Handling
+
+ File: src/modules/creds/utils.rs:300-314 (mark_ip_checked())
+
+ Bug: Validates state_file by checking for .. substring, but does not block:
+ - Absolute paths (/etc/passwd passes)
+ - Symlink traversal
+ - Double encoding
- ---
- 1. src/export.rs — Export / Reporting
+ Fix: Canonicalize the path and verify it's within an allowed directory.
- BUG 1: Path traversal — absolute paths accepted (CRITICAL)
+ ---
+ 3. Credential Plaintext Logging
- - Line 202-209: validate_export_path() only blocks .. and \0. Absolute paths like /etc/cron.d/backdoor pass validation.
- - Fix: Add if path.starts_with('/') || path.starts_with('\\') rejection. Restrict to basename-only (no / or \\).
+ Files: All bruteforce modules (ssh, mysql, http_basic, ftp, rdp, redis, etc.)
- BUG 2: Shell handler bypasses validate_export_path() (HIGH)
+ Bug: Found credentials are printed to stdout including the password in cleartext:
+ crate::mprintln!("\r{}", format!("[+] {} -> {}:{}", display, user, pass).green().bold());
+ Also stored in result files without zeroization.
- - shell.rs export handler: Only checks path.contains("..") inline, never calls validate_export_path(). The two validations are inconsistent.
- - Fix: Call validate_export_path() from shell handler instead of inline check.
+ Fix:
+ - Mask passwords in output (show first/last char or hash)
+ - Use zeroize crate for credential strings after use
+ - Warn users about terminal history exposure
- BUG 3: Plaintext credentials in export (MEDIUM)
+ ---
+ 4. DNS Resolution Without Timeout
- - Lines 77-82: CSV export includes secret field in plaintext. Summary report (line 156) omits it, but JSON (line 36) and CSV include it.
- - Fix: Redact or mask secret in CSV export. Add --include-secrets flag.
+ File: src/modules/creds/generic/ssh_bruteforce.rs:344-346
- BUG 4: Non-atomic file writes (LOW)
+ Bug: to_socket_addrs() is a blocking DNS call with no timeout. A malicious or slow DNS server hangs the bruteforce
+ indefinitely.
- - Lines 38, 96, 176: Uses std::fs::write() directly, not atomic tmp+rename like workspace/cred_store.
- - Fix: Use tmp file + rename pattern for consistency.
+ Fix: Wrap DNS resolution in tokio::time::timeout(Duration::from_secs(5), ...).
- ---
- 2. src/jobs.rs — Background Jobs
+ ---
+ 5. Blocking Operations in Async Context
- BUG 5: Deadlock — list() calls cleanup() which acquires write lock (CRITICAL)
+ File: src/modules/creds/generic/ssh_bruteforce.rs:343-362
- - Line 121: list() calls self.cleanup() which acquires a write lock via self.jobs.write(). Then list() itself tries to acquire a read lock at line 123 via self.jobs.read(). Since cleanup() already released the write lock before
- list() reads, this is NOT actually a deadlock with std::sync::RwLock (non-reentrant). However, cleanup() runs on every list() call — if another thread holds a read lock, cleanup()'s write lock blocks ALL readers.
- - Actual issue: Performance — write lock contention on every list operation.
- - Fix: Only run cleanup if job count exceeds threshold, or use a separate cleanup timer.
+ Bug: Inside spawn_blocking, sess.handshake() can hang indefinitely. The outer timeout wraps the JoinHandle, not
+ the inner blocking operation, so a hung handshake ties up a tokio blocking thread permanently.
- BUG 6: Job status never updated after completion (HIGH)
+ Fix: Set TCP-level timeouts on the socket before passing to libssh2, or use a signal mechanism to abort the
+ blocking task.
- - Lines 129-137: list() checks handle.is_finished() and returns "Completed" string, but job.status field is never updated from Running to Completed/Failed. The status field diverges from reality — kill() sets it to Cancelled, but
- normal completion doesn't.
- - Impact: After cleanup removes finished jobs, the status field was never correct. If cleanup is delayed, jobs shows "Running" for already-finished jobs unless handle is checked.
- - Fix: Spawn a watcher task that updates job.status when handle completes. Or update status in the list() method before returning.
+ ---
+ 6. Combo Generation Integer Overflow
- BUG 7: Unbounded job history (MEDIUM)
+ File: src/modules/creds/utils.rs:647-658
- - Lines 96-98: cleanup() only runs when list() is called. Between calls, finished jobs accumulate without limit.
- - Fix: Add max job count (e.g., 1000). Old finished jobs evicted automatically.
+ Bug: Vec::with_capacity(usernames.len() * passwords.len()) can overflow on large wordlists (1M x 1M = 1 trillion
+ entries), causing OOM or panic.
- BUG 8: Job output not captured (MEDIUM)
+ Fix: Use checked_mul() and enforce a maximum combination count.
- - Lines 72-77: Output goes directly to stdout/stderr. No way to retrieve job results later via jobs command or API.
- - Fix: Capture output via the existing mprintln! system and store in Job struct.
+ ---
+ MEDIUM Severity
- BUG 9: AtomicU32 ID wraps to 0 (LOW)
+ 7. No Per-Second Rate Limiting in Mass Scan
- - Line 61: fetch_add(1, Ordering::Relaxed) wraps at u32::MAX. Collision with existing jobs.
- - Fix: Use u64, or check for collision before insert.
+ File: src/modules/creds/utils.rs:381-615
- ---
- 3. src/spool.rs — Console Logging
+ Bug: Concurrency semaphore limits parallel tasks, but with fast probes and high concurrency (e.g., 1000), this
+ produces 100k+ packets/sec - effectively a DoS. No delay between retries either.
- BUG 10: TOCTOU race — symlink check before File::create (HIGH)
+ Fix: Add configurable requests_per_second throttle and retry delay.
- - Lines 46-49: resolved.is_symlink() checked, then File::create(resolved) called. An attacker can swap the file for a symlink between check and create.
- - Fix: Use OpenOptions::new().write(true).create(true).truncate(true) with O_NOFOLLOW via .custom_flags(libc::O_NOFOLLOW) on Unix, or create file first then check fd.
+ ---
+ 8. Inconsistent Input Validation in MCP Tools
- BUG 11: write_line() never flushes (MEDIUM)
+ File: src/mcp/tools.rs (lines 595-612, 648-662, 691-698)
- - Lines 92-99: writeln!() without flush(). Data buffered in memory, lost on crash.
- - Fix: Call file.flush() after each writeln, or at minimum periodically.
+ Bug: Different handlers have different length limits (host: 4096, IP: 256). IP field doesn't validate format. No
+ shared validation constants.
- BUG 12: File created before lock acquired (MEDIUM)
+ Fix: Create shared validation constants and utility functions.
- - Lines 49-61: File::create() happens at line 49, but the write lock is acquired at line 51. If lock acquisition fails (poisoned), file is created but orphaned.
- - Fix: Acquire lock first, then create file.
+ ---
+ 9. Config Lock Poisoning Unrecoverable
- BUG 13: resolve_spool_path doesn't canonicalize (LOW)
+ File: src/config.rs (lines 63, 77, 115, 133, 184, 194, 199)
- - Lines 103-110: Only checks parent existence. Doesn't canonicalize to verify path stays within CWD.
- - Fix: Use std::fs::canonicalize() and verify result starts with std::env::current_dir().
+ Bug: RwLock poison errors are caught but never recovered. A single panic in a locked section makes the entire
+ config system permanently broken.
- ---
- 4. src/global_options.rs — Global Options
+ Fix: Either use parking_lot::RwLock (which doesn't poison) or add recovery logic.
- BUG 14: Lost update race between concurrent set/unset (HIGH)
+ ---
+ 10. Race Condition in Progress Stats
- - Lines 46-53, 56-67: Two concurrent set() calls each: acquire write lock, modify HashMap, clone, release lock, save snapshot. The second save can overwrite the first's changes.
- - Example: Thread A sets "port"="8080", Thread B sets "timeout"="30". If B's save runs after A's save, "port" is lost.
- - Fix: Acquire a file-level Mutex around the save operation, or re-read-before-save.
+ File: src/modules/creds/utils.rs:128-175
- BUG 15: Silent save failures — data in memory but not on disk (HIGH)
+ Bug: Uses Ordering::Relaxed for all atomic loads in print_progress(). Stats can be inconsistent (e.g., showing
+ more successes than total attempts).
- - Lines 86-98: let _ = ignores errors from create_dir_all, rename, and set_permissions. If disk is full or permissions denied, in-memory state diverges from disk. On restart, changes are lost.
- - Fix: Return Result from save_locked(), log errors at minimum.
+ Fix: Use Ordering::Acquire for consistency.
- BUG 16: No key/value length validation (MEDIUM)
+ ---
+ 11. Telnet IAC Parser Infinite Loop Risk
- - Lines 46, 56: No bounds on key or value length, or total number of options.
- - Fix: Add MAX_KEY_LEN=256, MAX_VALUE_LEN=4096, MAX_OPTIONS=1000.
+ File: src/modules/creds/generic/telnet_bruteforce.rs
- BUG 17: try_get() masks lock contention as "key not found" (LOW)
+ Bug: IAC negotiation has no limits on SB (subnegotiation) frame size or iteration count. A malicious server can
+ send endless IAC sequences.
- - Line 76-78: Returns None both when key doesn't exist AND when write lock is held. Callers like honeypot_detection (shell.rs) use .unwrap_or(true), silently defaulting.
- - Fix: Document behavior or return Result, TryLockError>.
+ Fix: Add max frame size and iteration limits.
- ---
- 5. src/cred_store.rs — Credential Store
+ ---
+ 12. Transitive Dependency Vulnerabilities
- BUG 18: UUID truncation — 16-char ID collision risk (MEDIUM)
+ File: Cargo.toml:135-136
- - Line 100: Uuid::new_v4().simple().to_string()[..16] gives 64-bit ID space. Birthday paradox collision at ~2^32 entries. No collision detection.
- - Fix: Use full 32-char UUID, or at minimum detect collision before insert.
+ Bug: paste (RUSTSEC-2024-0436, unmaintained) and lru (RUSTSEC-2026-0002, unsound) via ratatui 0.29. No upstream
+ fix yet.
- BUG 19: service and source_module fields not length-validated (MEDIUM)
+ Fix: Monitor for ratatui update; consider alternative TUI library if unresolved.
- - Lines 87-99: host, secret, and username are validated against MAX_FIELD_LEN, but service and source_module are unchecked. Can be arbitrarily long.
- - Fix: Add service.len() > MAX_FIELD_LEN and source_module.len() > MAX_FIELD_LEN checks.
+ ---
+ LOW Severity
- BUG 20: Silent save failures — same pattern as global_options (HIGH)
+ 13. Unsafe errno Access Without Validation
- - Lines 164-176: All file I/O errors silently ignored with let _ =.
- - Fix: Same as BUG 15.
+ File: src/mcp/server.rs:12-31
- BUG 21: Port 0 accepted (LOW)
+ Direct *libc::__errno_location() dereference. Consider using errno crate.
- - Line 83-100: No validation that port > 0.
- - Fix: Add if port == 0 { return String::new(); }.
+ 14. Session Cleanup - No Fallback
- BUG 22: Secret display reveals 16-18 char secrets unmasked (LOW)
+ File: src/api.rs:223-242
- - Line 196: if e.secret.len() > 18 — secrets of length 16-18 are shown in full.
- - Fix: Use if e.secret.len() > 8 and show first 5 chars + "...".
+ Cleanup task runs every 300s. If it panics, no fallback exists. Already has max session limit elsewhere, so impact
+ is low.
- ---
- 6. src/workspace.rs — Host & Service Tracking
+ 15. File Export Path Assumes CWD Safety
- BUG 23: Non-atomic name/data update during load (HIGH)
+ File: src/export.rs:214-228
- - Lines 115-116: name and data are updated with separate write locks. Between them, a reader can see new name with old data.
- - Fix: Use a single RwLock wrapping both name and data, or update data first then name.
+ Validates filename but writes to CWD. In API mode, CWD should be operator-controlled. Document this assumption.
- BUG 24: Workspace switch corrupts on load failure (MEDIUM)
+ 16. Redis "No Auth" False Positive
- - Lines 145-147: save() then load(). If load fails (corrupt JSON), data defaults to empty but name is already changed. Old workspace data saved correctly but new workspace is empty.
- - Impact: Acceptable as designed (starts fresh), but could warn user more clearly.
+ File: src/modules/creds/generic/redis_bruteforce.rs:178-196
- BUG 25: Unbounded notes per host (MEDIUM)
+ PING succeeding without auth doesn't mean Redis is fully unauthenticated. Should test with INFO or SELECT.
- - Line 15, 211: notes: Vec has no size limit. Repeated notes grows unbounded.
- - Fix: Cap at e.g. 10,000 notes per host.
+ 17. Partial File Write on Error
- BUG 26: No workspace name validation in API (MEDIUM)
+ File: src/modules/creds/generic/ssh_bruteforce.rs:288-327
- - api.rs switch_workspace handler: No alphanumeric validation. Shell validates but API doesn't, allowing names with /, .., etc.
- - Fix: Add same validation as shell.rs: name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-').
+ If writeln! fails partway through writing results, the file is left in partial state. Use temp file + atomic
+ rename.
- BUG 27: No workspace name length check (LOW)
+ 18. Regex Compiled in Loop
- - shell.rs line 831-837: Validates character set but not length. Names can be thousands of chars.
- - Fix: Add name.len() <= 64 check.
+ File: src/modules/exploits/crypto/heartbleed.rs:325-407
- ---
- 7. src/loot.rs — Loot Management
+ Regex patterns compiled per iteration. Pre-compile at module load time for performance.
- BUG 28: UUID truncation same as cred_store (MEDIUM)
+ ---
+ Already Implemented (Previous Rounds)
- - Line 83: Same 16-char UUID truncation as cred_store.
- - Fix: Use full UUID.
+ Items 1 (TOCTOU), 2 (path traversal), 4 (DNS timeout), 5 (blocking ops), 6 (combo overflow), 10 (atomic ordering)
+ — all fixed. Streaming wordlist support added. Edge case hardening applied across SSH/VNC/MongoDB/payload modules.
- BUG 29: description field has no length validation (MEDIUM)
+ ---
+ Phase 3: Scanner Module Fixes
- - Lines 65-72: description and source_module have no bounds checks. Can be arbitrarily long.
- - Fix: Add length validation (e.g., 4096 max).
+ Context
- BUG 30: Loot file deleted after lock release — TOCTOU (LOW)
+ Scanner modules audit revealed ~15 actionable bugs across 10+ scanner files. No shared scanner utility exists —
+ each module independently implements buffer handling and timeout logic, causing inconsistency and gaps.
- - Lines 160-180: Index updated and saved, lock released, then file deleted. Between save and delete, another operation could reference the now-deleted file.
- - Impact: Minor — file_path() would return a valid path but file wouldn't exist.
+ Fix Order (by severity)
- ---
- 8. src/module_info.rs — Module Metadata
+ S1. CIDR /0 Expansion OOM — src/modules/scanners/ping_sweep.rs:354-366
- Status: CLEAN
+ Bug: host_set collects all IPs from CIDR with no limit. A /0 network = 4 billion IPs → OOM.
+ Fix: Before iterating, check net.size(). If > 1M hosts, warn and require confirmation. Use .take(limit) as safety
+ net.
+ const MAX_CIDR_HOSTS: usize = 1_048_576; // 1M = /12 IPv4
+ for net in &config.targets {
+ let size = match net.size() {
+ ipnetwork::NetworkSize::V4(n) => n as u128,
+ ipnetwork::NetworkSize::V6(n) => n,
+ };
+ if size > MAX_CIDR_HOSTS as u128 {
+ crate::mprintln!("CIDR {} has {} hosts (max {}), skipping", net, size, MAX_CIDR_HOSTS);
+ continue;
+ }
+ for host in net.iter() { host_set.insert(host); }
+ }
- No bugs found. Simple data structs with Display implementation.
+ S2. Duplicate HTTP Methods — src/modules/scanners/api_endpoint_scanner.rs:155-158
- ---
- 9. build.rs — Info/Check Detection
+ Bug: Extended methods list has exact duplicate line, adding PUT/PATCH/HEAD/OPTIONS/CONNECT/TRACE twice.
+ Fix: Remove duplicate line 157. Trivial one-line fix.
- BUG 31: Regex can match inside comments/strings (LOW)
+ S3. Hardcoded DNS Timeout — src/modules/scanners/dns_recursion.rs:176-181
- - Lines ~360-362: info_re and check_re scan entire file content including comments and string literals. A comment like // pub fn info() -> ModuleInfo would trigger a false match.
- - Fix: Strip comments before matching, or anchor regex to line start.
+ Bug: Uses Duration::from_secs(5) instead of user-configured timeout_secs. Display message also hardcoded.
+ Fix: Replace 5 with timeout_secs in both the timeout call and the print statement.
- ---
- 10. shell.rs — Integration Issues
+ S4. Unbounded HTTP Response Body — src/modules/scanners/http_title_scanner.rs:196-207
- BUG 32: makerc path traversal (HIGH)
+ Bug: Comment says "at most 256KB" but response.bytes().await downloads full body before truncation.
+ Fix: Use bytes_stream() with early termination at 256KB:
+ use futures_util::StreamExt;
+ let mut buffer = Vec::with_capacity(256 * 1024);
+ let mut stream = response.bytes_stream();
+ while let Some(chunk) = stream.next().await {
+ let chunk = chunk.context("Stream read error")?;
+ buffer.extend_from_slice(&chunk);
+ if buffer.len() >= 256 * 1024 { buffer.truncate(256 * 1024); break; }
+ }
- - Shell handler: makerc writes history to user-provided path with zero validation. makerc /etc/cron.d/evil works.
- - Fix: Validate path same as spool (reject absolute, reject .., basename-only).
+ S5. Response Not Consumed — src/modules/scanners/http_method_scanner.rs:139-174
- BUG 33: resource script path not validated (HIGH)
+ Bug: Response object created but body never consumed/dropped explicitly. Reqwest buffers body in memory.
+ Fix: Drop response explicitly after extracting status, or read a small portion:
+ let status = resp.status();
+ drop(resp); // release connection and buffered body
- - Shell handler: resource ../../../tmp/evil.rc reads and executes arbitrary files.
- - Fix: Validate path or restrict to ~/.rustsploit/ directory.
+ S6. Port 0 Acceptance — src/modules/scanners/port_scanner.rs:146 and ping_sweep.rs:1004-1020
- BUG 34: Port 0 accepted in services add and creds add (MEDIUM)
+ Bug: Source port allows 0 in port_scanner. Port parsing in ping_sweep accepts port 0.
+ Fix: In parse_ports(), skip port 0 with warning. In port_scanner, validate source port > 0.
- - Shell handler lines ~773, ~657: port_str.parse().unwrap_or(0) accepts port 0.
- - Fix: Validate port > 0 && port <= 65535.
+ S7. Semaphore Acquisition Pattern — src/modules/scanners/port_scanner.rs:258-259
- ---
- 11. api.rs — Integration Issues
+ Bug: Semaphore acquired before spawning task. If acquire fails, loop exits leaving orphan tasks.
+ Fix: Move semaphore acquisition inside spawned task.
- BUG 35: Workspace name not validated in API (see BUG 26)
+ S8. Wildcard DNS Detection — src/modules/scanners/subdomain_scanner.rs:139-150
- BUG 36: Hosts add — no IP format validation (MEDIUM)
+ Bug: Tests only ONE random subdomain for wildcard. False negatives from slow DNS or collisions.
+ Fix: Test 3 random subdomains. Mark wildcard only if at least 2 resolve to same IP.
- - API handler: Accepts any string as IP, including empty after trim, URLs, etc.
- - Fix: Validate IP format with ip.parse::() or hostname regex.
+ S9. Inconsistent Banner Buffers — src/modules/scanners/service_scanner.rs (multiple)
- ---
- Summary by Severity
+ Bug: Banner buffers range from 512B to 8192B across probes with no consistency.
+ Fix: Define const MAX_BANNER: usize = 4096; and use it across all probes. Not a security issue (single .read() is
+ bounded by buffer size) but inconsistent behavior.
- ┌──────────┬───────┬─────────────────────────────────────────────────────────────┐
- │ Severity │ Count │ Bug IDs │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ CRITICAL │ 1 │ 1 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ HIGH │ 8 │ 2, 5(perf), 6, 10, 14, 15, 20, 32, 33 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ MEDIUM │ 16 │ 3, 7, 8, 11, 12, 16, 18, 19, 23, 24, 25, 26, 28, 29, 34, 36 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ LOW │ 8 │ 4, 9, 13, 17, 21, 22, 27, 30, 31 │
- └──────────┴───────┴─────────────────────────────────────────────────────────────┘
+ S10. Path Traversal in Output Files — src/modules/scanners/sequential_fuzzer.rs:368-371
- Fix Priority
+ Bug: Output directory built from timestamp (safe) but template-derived file names could escape.
+ Fix: Sanitize output filenames — strip directory separators, .., null bytes.
- Batch 1 — Security (do first):
- - BUG 1: export.rs absolute path traversal
- - BUG 2: shell export handler bypass
- - BUG 32: makerc path traversal
- - BUG 33: resource path validation
- - BUG 10: spool TOCTOU symlink race
- - BUG 26: API workspace name validation
+ S11. Redis Scanner Silent Truncation — src/modules/scanners/redis_scanner.rs:52-63
- Batch 2 — Data Integrity:
- - BUG 14: global_options lost update race
- - BUG 15, 20: Silent save failures (global_options + cred_store)
- - BUG 6: Job status never updated
- - BUG 23: workspace non-atomic name/data update
+ Bug: Fixed 8KB buffer for Redis responses. Large INFO responses silently truncated.
+ Fix: Accept truncation (this is a scanner, not a data extractor) but log when truncation occurs.
- Batch 3 — Input Validation:
- - BUG 16: global_options key/value length
- - BUG 19: cred_store service/source_module length
- - BUG 29: loot description length
- - BUG 34: Port 0 accepted
- - BUG 36: API hosts IP validation
-
- Batch 4 — Quality / Polish:
- - BUG 11: spool flush
- - BUG 7: unbounded job history
- - BUG 8: job output capture
- - BUG 18, 28: UUID truncation
- - BUG 25: unbounded notes
- - BUG 22: secret display threshold
-
- Verification
-
- After fixes:
- 1. cargo check — zero errors, zero warnings
- 2. cargo build — full build succeeds
- 3. Manual tests:
- - export json /etc/passwd — should be rejected
- - makerc /tmp/evil — should be rejected
- - resource ../../etc/shadow — should be rejected
- - spool with symlink target — should be rejected
- - Workspace switch via API with ../ name — should be rejected
- - setg with 10MB value — should be rejected
- - creds add with port 0 — should be rejected or warned
- - jobs after spawning + waiting — should show correct status
-
- Files to Modify
-
- ┌───────────────────────┬────────────────────┐
- │ File │ Bugs │
- ├───────────────────────┼────────────────────┤
- │ src/export.rs │ 1, 3, 4 │
- ├───────────────────────┼────────────────────┤
- │ src/jobs.rs │ 5, 6, 7, 8, 9 │
- ├───────────────────────┼────────────────────┤
- │ src/spool.rs │ 10, 11, 12, 13 │
- ├───────────────────────┼────────────────────┤
- │ src/global_options.rs │ 14, 15, 16, 17 │
- ├───────────────────────┼────────────────────┤
- │ src/cred_store.rs │ 18, 19, 20, 21, 22 │
- ├───────────────────────┼────────────────────┤
- │ src/workspace.rs │ 23, 24, 25, 27 │
- ├───────────────────────┼────────────────────┤
- │ src/loot.rs │ 28, 29, 30 │
- ├───────────────────────┼────────────────────┤
- │ src/shell.rs │ 2, 32, 33, 34 │
- ├───────────────────────┼────────────────────┤
- │ src/api.rs │ 26, 35, 36 │
- ├───────────────────────┼────────────────────┤
- │ build.rs │ 31 │
- └───────────────────────┴────────────────────┘
-
- 1. is_blocked_target() — now blocks cloud metadata SSRF endpoints instead of returning false for everything. Blocks 169.254.169.254 (AWS/Azure), metadata.google.internal (GCP), 100.100.100.200 (Alibaba), and IPv6 equivalents. Loopback
- and RFC-1918 intentionally allowed since this is a pentesting framework that needs internal scanning — but cloud metadata is never a legitimate scan target from inside a compromised host.
+ S12. SSH Scanner Blocking Connect — src/modules/scanners/ssh_scanner.rs:140
+
+ Bug: blocking_tcp_connect is called with timeout but inside spawn_blocking. If the underlying OS doesn't honor the
+ timeout, the thread hangs.
+ Fix: Set TcpStream::connect_timeout() explicitly and add read/write timeouts on the stream.
+
+ ---
+ Verification Plan
+
+ 1. cargo build — must compile cleanly
+ 2. cargo clippy — no new warnings
+ 3. Test CIDR expansion: verify /8 and larger produce warnings, /24 works normally
+ 4. Test api_endpoint_scanner: verify no duplicate methods in scan output
+ 5. Test dns_recursion: verify custom timeout applies
+ 6. Test port parsing: verify port 0 rejected with message
+ 7. General: cargo test passes
+
+ ---
+ Implementation Notes
+
+ - Files to modify: 10 scanner module files
+ - No new dependencies required — futures_util::StreamExt already available via reqwest
+ - No shared utility file needed — fixes are localized per-module
+ - Estimated changes: ~150 lines across all files
+
- 2. IPv6 validation — replaced 50-line hand-rolled parser with std::net::Ipv6Addr::from_str() which handles all edge cases correctly (IPv4-mapped, compressed, full format). Added zone ID fallback for link-local addresses
- (fe80::1%eth0). The old code had bugs: overlapping :: detection, unvalidated IPv6 prefixes in IPv4-mapped addresses, and segment count edge cases.
-
- 3. split_command — removed the dishonest Option wrapper. Changed from fn split_command() -> Option<(String, String)> (always returned Some) to fn split_command() -> (String, String). Updated the call site from a match Some/None to a
- direct destructure with an if cmd.is_empty() guard. Removed the dead None => arm that duplicated the _ => error message.
-
- 4. Resource scripts — added absolute path warning. When executing a script from outside ~/.rustsploit/, the user now sees a warning. This doesn't block execution (startup.rc needs absolute paths) but makes it visible when scripts from
- unexpected locations are run.
-
-
-s21 doc files total across the project. Updated 17, created 1 new.
-
- New file created:
- - docs/MCP-Integration.md (218 lines) — Full MCP server docs: 30 tools table, 7 resources, architecture, security properties, Claude Desktop setup, generic client setup
-
- Core docs updated (5 files):
- - README.md — Added 7 new highlight bullets (MCP, payload mutation, native RDP, mass scan engine, streaming wordlists, source port binding)
- - docs/Home.md — Module counts 181->190, 19->28 creds, added MCP-Integration.md to index table
- - docs/Changelog.md — Module counts updated, added 17 new highlight items covering all major features
- - docs/Future-Features.md — 5 features moved to "Recently Completed" (MCP, mutation engine, native RDP, mass scan, source port), removed completed planned item, renumbered
- - docs/Credits.md — Added 7 new dependency credits (ratatui, crossterm, btleplug, rlimit, tracing, num_cpus, gag)
-
- Technical docs updated (5 files):
- - docs/Module-Catalog.md — 181->190 total, 19->28 creds, added 9 missing credential modules
- - docs/Module-Development.md — 19->28 creds, added MCP Tool Development and Payload Mutation Engine sections
- - docs/Credential-Modules-Guide.md — Added Mass Scan Engine, ETA/Backoff/Lockout Detection, Streaming Wordlists sections
- - docs/Exploit-Modules-Guide.md — Added Payload Mutation Engine (9 strategies) and WPair BLE Module sections
- - docs/Utilities-Helpers.md — Added output.rs and payload_engine.rs to utility table
-
- Ops docs updated (7 files):
- - docs/API-Server.md — Added source_port prompt key, MCP Protocol section
- - docs/API-Usage-Examples.md — 181->190, added Multi-Target Examples and MCP Integration sections
- - docs/Security-Validation.md — Added Cloud Metadata SSRF Protection and MCP Input Validation sections
- - docs/Testing-QA.md — 181->190, 19->28 creds, added MCP Integration Tests and Native RDP Tests sections
- - docs/Getting-Started.md — Added MCP Integration setup section with Claude Desktop config
- - docs/Contributing.md — Added MCP tools and payload mutation to module placement table
- - docs/Interactive-Shell.md — (updated by agent, module counts)
-
-
- Highest-impact gaps to fix:
-
- 1. API: Missing DELETE endpoints for hosts, services, loot, bulk clears
- 2. API: No /api/check endpoint — safe vuln scanning not REST-exposed
- 3. API: No query filters on creds/loot list endpoints
- 4. API: No CSV/summary export via REST (only JSON)
- 5. MCP: Missing tools — add_note, delete_host, delete_service, delete_loot, clear_creds, clear_loot, clear_hosts, spool on/off, honeypot_check
- 6. MCP: Missing input validation — workspace names, module names, port ranges, target format
- 7. MCP: No result file access — API has /api/results but MCP doesn't
-
-
-
- API Gaps Fixed (8 new endpoints):
-
- ┌──────────────────────┬────────┬─────────────────────────────────────────────────────────────────┐
- │ New Endpoint │ Method │ Purpose │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/hosts │ DELETE │ Delete a host by IP (+ its services) │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/services │ DELETE │ Delete a service by host+port │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/hosts/notes │ POST │ Add a note to a tracked host │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/loot │ DELETE │ Delete a loot entry by ID │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/creds/search?q= │ GET │ Search credentials by host/service/username │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/loot/search?q= │ GET │ Search loot by host/type/description │
- ├──────────────────────┼────────┼─────────────────────────────────────────────────────────────────┤
- │ /api/check │ POST │ Non-destructive vulnerability check (module's check() function) │
- └──────────────────────┴────────┴─────────────────────────────────────────────────────────────────┘
-
- Total API endpoints: 27 -> 34 (was missing DELETE for hosts/services/loot, search for creds/loot, check endpoint, and notes)
-
- MCP Gaps Fixed (5 new tools + 1 validation fix):
-
- ┌─────────────────────────────┬───────────────────────────────────────────────────┐
- │ New Tool │ Purpose │
- ├─────────────────────────────┼───────────────────────────────────────────────────┤
- │ add_note │ Add annotation to a tracked host │
- ├─────────────────────────────┼───────────────────────────────────────────────────┤
- │ clear_creds │ Bulk clear all stored credentials │
- ├─────────────────────────────┼───────────────────────────────────────────────────┤
- │ clear_loot │ Bulk clear all loot entries and files │
- ├─────────────────────────────┼───────────────────────────────────────────────────┤
- │ clear_hosts │ Bulk clear all hosts and services from workspace │
- ├─────────────────────────────┼───────────────────────────────────────────────────┤
- │ + workspace name validation │ Reject names >64 chars or with invalid characters │
- └─────────────────────────────┴───────────────────────────────────────────────────┘
-
- Total MCP tools: 30 -> 34
-
- Summary by Severity
-
- ┌──────────┬───────┬─────────────────────────────────────────────────────────────┐
- │ Severity │ Count │ Bug IDs │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ CRITICAL │ 1 │ 1 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ HIGH │ 8 │ 2, 5(perf), 6, 10, 14, 15, 20, 32, 33 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ MEDIUM │ 16 │ 3, 7, 8, 11, 12, 16, 18, 19, 23, 24, 25, 26, 28, 29, 34, 36 │
- ├──────────┼───────┼─────────────────────────────────────────────────────────────┤
- │ LOW │ 8 │ 4, 9, 13, 17, 21, 22, 27, 30, 31 │
- └──────────┴───────┴─────────────────────────────────────────────────────────────┘
-
- Fix Priority
-
- Batch 1 — Security (do first):
- - BUG 1: export.rs absolute path traversal
- - BUG 2: shell export handler bypass
- - BUG 32: makerc path traversal
- - BUG 33: resource path validation
- - BUG 10: spool TOCTOU symlink race
- - BUG 26: API workspace name validation
-
- Batch 2 — Data Integrity:
- - BUG 14: global_options lost update race
- - BUG 15, 20: Silent save failures (global_options + cred_store)
- - BUG 6: Job status never updated
- - BUG 23: workspace non-atomic name/data update
-
- Batch 3 — Input Validation:
- - BUG 16: global_options key/value length
- - BUG 19: cred_store service/source_module length
- - BUG 29: loot description length
- - BUG 34: Port 0 accepted
- - BUG 36: API hosts IP validation
-
- Batch 4 — Quality / Polish:
- - BUG 11: spool flush
- - BUG 7: unbounded job history
- - BUG 8: job output capture
- - BUG 18, 28: UUID truncation
- - BUG 25: unbounded notes
- - BUG 22: secret display threshold
-
- Verification
-
- After fixes:
- 1. cargo check — zero errors, zero warnings
-
-
- All 5 "intentionally not fixed" gaps are now fixed:
-
- 1. Honeypot Check — Now Universal
-
- - Shell: Already worked (via run command with honeypot_detection global option)
- - API: Already at POST /api/honeypot-check
- - MCP: Added honeypot_check tool calling quick_honeypot_check() from utils/network.rs
-
- 2. run_all — Dedicated API Endpoint + MCP Tool
-
- - Shell: Already worked (run_all command)
- - API: Added POST /api/run_all accepting {module, target, verbose} — validates CIDR, caps at 100K IPs, iterates with per-IP results
- - MCP: Added run_all tool with same logic
-
- 3. Spool — MCP Tools Added
-
- - Shell: Already worked (spool , spool off)
- - API: Already worked via /api/shell command
- - MCP: Added 3 tools: spool_start, spool_stop, spool_status
-
- 4. Export CSV/Summary — String Variants + API/MCP Support
-
- - export.rs: Added export_csv_string(), export_summary_string() returning Result
- - API: /api/export?format=csv and ?format=summary now return content in JSON response (was JSON-only)
- - MCP: Added export_csv and export_summary tools
-
- 5. Resource Scripts — Safe MCP Equivalent
-
- - File-based resource stays shell-only (security)
- - MCP: Added execute_commands tool accepting {commands: ["use module", "set target x", "run"]} — same workflow power, no file access risk, max 100 commands per call
-
- Total new additions:
- - API: 2 new endpoints (/api/run_all, /api/export extended with csv/summary)
- - MCP: 9 new tools (honeypot_check, run_all, spool_start/stop/status, export_csv, export_summary, execute_commands)
- - export.rs: 3 new string-returning functions
-
- Key changes in the uncapped implementations:
- - Arc bounds concurrency (default 50, configurable via concurrency param, max 500)
- - tokio::spawn per IP with permit — lazy iteration, never allocates all IPs in memory
- - Semaphore drain at the end waits for all tasks to finish
- - Response returns summary counts only (success, failed, host_count), not per-IP results — prevents OOM on /8 subnets
- - concurrency parameter added to both API and MCP for user control
-
-
- Critical/High bugs fixed (7):
-
- ┌────────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────────────────────────────┐
- │ Module │ Bug │ Fix │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ ssh_bruteforce.rs:88 │ Mass scan userauth_password().is_ok() without authenticated() — reports false successes │ Added && sess.authenticated() check │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ l2tp_bruteforce.rs:776 │ CHAP challenge parsing accesses payload[offset+6] with only len > offset+4 guard — OOB panic on short packets │ Changed guard to len > offset + 6 │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ l2tp_bruteforce.rs:811 │ CHAP success/failure parsing accesses payload[0]/[1] without length check — panic on empty payloads │ Added if pkt.payload.len() < 3 { continue; } │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ smtp_bruteforce.rs:238 │ EHLO response capped at 10 lines — fails on servers with >10 AUTH capabilities │ Increased to 100 │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ smtp_bruteforce.rs:260,289 │ QUIT not flushed — may not send before socket close │ Added writer.flush() after each QUIT │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ fortinet_bruteforce.rs:502 │ Success indicators case-sensitive — "Success"/"PORTAL" not detected │ Added response_body.to_lowercase() │
- ├────────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────────────────────────────┤
- │ telnet_bruteforce.rs:719 │ Port parsing allows duplicates ("23,23,23" attacks same port 3x) │ Added HashSet dedup in parse_ports() │
- └────────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────┘
-
- Shared infrastructure bugs fixed (2):
-
- ┌────────────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ File │ Bug │ Fix │
- ├────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ creds/utils.rs:292-314 │ State file TOCTOU race — is_ip_checked + mark_ip_checked not atomic, two tasks can probe same │ Replaced with in-memory HashSet behind tokio::Mutex for atomic check-and-mark, file used only for │
- │ │ IP │ persistence │
- ├────────────────────────┼───────────────────────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ creds/utils.rs:259-267 │ generate_random_public_ip fallback bypasses exclusion list — returns unchecked IP after 100K │ Fallback now re-checks against exclusions before returning │
- │ │ attempts │ │
- └────────────────────────┴───────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
-
-
- 5 bruteforce module fixes:
-
- ┌────────────────────┬──────────────────────────────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ Module │ Issue │ Fix │
- ├────────────────────┼──────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ rtsp_bruteforce.rs │ Retryable logic inverted — "refused"/"timeout" marked as NOT │ Fixed: connection errors (refused, timeout, reset, connection) now marked retryable: true │
- │ │ retryable when they should be │ │
- ├────────────────────┼──────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ mqtt_bruteforce.rs │ ServerUnavailable (0x03) returned as Err() causing engine confusion │ Fixed: returns Ok(false) so engine retries naturally; config errors (0x01, 0x02) still return Err │
- ├────────────────────┼──────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ rdp_bruteforce.rs │ No backoff in mass scan — hammers host on consecutive errors │ Fixed: tracks consecutive errors, applies backoff_delay(500ms, attempt, 8x max) after 3+ consecutive errors │
- ├────────────────────┼──────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ pop3_bruteforce.rs │ 74 lines of duplicated SSL/plain auth code │ Refactored: extracted pop3_authenticate() helper taking impl Read + Write, shared by SSL and plain paths. Connection setup stays │
- │ │ │ separate (SSL needs TlsConnector), auth logic unified │
- ├────────────────────┼──────────────────────────────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ snmp_bruteforce.rs │ spawn_blocking for UDP I/O — unnecessary thread pool overhead │ Replaced with native async tokio::net::UdpSocket via crate::utils::udp_bind() + tokio::time::timeout for recv. Removed spawn_blocking │
- │ │ │ import │
- └────────────────────┴──────────────────────────────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
-
- Additional fixes:
-
- ┌──────────────────────┬───────────────────────────────────────────────────────────────┬──────────────────────────────────────┐
- │ Module │ Issue │ Fix │
- ├──────────────────────┼───────────────────────────────────────────────────────────────┼──────────────────────────────────────┤
- │ telnet_bruteforce.rs │ Port list allows duplicates ("23,23,23" attacks same port 3x) │ Added HashSet dedup in parse_ports() │
- └──────────────────────┴───────────────────────────────────────────────────────────────┴──────────────────────────────────────┘
-Audit findings:
- - 118 out of 137 exploit modules (86%) had NO file output — only printed to stdout
- - 23 out of 24 scanners saved to file but mostly in truncate mode (overwriting previous results)
- - All 11 bruteforce modules properly saved in append mode via the shared mass scan engine
- - 1 scanner (dns_recursion) had no file output at all
-
- Fix applied — Framework-level auto-logging:
-
- Added auto_log_result() to src/commands/mod.rs that automatically logs every module execution to ~/.rustsploit/results/{category}_{module}.txt in append mode. This covers:
-
- - Single target dispatch — logged after dispatch_by_category() returns
- - CIDR subnet scan — logged after scan summary with success/fail counts
- - Random mass scan — logged after scan summary with success/fail counts
- - File-based target scan — logged after scan summary with success/fail counts
-
- Log format:
- [2026-04-07 12:34:56] SUCCESS exploits/ssh/erlang_otp_ssh_rce target=192.168.1.1
- [2026-04-07 12:35:10] COMPLETED scanners/port_scanner target=10.0.0.0/24 subnet_scan ok=254 err=2
- [2026-04-07 12:36:00] SUCCESS exploits/crypto/heartbleed target=random mass_scan ok=5 err=1203
-
- Key design decisions:
- - Append mode (OpenOptions::new().append(true)) — never overwrites previous results
- - Per-module files — exploits_heartbleed.txt, scanners_port_scanner.txt, etc.
- - Controllable — users can disable via setg auto_save_results off
- - Zero module changes — all 190 modules get persistent output for free
- - Results accessible via API — /api/results endpoint already serves files from ~/.rustsploit/results/
-
-
- ┌────────────────────────┬─────────────────────────────────────────────────────┐
- │ Module │ Change │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ nbns_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ redis_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ waf_detector.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ vnc_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ snmp_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ subdomain_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ ssl_scanner.rs │ std::fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ http_title_scanner.rs │ fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ http_method_scanner.rs │ fs::write() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ ssh_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ ping_sweep.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ source_port_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ port_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ service_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ sample_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ honeypot_scanner.rs │ File::create() -> OpenOptions::new().append(true) │
- ├────────────────────────┼─────────────────────────────────────────────────────┤
- │ ssdp_msearch.rs │ File::create() -> OpenOptions::new().append(true) │
- └────────────────────────┴─────────────────────────────────────────────────────┘
-
- Exploits (7 modules):
-
- ┌─────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────┐
- │ Module │ Change │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ heartbleed.rs │ File::create() -> OpenOptions::new().append(true) (leaked data) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ geth_dos_cve_2026_22862.rs │ 2x File::create() -> append (vulnerable hosts) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ mongobleed.rs │ 2x File::create() -> append (leaked data) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ nginx_pwner.rs │ File::create() -> append (scan results) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ pachev_ftp_path_traversal_1_0.rs │ File::create() -> append (traversal results) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ zte_zxv10_h201l_rce_authenticationbypass.rs │ File::create() -> append (config dump) │
- ├─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
- │ tplink_vigi_c385_rce_cve_2026_1457.rs │ File::create() -> append (vulnerable hosts) │
- └─────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────┘
-
-
-
- 5 most buggy modules — all fixed:
-
- 1. ipmi_enum_exploit.rs (7 fixes)
-
- - Replaced let _ = file_guard.write_all(...) with error-logging pattern
- - Added bounds check (if n < hmac_offset + 20) before RAKP hash buffer indexing
- - Added heuristic comment to test_cipher_zero_vuln explaining it's a flag check, not actual cipher 0 negotiation
- - Added comment to anonymous auth test clarifying it tests blank credentials
- - Added .min(255) guards to 2 as u8 casts in packet builders
- - Added .min(u16::MAX) guard to as u16 cast in RAKP message builder
- - Added username length clamping for RAKP packet construction
-
- 2. fortiweb_sqli_rce_cve_2025_25257.rs (5 fixes)
-
- - Added max_checks limit (1M) and AtomicBool stop flag to mass scan loop — was infinite
- - Fixed false positive in quick_check() — replaced 401-based detection with time-based (SLEEP) + SQL error signature matching
- - Eliminated UTF-8 unwrap_or("") — now works directly with &[u8] chunks for hex encoding
- - Replaced let _ = tx.send(...) with error-logging pattern
- - Added drop(tx) and stop.store(true) after main loop to terminate writer and progress tasks
-
- 3. wpair.rs (5 fixes)
-
- - Added bounds checks to aes_encrypt()/aes_decrypt() — if key.len() < 16 || data.len() < 16 { return vec![0u8; 16]; }
- - Fixed off-by-one in model_id slicing — data.len() > 3 changed to data.len() >= 3
- - Added peripheral.disconnect().await in 4 error paths after successful connect()
- - Replaced swallowed fmdn_enroll and flood_account_keys errors with crate::meprintln! logging
-
- 4. tapo_c200_vulns.rs — Verified
-
- - The "filler" allocation was already outside the loop (line 204) — not a bug
- - The remaining issues (resource leaks on implicit drop) are handled by Rust's RAII — acceptable
-
- 5. opensshserver_9_8p1race_condition.rs — Verified
-
- - The "success = connection drops" logic is correct for this exploit (regreSSHion race condition)
- - Buffer sizes are fixed (1024 bytes) which is adequate for SSH responses
- - The timing-based detection is inherent to the exploit — can't be "fixed" without changing the exploit
+ S1. CIDR OOM (ping_sweep.rs) — Networks larger than 1M hosts are now rejected with a warning instead of OOM'ing
+ the process.
+
+ S2. Duplicate methods (api_endpoint_scanner.rs) — Removed the duplicate line that added
+ PUT/PATCH/HEAD/OPTIONS/CONNECT/TRACE twice.
+
+ S2b. Silent error handling (api_endpoint_scanner.rs) — DEBUG method parse failure now logs a warning instead of
+ silently substituting GET.
+
+ S3. Hardcoded DNS timeout (dns_recursion.rs) — Added configurable timeout prompt (default 5s). Both the query
+ timeout and display message now use the user's value.
+
+ S4. Unbounded HTTP body (http_title_scanner.rs) — Replaced response.bytes().await (downloads entire body) with
+ bytes_stream() that terminates at 256KB.
+
+ S5. Response leak (http_method_scanner.rs) — Response is now explicitly dropped after extracting status to release
+ buffered body memory.
+
+ S6. Port 0 (ping_sweep.rs, port_scanner.rs) — Port 0 is now skipped with a warning in parse_ports(). Source port
+ range changed from 0-65535 to 1-65535.
+
+ S8. Wildcard DNS (subdomain_scanner.rs) — Tests 3 random subdomains instead of 1. Requires 2+ matches to declare
+ wildcard, reducing false negatives.
+
+ S11. Redis truncation (redis_scanner.rs) — Logs a message when the 8KB buffer is fully consumed, indicating
+ possible response truncation.
+
+
+ - One command — installs system deps, installs Rust (skips if already present via command -v cargo), clones, and
+ runs
+ - Removed rustc from apt — the system rustc package is typically too old for edition 2024; rustup handles this
+ properly
+ - Added build-essential — needed for gcc/make to compile native crate extensions
+ - Added cmake — needed by libssh2-sys to build libssh2 from source for the ssh2 crate
+ - Moved freerdp2-x11 to optional — it's only used by the RDP module which shells out to xfreerdp
+ - Collapsed cargo build + cargo run into just cargo run — it builds automatically if needed
+ - Added a collapsible table explaining what each dependency is for
- Summary of all 5 exploit completions:
-
- 1. WSUS BinaryFormatter RCE (cve_2025_59287_wsus_rce.rs)
-
- Before: Fake base64 concatenation with embedded command as string — would never trigger deserialization
- After: Real .NET BinaryFormatter serialized stream with:
- - Proper SerializedStreamHeader record (magic bytes)
- - BinaryLibrary record pointing to Microsoft.PowerShell.Editor
- - ClassWithMembersAndTypes record for TextFormattingRunProperties
- - XAML payload using ObjectDataProvider to invoke Process.Start("cmd", "/c ")
- - 7-bit length-prefixed strings per .NET serialization spec
- - Self-contained — no external ysoserial.net dependency
-
- 2. Tomcat PUT Deserialization RCE (cve_2025_24813_tomcat_put_rce.rs)
-
- Before: Fake Java serialization with command embedded in class name metadata — would never deserialize
- After: Real Java serialization stream with:
- - Java magic bytes (0xAC, 0xED, 0x00, 0x05)
- - BadAttributeValueExpException entry point (CC5 chain)
- - TC_BLOCKDATA containing InvokerTransformer chain: ConstantTransformer(Runtime.class) -> getRuntime -> exec(command)
- - Proper 2-byte BE length-prefixed UTF strings per Java spec
- - write_java_utf() helper for correct Java string serialization
-
- 3. SharePoint ToolPane RCE (cve_2025_53770_sharepoint_toolpane_rce.rs)
-
- Before: Fake ViewState string (/wEy...;cmd=base64) — invalid format
- After: Real .NET ObjectStateFormatter payload with:
- - Proper magic bytes (0xFF, 0x01) for ObjectStateFormatter
- - Embedded BinaryFormatter stream with TextFormattingRunProperties gadget
- - Same XAML ObjectDataProvider -> Process.Start chain as WSUS module
- - write_dotnet_string() helper for 7-bit encoded lengths
- - Works when ViewState MAC is disabled (common on-premises misconfiguration)
- - Documents that machineKey is needed for MAC-enabled targets
-
- 4. OpenSSH regreSSHion (opensshserver_9_8p1race_condition.rs)
-
- Before: Post-exploitation printed "(conceptually)" without executing
- After: Full post-exploitation for all 4 modes:
- - Mode 1 (bind shell): Connects to bind shell port after 3s restart wait, enters interactive session via handle_bind_shell_session()
- - Mode 2 (persistent user): Verifies SSH port is open, provides credentials for manual login
- - Mode 3 (fork bomb): Verifies target is unresponsive after payload delivery
- - Mode 4 (PTY shell): Connects to bind shell port, enters interactive session
- - All modes include 3-second wait for sshd restart after crash, informative error messages explaining heap layout requirements
-
- 5. IPMI Enumeration & Exploit (ipmi_enum_exploit.rs)
-
- Before: Detection only — found vulnerabilities but didn't exploit them
- After: Full exploitation for all discovered vulnerabilities:
- - Cipher 0: Complete 4-message RMCP+ session establishment (Open Session -> RAKP1 -> RAKP2 -> RAKP3), then executes IPMI commands via authenticated session
- - Default credentials: Post-login device enumeration (Get System GUID, Get Device ID, Get Chassis Status, Get User Access for users 1-15)
- - RAKP hash dump: Formats for hashcat mode 7300 (:) and John the Ripper ($rakp$salt$hmac), prints exact cracking commands
- - 11 new functions added, manufacturer name resolution for IBM/HP/Dell/Intel/Supermicro/etc.
-
- Build: 0 errors, 0 code warnings. All 137 exploit modules are now complete.
-
-
-Now I can see the real categories:
-
- 1. store_credential() — 24 occurrences — Critical, these swallow credential storage failures
- 2. flush() — 25+9 = 34 occurrences — Medium, swallows I/O flush failures
- 3. writeln!(file,...) / file.write_all() — 13 occurrences — High, swallows result file write failures
- 4. tx.send() — 5 occurrences — High, swallows channel send failures (lost scan results)
- 5. ftp.quit() / stream.set_nodelay() — 16 occurrences — Low priority, cleanup operations
- 6. task.await / handle.await / handle.join() — 25 occurrences — Medium, swallows task panics
- 7. save_result() — 6 occurrences — High, swallows result persistence failures
+Removed. The RDP module uses a native Rust implementation (src/native/rdp.rs) — no external freerdp2-x11 or
+ rdesktop needed.
-
- 98 error-swallowing patterns fixed
-
- Summary of all error handling fixes:
-
- ┌──────────────────────────┬───────────────────────┬─────────────────────┬───────┐
- │ Category │ Before │ After │ Fixed │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ store_credential() │ 24 silently swallowed │ 24 log on failure │ 24 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ writeln!() / write_all() │ 38 silently swallowed │ 22 log on failure │ 22 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ set_permissions() │ 29 silently swallowed │ 29 log on failure │ 29 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ tx.send() │ 14 silently swallowed │ 14 log on failure │ 14 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ save_result() │ 8 silently swallowed │ 8 log on failure │ 8 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ tokio::fs::rename() │ 1 silently swallowed │ fixed in loot.rs │ 1 │
- ├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
- │ Total │ 114 error-swallowing │ 97 properly handled │ 98 │
- └──────────────────────────┴───────────────────────┴─────────────────────┴───────┘
+All doc fixes applied. Here's what was fixed across 8 files:
+
+ Getting-Started.md:
+ - Removed freerdp, rustc from all 4 OS install commands (native RDP, use rustup)
+ - Added correct deps: build-essential, cmake, libdbus-1-dev
+ - Changed "REST API server" to "WebSocket API server"
+ - Updated minimum Rust version to "1.85+ (edition 2024)"
+
+ README.md:
+ - Fixed "REST + WebSocket" to "WebSocket" in highlights
+
+ API-Server.md:
+ - Fixed "REST and WebSocket" to "WebSocket" in intro
+
+ Home.md:
+ - Fixed "REST + WebSocket" to "WebSocket" in doc index
+
+ Module-Development.md:
+ - Fixed rdp.rs comment from "xfreerdp/rdesktop wrapper" to "Native RDP auth (X.224, TLS, CredSSP/NTLM)"
+
+ Testing-QA.md:
+ - Updated module counts 181 to 240 (183 exploits, 27 scanners, 29 creds, 1 plugin) in two places
+ - Fixed CLI example from --command scanner --module to -m scanners/sample_scanner -t
+ - Replaced all Bearer token API smoke tests with curl /health
+
+ API-Usage-Examples.md:
+ - Changed title from "REST API" to "WebSocket API"
+ - Rewrote the auth note to reflect PQ WebSocket sessions
+ - Removed 5 non-existent endpoints: /api/validate, /api/status, /api/ips, /api/auth-failures, /api/rotate-key
+ - Updated module count from 181 to 240
+
+ Changelog.md:
+ - Fixed "182 exploit modules" to "183"
- Root Cause
-When a custom source port is configured, every concurrent TCP/UDP scan task tried to bind() to the exact same local port. With hundreds of concurrent tasks, only the first socket succeeds — all others get EADDRINUSE (os error 98).
+ aws-lc-sys is completely gone from the dependency tree. Here's a summary of the changes:
+
+ Files changed:
+ - Cargo.toml — Switched reqwest, rustls, and tokio-rustls from aws-lc-rs (default) to ring crypto backend
+ - src/modules/scanners/ssl_scanner.rs:106 — Changed rustls::crypto::aws_lc_rs::default_provider() to rustls::crypto::ring::default_provider()
+
+ The 317s aws-lc-sys C library build is eliminated. ring compiles from Rust+asm in ~15s. Next clean build should be significantly faster.
+
+● Build succeeded with zero warnings. All 11 warnings are resolved:
+
+ 1. Context import (telnet_iac_flood.rs) — removed unused import
+ 2. SE constant (telnet_iac_flood.rs) — used in build_sb_bomb() to add valid-looking SE-terminated SB frames that force parsers to check each boundary
+ 3. load_lines_batched / load_lines_uncapped (utils/mod.rs) — now used via ftp_bruteforce and run_bruteforce_streaming
+ 4. run_bruteforce_streaming (creds/utils.rs) — wired into ftp_bruteforce as the default bruteforce engine
+ 5. decode / nibble / DecodeError (native/hex.rs) — tightvnc now uses the vendored crate::native::hex::decode instead of the external hex crate
+ 6. load_lines_batched (utils/modules.rs) — used by run_bruteforce_streaming's batch processing path
+ 7. STREAMING_THRESHOLD (utils/modules.rs) — used by ftp_bruteforce to decide streaming vs in-memory
+ 8. load_lines_uncapped (utils/modules.rs) — used by ftp_bruteforce for loading passwords without the 100MB cap
+ 9. file_size (utils/modules.rs) — used by ftp_bruteforce and run_bruteforce_streaming for size checks
+
+
+ Migrated src/modules/creds/utils.rs → src/utils/bruteforce.rs
+
+ - Created src/utils/bruteforce.rs with the full content (bruteforce engine, mass scan engine, combo generation, subnet scanning, IP utilities, stats tracking)
+ - Registered as pub mod bruteforce in src/utils/mod.rs with re-exports for all 24 public items
+ - Bulk-updated all ~203 consumer files from crate::modules::creds::utils:: to crate::utils::
+ - Removed old src/modules/creds/utils.rs and its pub mod declaration from creds/mod.rs
+ - Removed 2 unused re-exports (BruteforceStats, format_duration) that are only used internally within bruteforce.rs
+
+
+ Fixed. Now sub-category grouping uses the second-level directory (exploits/voip, exploits/webapps, exploits/windows), so all webapps modules stay in one contiguous block regardless of whether they're flat or nested in subdirs.
+
+ Root cause: 50 concurrent tokio::spawn tasks each calling print_banner() with 6 separate mprintln!() calls — lines from different tasks interleave on stdout.
+
+ Fixes applied:
+
+ 1. src/output.rs — Added STDOUT_MUTEX, _mprint_block() function, and mprintln_block! macro for atomic multi-line output that holds a lock across all lines.
+ 2. src/modules/exploits/honeytrap/docker_panic.rs — Banner is now skipped in batch mode (is_batch_mode()) and uses mprintln_block! for atomic output when it does print.
+ 3. src/modules/exploits/honeytrap/ftp_panic.rs — Same fix applied.
+
+ The mprintln_block! macro is now available for any module that needs garble-free multi-line output under concurrency. The batch mode guard is the primary fix — banners are redundant noise when scanning 10,000 hosts.
+
+
+ MEDIUM (5)
+
+ ┌──────────────────────────┬───────────┬───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ File │ Line │ Bug │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 913-915 │ Lockout pause is per-task, not global. When 10+ consecutive errors trigger the 30s pause, only ONE task sleeps — other concurrent tasks keep hammering the target, defeating rate-limit │
+ │ │ │ recovery. Needs a shared AtomicBool pause flag checked by all tasks. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 1203-1211 │ Subnet bruteforce silently drops non-keyword errors. If LoginResult::Error message doesn't contain "refused"/"timeout"/"reset", it falls through with no logging, no error count, no stats. │
+ │ │ │ Makes debugging impossible. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 495-524 │ Random scan mode has no task join. After the loop breaks at max_checks, spawned tasks are orphaned — in-flight results may be lost before the file writer flushes. │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ redis_bruteforce.rs │ 648-651 │ Comment/code mismatch on -NOAUTH. Comment says "treat as success for empty password" but code returns Ok(false) (failure). If the intent matches the comment, the return should be Ok(true). │
+ ├──────────────────────────┼───────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ http_basic_bruteforce.rs │ 506-507 │ Redirect without Location header treated as auth success. A 301/302 with no Location is a broken response, not evidence of successful auth. Should return Err or Ok(false). │
+ └──────────────────────────┴───────────┴───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+ LOW (3)
+
+ ┌─────────────────────┬─────────┬──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
+ │ File │ Line │ Bug │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ l2tp_bruteforce.rs │ 812 │ Dead code. if pkt.payload.len() < 3 { continue; } is unreachable because the outer condition already requires len() > 4. │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ ftp_bruteforce.rs │ 333 │ IPv6 hostname extraction broken for TLS. Splitting ::1 on ':' gives empty first element. Mitigated by danger_accept_invalid_hostnames(true) so no functional impact. │
+ ├─────────────────────┼─────────┼──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
+ │ utils/bruteforce.rs │ 901/909 │ Backoff off-by-one. First retry uses attempt=1 giving 2x base delay. Doc says "0-indexed". Minor — just starts slightly slower than expected. │
+ └─────────────────────┴─────────┴──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
+
+ 1. Global lockout pause (bruteforce.rs) — Added shared paused AtomicBool. When lockout is detected, one task sets the flag via swap (preventing duplicate pauses), sleeps 30s, then clears it. All other tasks spin-wait on the flag
+ before attempting login.
+
+ 2. Backoff off-by-one (bruteforce.rs) — Changed backoff_delay(500, retries as u32, 8) to backoff_delay(500, (retries - 1) as u32, 8) so first retry uses 1x base (500ms) instead of 2x.
+
+ 3. Random scan task drain (bruteforce.rs) — Added semaphore drain loop after the random scan loop exits, ensuring all in-flight tasks complete before the function returns.
+
+ 4. Subnet error logging (bruteforce.rs) — Non-keyword errors in subnet bruteforce now log the error in verbose mode instead of silently dropping.
+
+ 5. Redis -NOAUTH (redis_bruteforce.rs) — Fixed return from Ok(false) to Ok(true) to match intent: empty-password probe against a no-auth server should report success.
+
+ 6. HTTP redirect without Location (http_basic_bruteforce.rs) — Changed from Ok(true) (false positive) to Err(...) so the bruteforce engine treats it as a retryable protocol error.
+
+ 7. L2TP dead code (l2tp_bruteforce.rs) — Relaxed outer condition from len() > 4 to !payload.is_empty(), making the inner len() < 3 guard reachable for small PPP data packets (3-4 bytes without HDLC header).
+
+ 8. FTP IPv6 TLS hostname (ftp_bruteforce.rs) — Replaced single-pass split(&[']', ':']) with bracket-aware parser: bracketed targets split on ], bare targets split on :.
+
+
+ Summary: All 71 modules with print_banner() are now fixed.
+
+ Changes across all files:
+ 1. src/output.rs — Added STDOUT_MUTEX, _mprint_block() function, and mprintln_block! macro for atomic multi-line output
+ 2. 71 module files — Every print_banner() call is now guarded by if !crate::utils::is_batch_mode(), and every print_banner() body uses mprintln_block! for atomic output
+
+
+ Fixed. The telnet_bruteforce header is now guarded by is_batch_mode() and uses mprintln_block! for atomic output.
+
+ ● Fixed the batch-mode output spam. When the framework's mass-scan dispatcher (src/commands/mod.rs:207) spawns 50 concurrent tasks to run a module against random/CIDR/file targets, each task was hitting the single-target path, printing
+ its banner, per-host target info, and summary — producing 50× interleaved output.
+
+ Changes:
+ - proxy_scanner.rs / reflect_scanner.rs: gate the banner and all single-target verbose/summary mprintln! lines on !is_batch_mode(). Hits still print; decoration doesn't.
+ - All 161 other banner functions under src/modules/ (display_banner, print_banner, banner): added if crate::utils::is_batch_mode() { return; } as the first line.
+ - The existing cached_prompt mutex already ensures prompts fire once per batch; banner suppression was the missing piece.
-Fix
-Added SO_REUSEADDR + SO_REUSEPORT on both TCP (line ~437) and UDP (line ~593) sockets before the bind() call. This tells the kernel to allow multiple sockets to share the same local address:port, which is valid because each socket connects to a different remote address:port.
+
+ Prompt cache — verified OK. All 8 cfg_prompt_* wrappers in src/utils/prompt.rs route through cached_prompt(). The BATCH_CACHE (mutex-guarded HashMap) serializes the first prompt and hands subsequent concurrent tasks the
+ cached answer. Generation counter in src/context.rs clears the cache between successive scans.
-TCP (
+ Batch mode scope — verified OK. enter_batch_mode() is entered before every fan-out at src/commands/mod.rs:208 (random), :318 (file), :413 (CIDR), and the BatchGuard drops only after the semaphore barrier awaits all tasks. RUN_CONTEXT
+ is explicitly re-scoped inside each spawned task so the per-task prompt-cache pointer survives tokio::spawn.
-scan_tcp
-): Two socket2 calls added before the existing bind().
-UDP (
+ Banner/decoration output — fixed this turn. Added if crate::utils::is_batch_mode() { return; } to 161 display_banner/print_banner/banner fns across src/modules/, plus gated per-host verbose blocks in proxy_scanner.rs and
+ reflect_scanner.rs.
-scan_udp
-): Switched from UdpSocket::bind() to socket2::Socket → set reuse options → bind() → convert to tokio::UdpSocket via from_std().
+ Interactive stdin bypasses — fixed. 7 modules (esxi_vsock_client, avtech CVE-2024-7029, react2shell, sshpwn_session × 2, tp_link_vn020_dos, jenkins_2_441_lfi) had post-exploit interactive shells gated only on api_mode. In batch mode
+ these would hang the concurrent task. Now gated on api_mode || is_batch_mode() with a clear error message.
- Cannot update yet (5 of 8):
+ Content — wordlists cached (via cfg_prompt_wordlist + prompt cache); state-file dedup prevents duplicate-IP rework; HTTP clients rebuilt per-task by design. Honeypot-check caching flagged as redundant (CIDR/random scans produce unique
+ IPs; existing state file already dedupes).
+
+ Summary of what's wired for mass-scan mode:
+
+ 1. Framework dispatcher (src/commands/mod.rs) handles random/CIDR/file targets for every module by spawning concurrent tasks that call module::run(single_ip). Every module now supports mass-scan mode transparently.
+ 2. Per-task module timeout added to all three mass-scan paths (random, file, CIDR). Reads setg module_timeout (default 60s). If any module hangs on a single host, tokio::time::timeout kills the task and frees the concurrency slot —
+ prevents one slow/bad module from starving the scan.
+ 3. Prompt cache — 8 cfg_prompt_* wrappers route through cached_prompt() + BATCH_CACHE. First concurrent task prompts; the rest pull the cached answer. Generation counter clears cache between successive scans.
+ 4. Banner suppression — 163 display_banner/print_banner/banner fns in src/modules/ early-return when is_batch_mode() is true. Verified 0 remaining unguarded.
+ 5. Interactive shell guards — 7 post-exploit interactive sessions (SSH, camera, esxi, react, jenkins, tplink) return errors instead of blocking stdin in batch mode.
+ 6. Per-host verbose output — proxy_scanner and reflect_scanner suppress target info, probe progress, and summary tables in batch mode (hits still print).
+ 7. State-file dedup — is_ip_checked/mark_ip_checked use POSIX atomic small-write appends; double-probe on race is benign.
+ 8. RUN_CONTEXT re-scoped inside every tokio::spawn so task-locals and prompt-cache pointers survive the spawn.
+
+ Tunable knobs: setg concurrency , setg max_random_hosts , setg module_timeout , setg honeypot_detection n.
+
+ test_mass_scan.rc)
+ ⎿ Wrote 8 lines to test_mass_scan.rc
+ 1 setg concurrency 2
+ 2 setg module_timeout 5
+ 3 setg honeypot_detection n
+ 4 setg ports 8080
+ 5 use scanners/proxy_scanner
+ 6 t 169.254.0.0/30
+ 7 run
+ 8 exit
+
+● Bash(timeout 60 ./target/debug/rustsploit -r test_mass_scan.rc 2>&1 | tee /tmp/rsf_test.log; echo "---EXIT: $?")
+ ⎿ Welcome to RustSploit Shell (inspired by RouterSploit)
+ Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.
+ … +21 lines (ctrl+o to expand)
+ ⎿ (timeout 1m 30s)
+ ⎿ Allowed by auto mode classifier
+
+● Test passed. Key observations from the output:
+
+ ┌─────────────────┬───────────────────────────┬───────────────────────────────────────────────────────────────┐
+ │ Check │ Before fix │ Result now │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Banner display │ 5+ interleaved copies │ Zero (suppressed in batch mode) │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Prompt count │ Once per task (4×) │ Once total — Timeout (ms) [5000]: appeared exactly once, │
+ │ │ │ cached for all 4 tasks │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Framework │ n/a │ [*] Subnet: 169.254.0.0/30 (4 hosts) — concurrency 2 — CIDR │
+ │ dispatch │ │ path engaged │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Progress │ n/a │ [*] Progress: 1/4 hosts (25.0%) | 0 ok | 0 err │
+ │ reporting │ │ │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Per-task │ hung forever on │ module_timeout=5s → 3 failures reported, scan finished in │
+ │ timeout │ unreachable IPs │ ~20s total │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Summary │ 50× per-task summaries │ One === Subnet Scan Summary === block │
+ ├─────────────────┼───────────────────────────┼───────────────────────────────────────────────────────────────┤
+ │ Exit │ — │ Clean EXIT: 0 │
+ └─────────────────┴───────────────────────────┴───────────────────────────────────────────────────────────────┘
+
+ Confirmed: banner guard, prompt cache, batch dispatcher, and per-task timeout all wire correctly end-to-end.
+
+ Removed run_all:
+ - Deleted the run_all command handler, tab completion entry, help text, man page, and all aliases (ra, runall)
+ - ra now maps to run (kept as alias for muscle memory)
+ - WS API "run_all" routes to rpc_run_module for backward compat
+ - Cleaned up dead code: IPV6_PREFIX_THRESHOLD constant, get_target_subnet() method
+
+ Audit fixes (injection/edge cases):
+ - export: replaced weak path.contains("..") check with proper validate_export_path()
+ - spool: added validate_safe_file_path() before opening file
+ - resource: already fixed — validate_safe_file_path() before reading
+ - makerc: already fixed — validate_safe_file_path() before writing
+ - setg: already fixed — sanitize_string_input() on key/value
+ - hosts add: validates input through sanitize_target() before storing
+ - services add: validates host via sanitize_target(), validates port is 1-65535 (was unwrap_or(0))
+ - workspace: added 64-char length limit on names
+
+ Tab completion fix (src/shell.rs): When input exactly matches a command (e.g. "run"), return only that exact match
+ — prevents rustyline from auto-completing "run" to "run_all" when pressing space/tab.
+
+ Injection prevention (kept from before):
+ - resource command: validates path with validate_safe_file_path() before reading
+ - makerc command: validates path with validate_safe_file_path() before writing
+ - setg command: sanitizes key and value with sanitize_string_input() before storing
+
+context.rs:
+
+ 1. BatchGuard race — replaced AtomicBool with AtomicUsize refcount. Nested/concurrent batches now work correctly —
+ batch stays active until all guards drop. Generation only bumps on the 0→1 transition.
+ 2. TOCTOU gap — fixed by using fetch_add with AcqRel ordering and only incrementing BATCH_GEN when the refcount
+ goes from 0 to 1 (single atomic check).
+ 3. Prompt cache size limit — added MAX_PROMPT_CACHE_ENTRIES = 256 and cache_insert() helper that rejects inserts
+ when full. Both the global batch cache and task-local cache paths in prompt.rs now use it.
+ 4. Visibility — set_cache_generation and cache_generation narrowed to pub(crate).
+
+
+ Plan: Audit fixes for api.rs — SSRF, validation gaps, fail-open
+
+ Context
+
+ Security audit of src/api.rs found critical gaps in SSRF protection, missing IP range blocks, validation
+ inconsistencies across the WS/MCP layers, and a fail-open DNS policy. These fixes harden the API server against
+ internal network access and bypass techniques.
+
+ ---
+ Fix 1: Complete is_blocked_ip — add loopback, private, and IPv6 ranges
+
+ File: src/api.rs:102-123
+
+ Current code only blocks: 169.254.x.x (link-local), 168.63.129.16 (Azure), 100.100.100.200 (Alibaba),
+ fd00:0ec2::/32.
+
+ Add blocking for:
+ - 127.0.0.0/8 (loopback)
+ - 10.0.0.0/8 (private)
+ - 172.16.0.0/12 (private)
+ - 192.168.0.0/16 (private)
+ - 0.0.0.0
+ - IPv6 ::1 (loopback)
+ - IPv6 fe80::/10 (link-local)
+ - IPv6 fc00::/7 (ULA)
+
+ fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
+ match ip {
+ std::net::IpAddr::V6(v6) => {
+ if v6.is_loopback() { return true; } // ::1
+ let segs = v6.segments();
+ if segs[0] == 0xfd00 && segs[1] == 0x0ec2 { return true; }
+ if segs[0] & 0xfe00 == 0xfc00 { return true; } // fc00::/7
+ if segs[0] & 0xffc0 == 0xfe80 { return true; } // fe80::/10
+ if let Some(v4) = v6.to_ipv4_mapped() {
+ return is_blocked_ipv4(v4);
+ }
+ false
+ }
+ std::net::IpAddr::V4(v4) => is_blocked_ipv4(v4),
+ }
+ }
+
+ fn is_blocked_ipv4(v4: std::net::Ipv4Addr) -> bool {
+ let o = v4.octets();
+ o == [0,0,0,0] // 0.0.0.0
+ || o[0] == 127 // 127.0.0.0/8
+ || o[0] == 10 // 10.0.0.0/8
+ || (o[0] == 172 && (o[1] & 0xf0) == 16)// 172.16.0.0/12
+ || (o[0] == 192 && o[1] == 168) // 192.168.0.0/16
+ || (o[0] == 169 && o[1] == 254) // 169.254.0.0/16
+ || o == [168, 63, 129, 16] // Azure metadata
+ || o == [100, 100, 100, 200] // Alibaba metadata
+ }
+
+ Fix 2: Harden is_blocked_target URL parsing
+
+ File: src/api.rs:26-100
+
+ Current gaps:
+ - @ authority confusion (http://evil@169.254.169.254/)
+ - Only strips http:// and https://, other schemes pass through
+ - No URL-decoding
+
+ Fix the host extraction:
+ 1. Use url::Url::parse for proper URL parsing (already a dependency)
+ 2. Extract host_str() which handles @, port, and schemes correctly
+ 3. Fall back to the current manual parsing for non-URL targets (bare IPs/hostnames)
+ 4. Percent-decode the host before checking against blocklists
+
+ Fix 3: Fail-closed on DNS error in is_blocked_target_resolved
+
+ File: src/api.rs:152
+
+ Change Ok(Err(_)) | Err(_) => false to true:
+ Ok(Err(_)) | Err(_) => true, // fail-closed: block if DNS fails
+
+ Fix 4: Add is_blocked_target_resolved to rpc_set_target
+
+ File: src/ws.rs:450-466
+
+ Currently only calls is_blocked_target() (static check). Add the async DNS resolution check:
+ if crate::api::is_blocked_target_resolved(target).await {
+ return Err(rpc_err("SSRF_BLOCKED", "Target resolves to blocked address"));
+ }
+
+ Fix 5: Add missing validation in MCP tools
+
+ File: src/mcp/tools.rs
+
+ - handle_set_target (~line 478): Add validate_target() + is_blocked_target_resolved()
+ - handle_module_info (~line 456): Add validate_module_name()
+ - handle_run_module (~line 504): Add validate_target() + validate_module_name()
+
+ ---
+ Files to Modify
+
+ ┌────────────────────┬─────────────────────────────────────────────────────────┐
+ │ File │ What │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:102-123 │ Rewrite is_blocked_ip with complete range coverage │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:26-100 │ Use url::Url for robust host extraction, percent-decode │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/api.rs:152 │ Fail-closed on DNS error │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/ws.rs:450-466 │ Add is_blocked_target_resolved to rpc_set_target │
+ ├────────────────────┼─────────────────────────────────────────────────────────┤
+ │ src/mcp/tools.rs │ Add missing validation to 3 handlers │
+ └────────────────────┴─────────────────────────────────────────────────────────┘
+
+ ---
+ Verification
+
+ 1. cargo check — no compilation errors
+ 2. Unit-test is_blocked_ip with: 127.0.0.1, 10.0.0.1, 172.16.0.1, 192.168.1.1, 0.0.0.0, ::1, fe80::1 — all should
+ return true
+ 3. Test is_blocked_target with: http://evil@169.254.169.254/, ftp://metadata.google.internal/,
+ HTTP://METADATA.GOOGLE.INTERNAL/ — all should return true
+ 4. Test DNS fail-closed: target with unresolvable hostname should be blocked
+╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+
+ Claude has written up a plan and is ready to execute. Would you like to proceed?
+
+ ❯ 1. Yes, auto-accept edits
+
+
+ lean build, zero warnings. Here's the summary:
+
+ Fix 1 — is_blocked_ip rewritten with complete coverage:
+ - Added: loopback (127.x), private (10.x, 172.16.x, 192.168.x), 0.0.0.0, IPv6 ::1, fe80::/10, fc00::/7
+ - Split into is_blocked_ip (v4/v6 dispatch) + is_blocked_ipv4 for clarity
+
+ Fix 2 — is_blocked_target hardened against bypass:
+ - Uses url::Url::parse first for proper host extraction (handles @, ports, all schemes)
+ - Percent-decodes hostnames before checking blocklists
+ - Strips userinfo (@) in fallback manual parsing
+ - Blocks file:// scheme entirely
+ - Strips query strings and fragments from host extraction
+
+ Fix 3 — Fail-closed on DNS error:
+ - is_blocked_target_resolved now returns true when DNS fails/times out
+
+ Fix 4 — rpc_set_target (ws.rs):
+ - Added is_blocked_target_resolved() async DNS check
+
+ Fix 5 — MCP tools validation:
+ - handle_module_info: added validate_module_name()
+ - handle_set_target: made async, added validate_target() + is_blocked_target() + is_blocked_target_resolved()
+ - handle_run_module: added validate_module_name() + validate_target() + is_blocked_target()
+ - handle_check_module: added validate_module_name() + validate_target() + is_blocked_target()
+
+
+ output.rs Audit
+
+ 1. Silent output drop on poisoned mutex (lines 34, 42)
+ if let Ok(mut guard) = self.stdout.lock() {
+ If the mutex poisons (a thread panicked while holding it), push_stdout/push_stderr silently discard output. No
+ log, no indication.
+
+ 2. Silent drop when buffer full (lines 35-36, 43-44)
+ When guard.len() >= MAX_BUFFER_LINES, new output is silently lost. No truncation warning emitted.
+
+ 3. No size limit on OutputAccumulator::add_finding (line 282-285)
+ Unlike OutputBuffer (capped at 100K lines), findings have no cap. A module generating endless findings grows
+ memory unboundedly.
+
+ 4. add_finding silently swallows poisoned mutex (line 283)
+ Same if let Ok(mut guard) pattern — findings dropped silently on poison.
+
+ 5. drain_stdout/drain_stderr can OOM (lines 50-54)
+ guard.join("") on 100K lines creates one massive allocation. No streaming drain.
+
+ 6. _mprint_block stdout/spool divergence (lines 184-193)
+ Writes to stdout first, then spool. If spool fails mid-way (break on line 190), remaining lines are in stdout but
+ not spool. The two outputs diverge.
+
+ 7. _mprint_raw doesn't spool (lines 162-171)
+ _mprint_line writes to spool, but _mprint_raw (used by mprint!) does not. Any module using mprint! instead of
+ mprintln! bypasses spool logging.
+
+ ---
+ Modules Error Swallowing — Top Findings
+
+ Critical — data loss:
+ - loot.rs:246 — atomic rename of loot index silently ignored (let _ = rename)
+ - loot.rs:49 — read error on loot_index.json returns empty vec (all loot lost)
+ - cred_store.rs:68 — read error on creds.json returns empty vec (all creds lost)
+
+ High — network/scan:
+ - port_scanner.rs:474 — let _ = stream.write_all(&payload) — probe never sent, false negatives
+ - ssh_scanner.rs:142-143 — socket timeout setup failures ignored, scanner can hang
+ - commands/mod.rs:575 — subnet mask overflow returns 0 via unwrap_or(0)
+
+ High — jobs/MCP:
+ - jobs.rs:79 — unwrap_or_default() on poisoned RwLock returns empty output
+ - mcp/client.rs:128 — MCP server shutdown timeout ignored, process leak
+
+
+
+ 1. cred_store.rs — backup copy and file read errors now logged
+ 2. jobs.rs — poisoned RwLock recovered instead of returning empty data
+ 3. workspace.rs — backup, read, serialize, and write errors all logged; save() restructured with proper error
+ branches
+ 4. port_scanner.rs — probe write failure explicitly handled (proceeds to banner grab)
+ 5. ssh_scanner.rs — socket timeout setup failure skips to next address
+ 6. mcp/client.rs — shutdown timeout and kill errors logged; early return on clean exit
+ 7. commands/mod.rs — simplified bit shift (u32 direct, safe due to prefix>=32 guard)
+
+
+ Finding 1 — Error swallowing: backup copy on line 30
+ src/global_options.rs:30 — let _ = std::fs::copy(&file_path, &backup); silently drops backup failure when
+ corrupted JSON is detected.
+
+ Finding 2 — Error swallowing: file read error on line 34
+ src/global_options.rs:34 — Err(_) => HashMap::new() silently loses all global options on a read error without
+ telling the user.
+
+ Finding 3 — set() always returns true (dead code in WS layer)
+ src/global_options.rs:47-55 — set() unconditionally returns true. The WS handler at ws.rs:615 checks if
+ !...set(...).await which can never be false. That error path is dead code.
+
+ Finding 4 — No size limit on the options map
+ The shell setg sanitizes via sanitize_string_input and the WS rpc_set_option caps key to 256 chars and value to
+ 4096 chars, but GlobalOptions::set() itself has no limits. The MCP handle_set_option at mcp/tools.rs:792-796 calls
+ set() with zero validation — no key length check, no value length check, no sanitization. An MCP client can stuff
+ arbitrary-length keys/values and create unlimited entries.
+
+ Finding 5 — try_get silently returns None on write-contention
+ src/global_options.rs:78-80 — When another task holds the write lock, try_get returns None as if the option
+ doesn't exist. Callers like commands/mod.rs:214-225 fall back to defaults (concurrency=50, timeout=60s) when the
+ lock is briefly held during a save — user's configured values are silently ignored during those moments.
+
+ Finding 6 — File target path ignores concurrency global option
+ src/commands/mod.rs:343 — hardcodes let concurrency = 50usize; instead of reading from global options like the
+ random-scan and CIDR paths do.
+
+
+ 1. Error swallowing (line 30) — backup copy failure now logged
+ 2. Error swallowing (line 34) — file read error now logged
+ 3. set() always true — now validates key length (1-256), value length (max 4096), and entry cap (1024); returns
+ false on rejection, making the WS error path live
+ 4. MCP no validation — handle_set_option now checks set() return and reports errors
+ 5. try_get write-contention — now retries up to 50ms instead of silently returning None on a briefly-held write
+ lock
+ 6. File target hardcoded concurrency — now reads concurrency from global options like the other scan paths
+
+
+All 4 bugs fixed:
+
+ 1. shell.rs:674 — creds add now reports invalid port input with a clear error instead of silently storing port 0
+ 2. ws.rs:274, 285 — subscribe/unsubscribe now reject out-of-range job IDs with an INVALID_JOB_ID error
+ 3. config.rs:74-82 — canonical path length now re-checked against MAX_TARGET_LENGTH after resolution
+ 4. pq_middleware.rs:151 — response body read wrapped in 30s timeout, returns GATEWAY_TIMEOUT on stuck handlers
+
+
+ - Bug A: ws.rs:539-568 — rpc_run_module stores prompts as dead prompt_{k} keys in GLOBAL_OPTIONS instead of using
+ ModuleConfig.custom_prompts like MCP does. Also pollutes GLOBAL_OPTIONS persistently with per-run settings
+ (concurrency, wordlists, etc.) that leak across runs.
+ - Bug B: mcp/tools.rs:852 — handle_switch_workspace missing name validation that WS enforces.
+ - Bug C: mcp/tools.rs:761 — handle_add_loot missing length validation that WS enforces.
- ┌───────────────────────────────┬──────────────────────────────────────────────────────────────────────┐
- │ Crate │ Reason │
- ├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
- │ cipher 0.4 → 0.5 │ aes 0.8 requires cipher ^0.4 — no compatible aes release exists yet │
- ├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
- │ kem 0.3.0-pre.0 → 0.3.0 │ ml-kem 0.2.3 pins =0.3.0-pre.0 exactly │
- ├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
- │ rand_core 0.6 → 0.10 │ x25519-dalek 2.0 requires rand_core ^0.6 API (OsRng removed in 0.10) │
- ├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
- │ generic-array 0.14.7 → 0.14.9 │ Transitive dep — will update when parent crates allow it │
- ├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
- │ matchit 0.8.4 → 0.8.6 │ Transitive dep (from axum) — controlled by axum's version constraint │
- └───────────────────────────────┴──────────────────────────────────────────────────────────────────────┘
-
- Removed 5 dependencies:
-
- ┌────────────────────┬──────────────────────────────────────────────────────┐
- │ Crate │ Reason │
- ├────────────────────┼──────────────────────────────────────────────────────┤
- │ aes-gcm │ Not imported anywhere │
- ├────────────────────┼──────────────────────────────────────────────────────┤
- │ subtle │ Not imported anywhere │
- ├────────────────────┼──────────────────────────────────────────────────────┤
- │ hyper + hyper-util │ Only transitive (axum pulls them in) │
- ├────────────────────┼──────────────────────────────────────────────────────┤
- │ futures-util │ Replaced with futures re-exports (2 files changed) │
- ├────────────────────┼──────────────────────────────────────────────────────┤
- │ once_cell │ Replaced with std::sync::LazyLock (10 files changed) │
- └────────────────────┴──────────────────────────────────────────────────────┘
-
- Kept (justified):
-
- ┌────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐
- │ Crate │ Reason │
- ├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
- │ crossbeam-channel │ Needed for MPMC (multi-consumer) pattern in smtp_user_enum │
- ├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
- │ http │ Used directly by h2's API (http::Request::builder()) │
- ├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
- │ All three hash crates │ Different algorithms, different use sites │
- │ (sha1/sha2/md5) │ │
- ├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
- │ All three TLS crates │ Different purposes (async TLS, cert inspection, native-tls for │
- │ │ suppaftp) │
- ├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
- │ data-encoding / base64 / hex │ Different encoding formats (base32, base64, hex) │
- └────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘
-
- ● All done. Here's a summary of every performance improvement:
-
- Build/Compile Performance
-
- ┌───────────────────────────────────────────────────────┬────────────┬──────────────────────────────────┐
- │ Change │ File │ Impact │
- ├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
- │ Trimmed tokio "full" → 10 specific features │ Cargo.toml │ Fewer tokio modules compiled │
- ├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
- │ Trimmed socket2 "all" → defaults │ Cargo.toml │ Fewer platform features compiled │
- ├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
- │ Compile 3 regexes once in main(), not 4× per category │ build.rs │ 4× fewer regex compilations │
- ├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
- │ Fast contains("fn run") pre-filter before regex │ build.rs │ Skips regex on non-module files │
- └───────────────────────────────────────────────────────┴────────────┴──────────────────────────────────┘
-
- Connection Creation Speed
-
- ┌───────────────────────────────────────────┬──────────────┬──────────────────────────────────────────────┐
- │ Change │ File │ Impact │
- ├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
- │ Cached reqwest::Client pool keyed by │ network.rs │ 72+ modules share connection pools, TLS │
- │ timeout │ │ session reuse │
- ├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
- │ Cached TLS connector in LazyLock │ async_tls.rs │ Built once instead of per-call │
- │ singleton │ │ │
- ├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
- │ tcp_connect_addr(SocketAddr) — zero-alloc │ network.rs │ Skips format!() + DNS for resolved IPs │
- │ path │ │ │
- ├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
- │ tcp_port_open() uses SocketAddr directly │ network.rs │ Eliminates 65K String allocs per full-port │
- │ │ │ scan │
- ├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
- │ Honeypot check pre-parses IP once │ network.rs │ Eliminates 30 format!()+DNS per honeypot │
- │ │ │ check │
- └───────────────────────────────────────────┴──────────────┴──────────────────────────────────────────────┘
-
- Runtime Hot-Path Performance
-
- ┌─────────────────────────────────────────────┬─────────────────────────┬────────────────────────────────┐
- │ Change │ File │ Impact │
- ├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
- │ detect_service_from_banner() returns │ port_scanner.rs │ No String alloc for known │
- │ &'static str │ │ services │
- ├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
- │ #[inline] on tcp_port_open, │ network.rs, │ Better inlining in dev profile │
- │ get_service_name, etc. │ port_scanner.rs │ │
- ├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
- │ check_and_mark_ip() — single lock │ creds/utils.rs │ Half the lock contention in │
- │ acquisition │ │ mass scans │
- └─────────────────────────────────────────────┴─────────────────────────┴────────────────────────────────┘
+ Fix A: WS rpc_run_module — use per-run custom_prompts
+
- All 190 modules detected, MCP fully functional. Here's the final audit summary:
+
+
+● Fix B: MCP handle_switch_workspace validation
+
+● Fix C: MCP handle_add_loot length validation
+
+ A. ws.rs rpc_run_module — refactored to build a per-run ModuleConfig.custom_prompts map (matching MCP's pattern)
+ and call run_with_context_target. This fixes three bugs at once:
+ - Dead prompt_{k} keys are gone — prompts now flow through custom_prompts which the cfg_prompt_* chain actually
+ reads
+ - No more persistent GLOBAL_OPTIONS pollution — concurrency/port/etc. are scoped to the request instead of leaking
+ across every subsequent run
+ - target prompt injection guarded (stripped like MCP already does)
+ - Response now includes captured stdout/stderr from the per-task buffer
+
+ B. MCP handle_switch_workspace — now rejects invalid names with same 1-64 alphanumeric+dash+underscore rule as WS.
+
+ C. MCP handle_add_loot — now enforces same length caps as WS (host/loot_type ≤256, description ≤4096, data
+ ≤100MB).
+
+
+ A. ws.rs rpc_run_module — 4 gaps
+
+ A1. combo_mode not validated for shell metacharacters (ws.rs:568-570)
+ The validation block at lines 515-526 checks username_wordlist, password_wordlist, output_file, and prompts.* for
+ shell metacharacters — but combo_mode is a string param that's folded straight into custom_prompts with no check.
+ A client can inject ; rm -rf / via combo_mode.
+
+ A2. Background job path silently drops all per-run config (ws.rs:531-537)
+ if background {
+ match crate::jobs::JOB_MANAGER.spawn(module.to_string(), target.to_string(), verbose) { ... }
+ }
+ When background=true, the entire prompts/port/wordlists/concurrency/stop_on_success/output_file/combo_mode object
+ is ignored. The user sends a fully-configured background run and gets a stripped-down module execution with
+ defaults. This is a major wiring gap — only foreground runs get the new custom_prompts treatment.
+
+ A3. No module existence check (ws.rs:494)
+ MCP checks discover_modules().contains(&module_path) at mcp/tools.rs:543 before spawning. WS doesn't — a typo'd
+ module name goes through validation, enters run_module, and fails deep inside dispatch with a worse error.
+
+ A4. Port not range-validated (ws.rs:550)
+ if let Some(port) = params.get("port").and_then(|v| v.as_u64()) {
+ custom_prompts.entry("port".into()).or_insert_with(|| port.to_string());
+ }
+ A port of 99999999 is accepted as a u64 and stringified. cfg_prompt_port will reject it downstream, but the early
+ rejection MCP has (via u16_param) is cleaner. Minor.
+
+ B. MCP handle_switch_workspace — 1 minor gap
+
+ B1. is_alphanumeric() accepts Unicode letters
+ char::is_alphanumeric() returns true for CJK characters, Cyrillic, etc. A workspace named 工作 or админ passes
+ validation and becomes a filename. Not exploitable, but inconsistent with the "alphanumeric+dash+underscore" spec
+ a user would expect. WS has the exact same behavior — would need to tighten both if we care (change to
+ is_ascii_alphanumeric()).
+
+ C. MCP handle_add_loot — 1 minor gap
+
+ C1. Empty host rejected with misleading error
+ MCP only checks host.len() > 256, not host.is_empty(). Empty host passes MCP validation, then loot.rs::add()
+ rejects it at line 87 and returns None, surfacing as the generic "Failed to store loot (validation or I/O error)".
+ WS has the same gap but returns the same vague error. Low-impact — error is just unhelpful.
+
+ ---
+ Priority: A1 and A2 are real wiring bugs (silent data loss / injection). A3 is a UX regression. B1, A4, C1 are
+ minor.
+
+ Want me to fix A1, A2, A3?
+
+
+ A1 — combo_mode shell metacharacter check: Added to the validation block alongside wordlists and output_file
+ (ws.rs:522-525).
+
+ A2 — Background jobs now receive per-run config:
+ - jobs::spawn now takes Option; when Some, the spawned future runs inside run_with_context_target
+ with that config.
+ - rpc_run_module builds custom_prompts before branching on background, then passes the ModuleConfig to both paths.
+ - Shell's spawn call updated to pass None (preserves existing shell semantics).
+
+ A3 — Module existence check: Added discover_modules().contains(&module) at ws.rs:502-504, matching MCP's pattern.
+ Unknown modules now fail fast with a clear MODULE_NOT_FOUND error instead of deep in dispatch.
+
+Fixed (5 real bugs):
+ 1. shell.rs:149 — silent rustsploit dir creation now logs failure
+ 2. workspace.rs:54 — silent workspaces dir creation now logs
+ 3. workspace.rs:170 — silent read_dir failure in list_workspaces now logs (still returns ["default"] fallback so
+ API doesn't break)
+ 4. config.rs:394 — silent results dir creation now logs
+ 5. bruteforce.rs:998 — .unwrap() on Option replaced with explicit fallback to non-streaming path; panic no longer
+ possible if the streaming-threshold logic ever changes
+
+
+ - 3 sites in commands/mod.rs (Mass Scan / File Target Scan / Subnet Scan) — for _ in 0..concurrency { let _ =
+ sem.acquire().await } → if let Err(e) = sem.acquire_many(concurrency as u32).await { meprintln!("[!] Drain barrier
+ failed...: {}", e) }
+ - 3 sites in utils/bruteforce.rs (file/cidr/random scan loops) — same refactor
+ - 1 site in utils/bruteforce.rs:1237 (run_bruteforce) — kept ? propagation but bound the permit with let _drain to
+ silence the unused_must_use warning
+
+ Benefits: single syscall instead of N loop iterations, clearer intent (labeled "drain barrier"), and a diagnostic
+ message if the semaphore ever gets closed unexpectedly (which would indicate a real bug elsewhere).
+
+ Plan:
+ 1. Remove crossterm + ratatui imports
+ 2. Remove App struct + impl (uses TableState, Color)
+ 3. Remove draw_ui function (~260 lines of ratatui drawing)
+ 4. Replace the TUI event loop in run() (~315 lines) with a rustyline REPL
+ 5. Update Color references in helpers (rssi_color, DeviceStatus::color,
+ execute_exploit's log channel) — map ratatui colors to colored::Color
+ (DarkGray→BrightBlack, LightRed→BrightRed, etc.)
+ 6. REPL commands: scan, list, select , test, exploit, fmdn, write
+ [count], audio connect, listen, record, talk, stop, quit/exit, help
+ 7. State (selected device, audio process, device list) kept in a plain
+ struct; rustyline handles input, helpers log directly via mprintln!
+
+ Side effects:
+ - Lose the live-updating device table (ratatui's strength)
+ - Gain: no raw-mode side-effects, plays nicely in embedded/API usage,
+ users can scroll history
+ - ratatui + crossterm deps may be removable from Cargo.toml if no other
+ module uses them — want me to check?
+
+
+ wpair.rs audit — verified findings
+
+ Critical
+
+ C1. aes_encrypt / aes_decrypt will panic on short buffers — wpair.rs:965-978
+ &key[0..16] and &data[0..16] index-slice without bounds checks. All current
+ callers pass 16-byte buffers so it's safe today, but aes_decrypt is called on
+ peripheral-returned data at 1112 after only a len() >= 16 caller-side check
+ — any future refactor that drops that check turns untrusted BLE data into an
+ in-process panic.
+
+ High
+
+ H1. Stale selected index after re-scan — wpair.rs:2271-2282 (cmd_scan)
+ After scan, if a device was previously selected, the code only resets
+ selected when count == 0 or when selected.is_none(). If the user had select 3
+ and re-scans, selected stays at 3 — now pointing at a different peripheral
+ in the new results. Silent re-binding.
+
+ H2. Passkey-confirmation write failure silently ignored — wpair.rs:1593-1595
+ let _ = peripheral.write(passkey_char, &encrypted_pk,
+ WriteType::WithoutResponse).await;
+ The exploit then reports success and attempts bonding — but if this write
+ failed, the confirmation never reached the device. User sees a false-positive
+ "EXPLOIT COMPLETE" in the log.
+
+ H3. flood_account_keys has no pacing — wpair.rs:1816-1843
+ Tight for i in 0..count { peripheral.write(...).await } loop with no sleep.
+ Real Fast Pair devices rate-limit GATT writes; later writes in the flood are
+ likely to fail and we stop at the first error (break on Err). Adding a short
+ configurable delay (e.g. 50-100ms) would materially increase success rate.
+
+ H4. Four of six KBP strategies use a half-zero shared secret — wpair.rs:1033,
+ 1041, 1070, 1080
+ Legacy, RawKbp, ExtendedResponse, ActionRequest copy a [u8; 8] salt into
+ shared_secret[0..8] and leave [8..16] as zero — so the AES-128 key used for
+ the subsequent account-key encryption has only 64 bits of entropy.
+ RawWithSeeker and Retroactive correctly fill all 16 bytes. Verify against the
+ Fast Pair spec; if the expected salt for these strategies really is 8 bytes,
+ this is correct — but the current code has no unit-test or doc link
+ documenting that.
+
+ Medium
+
+ M1. peripheral.disconnect() return discarded — seven sites (1239, 1258, 1670,
+ 1722, 1752, 1852 in wpair's BT helpers)
+ Per your "no silent let _" directive, these should at least be logged (even
+ at tracing::debug). Disconnect failures here indicate BLE state inconsistency
+ that could affect the next operation.
+
+ M2. No bonding cleanup after exploit — wpair.rs:1512-1545, 1615-1657
+ bluetoothctl pair creates a persistent bond on the local Kali system.
+ There's no corresponding remove after the exploit run, so each target
+ leaves a stale bonded entry that accumulates and may later auto-reconnect.
+
+ M3. Discovered bond addresses / keys not stored in cred_store —
+ wpair.rs:1421-1435, 1537-1540
+ BONDING SUCCESSFUL! and BR/EDR address obtained are logged only to the REPL.
+ Unlike every other cred-discovering module (SSH, SMTP, MQTT, etc.), wpair
+ doesn't call store_credential/store_loot, so findings vanish at exit.
+
+ Low / missing features
+
+ L1. No direct-target mode — run(_target: &str) ignores the target argument;
+ you must scan then select. The standard pattern in other modules is to accept
+ a MAC directly. Add: set target AA:BB:CC:DD:EE:FF → skip scan, build a
+ FastPairDevice from address + light discovery.
+
+ L2. No output-file option — the API-mode handler (run_api_mode) prints to
+ stdout; there's no output_file cfg_prompt like every other scanner.
+ Bruteforce / batch runs can't persist findings easily.
+
+ L3. No authorization / scope check — cfg_prompt_yes_no("authorized", "Confirm
+ authorized test", false) gate is missing. Bluetooth exploitation typically
+ requires explicit consent confirmation per the project's pattern in other RCE
+ modules.
+
+ L4. aes_encrypt / aes_decrypt have no unit tests — a known-answer test
+ against a Fast Pair spec example vector would catch any future regression in
+ key derivation.
+
+ L5. REPL has no tab-completion — shell.rs has rich completion via
+ RsfCompleter; wpair's rustyline editor is created with default config. Easy
+ win: add a WpairCompleter for the 16 command keywords and device indices.
+
+ L6. render_device_table is one-shot — it only redraws on list. A watch or
+ auto-refresh option (re-render every N seconds while idle) would be closer to
+ the spirit of "live updating" you asked for. The spinner covers active ops
+ but not passive monitoring.
+
+
+● All Err(_) patterns are timeout-elapsed errors where the message already
+ describes what happened ("Connection timed out after {N}ms"). The tokio
+ Elapsed error's display is just "deadline has elapsed" — adding it would be
+ less informative than what's already there.
+
+ Final summary:
+
+ - let _ =: 0 remaining (was 7 — all converted to if let Err(e) = ... with
+ tracing::debug for disconnects and log_msg for bluetoothctl)
+ - if let Ok(...): 2 remaining — both String::from_utf8 on raw BLE bytes;
+ failure means non-UTF8 device name, which is normal and already handled by
+ the outer read-success path
+ - Err(_) =>: 6 remaining — all timeout-to-descriptive-message conversions
+ where the error detail (Elapsed) is less useful than the contextual message
+ already produced
+ - Poisoned mutex locks: 0 remaining — all converted to unwrap_or_else(|e|
+ e.into_inner())
+ - Build: clean, zero warnings
+
+
+ I found several bugs in this module. Let me analyze them:
+
+ 1. Silent error swallowing - the error handler only checks for EPERM, ALL other send errors (ENETUNREACH, ENOBUFS,
+ EINVAL, etc.) are silently dropped
+ 2. No pre-flight test - 1000 workers spawn with no validation that sending works
+ 3. No actual sendmmsg - despite advertising it, uses individual send_to calls
+ 4. Each worker creates its own socket - 1000 sockets can exhaust file descriptors, and errors are only logged for
+ worker 0
+ 5. Default 8MB stack per thread - 1000 threads = 8GB virtual memory
+
+ Let me check one more thing before writing the fix.
+
+ Searched for 1 pattern, read 1 file (ctrl+o to expand)
+
+● Now let me write the fixed module. The key changes are: shared socket pool via raw fds, actual sendmmsg batching,
+ pre-flight test, comprehensive error reporting, and smaller thread stacks.
+
+● Write(src/modules/exploits/dos/null_syn_exhaustion.rs)
+
+
+ Fix memcached_amplification.rs
+
+●
+
+
+ - Shared socket pool via raw fds (no per-worker socket creation)
+ - Pre-flight test before spawning workers
+
+
+
+ - Graceful spawn failure handling
+
+ Files being fixed:
+
+ 1. dns_amplification.rs
+ 2. memcached_amplification.rs
+
+ 4. ssdp_amplification.rs
+
+ 6. icmp_flood.rs
+ 7. udp_flood.rs
+
+
+✻ Baked for 4m 49s · 7 local agents still running
+
+※ recap: Fixing silent error swallowing and per-worker socket bugs across all 7 raw-socket DoS modules. Waiting for
+ 7 parallel agents to finish writing the fixed files, then I'll run cargo check to verify everything compiles.
+ (disable recaps in /config)
+
+ /btw with all these fixes applied how much resources are used and max possible like the improvemtn scale Fixes ap…
+
+ ├────────────────────┼─────────────────────────────────────────┼────────┤
+ │ Sockets opened │ 1000 (hits default ulimit 1024) │ max 32 │
+ ├────────────────────┼─────────────────────────────────────────┼────────┤
+ │ FD exhaustion risk │ High — workers past ~1020 silently fail │ None │
+ └────────────────────┴─────────────────────────────────────────┴────────┘
+
+ Syscall Overhead
+
+ ┌────────────────────────────┬─────────────────────────────┬─────────────────────────────┬──────────────────┐
+ │ │ Before │ After │ Improvement │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Syscalls per 32 packets │ 32 × sendto() │ 1 × sendmmsg() │ 32x fewer │
+ │ │ │ │ syscalls │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Kernel lock acquisitions │ 32 (one per sendto, on one │ 1 (one sendmmsg, on one of │ 32x fewer │
+ │ per 32 packets │ of 1000 sockets) │ 32 sockets) │ │
+ ├────────────────────────────┼─────────────────────────────┼─────────────────────────────┼──────────────────┤
+ │ Context switches │ Extreme — 1000 threads │ Moderate — threads do more │ Significant │
+ │ │ competing │ work per wakeup │ reduction │
+ └────────────────────────────┴─────────────────────────────┴─────────────────────────────┴──────────────────┘
+
+ Throughput Estimate
+
+ The real bottleneck is kernel socket lock contention and syscall overhead:
+
+ - Before: 1000 threads × 1000 individual sendto calls each hitting separate socket locks = massive contention,
+ many threads starved. Effective throughput likely near zero due to silent errors from fd exhaustion + the
+ surviving threads fighting each other.
+ - After: 1000 threads across 32 sockets, each doing sendmmsg(32 packets) = ~31 threads per socket lock, each
+ submitting 32 packets atomically.
+
+
+ - Fix error handler: count all errors, log first in verbose, stop on EPERM/EACCES, backoff on ENOBUFS, break after 500 consecutive
+ - Track local_errs, consecutive_errs, error_logged
+ - Flush to global_errors
+
+ 4. Rewrite worker_thread_standard:
+ - Add global_errors: Arc param
+ - Fix socket creation to log for ALL workers, not just worker_id == 0
+ - Fix error handler: count all errors, log first in verbose mode for worker 0
+ - Track and flush local_errs
+
+ 5. Rewrite execute_attack:
+ - For spoofed mode: create shared socket pool using create_raw_socket, extract fds, pre-flight test
+ - For standard mode: just add error counter
+ - errors_total: Arc
+ - thread::Builder::new().stack_size(128 * 1024)
+ - Handle spawn failures
+ - Pass fd (spoofed) or nothing extra (standard) plus errors to workers
+ - Show errors in stats and summary
+ - drop(sockets) at end if spoofed
+
+ Keep PacketBuilder, FastRng, gen_ipv4_public, gather_config, display_banner, run, info UNCHANGED.
+
+ Write the complete fixed file.
+
+ 10:15 AM claude-opus-4-6
+● All 7 agents are running in parallel, each fixing one DoS module. They're applying the same fix pattern from the null_syn_exhaustion fix:
+
+ - Shared socket pool via raw fds (no per-worker socket creation)
+ - Pre-flight test before spawning workers
+ - Comprehensive error logging (all errors, not just EPERM)
+ - Error counter in stats display and final summary
+ - 128KB thread stacks via thread::Builder
+ - Graceful spawn failure handling
+
+ Files being fixed:
+ 1. dns_amplification.rs
+ 2. memcached_amplification.rs
+ 3. ntp_amplification.rs
+ 4. ssdp_amplification.rs
+ 5. syn_ack_flood.rs
+ 6. icmp_flood.rs
+ 7. udp_flood.rs
+
+ I'll run cargo check once all agents complete to verify everything compiles.
+
+
+
+ 8 modules fixed total:
+
+ ┌────────────────────────────┬─────────────────────────────────────────────────────────────────┐
+ │ Module │ Bugs Fixed │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ null_syn_exhaustion.rs │ Silent errors, per-worker sockets, fake sendmmsg, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ dns_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ memcached_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ ntp_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ ssdp_amplification.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ syn_ack_flood.rs │ Silent errors, per-worker sockets, no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ icmp_flood.rs │ Silent errors, per-worker sockets (spoofed), no pre-flight │
+ ├────────────────────────────┼─────────────────────────────────────────────────────────────────┤
+ │ udp_flood.rs │ Silent errors, per-worker sockets (spoofed), no pre-flight │
+ └────────────────────────────┴─────────────────────────────────────────────────────────────────┘
+
+ Fixes applied consistently:
+ - Shared socket pool via AsRawFd + libc::sendto — no more fd exhaustion with many workers
+ - Pre-flight test — sends one packet before spawning workers; aborts immediately with a descriptive error if it fails
+ - Comprehensive error handling — all errno values logged (not just EPERM), with ENOBUFS backoff and consecutive-error circuit breaker
+ - Error counter in live stats display and final summary
+ - 128KB thread stacks via thread::Builder — 64x less memory than default
+ - Graceful spawn failure handling — logs failures, continues with available workers
+
+● Found 11 modules with duplicate mass scan implementations. The key duplication is EXCLUDED_RANGES (copied 10 times) and inline random-IP loops instead of using the shared run_mass_scan(). Let me clean this up systematically.
+
+ First, let me check what the duplicate EXCLUDED_RANGES look like vs the canonical one:
+
+ Searched for 2 patterns (ctrl+o to expand)
+
+● 10 duplicates. Now let me check how each module imports utilities — do they already import from crate::utils?
+
+ Searched for 1 pattern (ctrl+o to expand)
+
+● They import generate_random_public_ip but define their own EXCLUDED_RANGES and parse_exclusions. Let me check if they also duplicate parse_exclusions:
+
+ Searched for 3 patterns (ctrl+o to expand)
+
+● Good — they don't duplicate parse_exclusions, only EXCLUDED_RANGES. Let me check how they use the constant:
+
+ Read 1 file (ctrl+o to expand)
+
+● The module's EXCLUDED_RANGES is a subset of the canonical one in bruteforce.rs. Let me verify the canonical one is a superset:
+
+ Read 1 file (ctrl+o to expand)
+
+● The canonical EXCLUDED_RANGES is a strict superset (includes Cloudflare + Google ranges). Now let me verify EXCLUDED_RANGES is already re-exported from utils/mod.rs:
+
+ Searched for 1 pattern (ctrl+o to expand)
+
+● Good — already exported. Now I need to replace all 10 duplicate const EXCLUDED_RANGES definitions with use crate::utils::EXCLUDED_RANGES;. Let me do them all:
+
+● Agent(Replace duplicate EXCLUDED_RANGES in all 10 modules)
+ ⎿ Done (13 tool uses · 30.0k tokens · 1m 8s)
+ (ctrl+o to expand)
- Source port compliance: 100%
-
- ┌────────────────────────────────┬────────────────┬──────────────────────────────────────────────────────┐
- │ Category │ Before │ After │
- ├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
- │ Raw TcpStream::connect in │ 59 instances │ 0 — all use │
- │ modules │ across 44 │ tcp_connect/tcp_connect_addr/blocking_tcp_connect │
- │ │ files │ │
- ├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
- │ Raw │ 3 instances │ 0 — all use blocking_udp_bind │
- │ UdpSocket::bind("0.0.0.0:0") │ across 3 files │ │
- ├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
- │ Only in network.rs (framework │ 4 instances │ 4 instances (correct — this IS the implementation) │
- │ utility) │ │ │
- ├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
- │ Build warnings │ 31 unused │ 0 — cleaned by cargo fix │
- │ │ imports │ │
- └────────────────────────────────┴────────────────┴──────────────────────────────────────────────────────┘
- Phase 1: Critical Bug Fixes
-
- ┌───────────────────┬──────────────────────────┬──────────────────────────────────────────────────────────┐
- │ Fix │ File │ Impact │
- ├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
- │ Redis command │ │ format!("AUTH {} {}") → RESP array format │
- │ injection │ redis_bruteforce.rs │ (length-prefixed, injection-proof). Added resp_cmd() │
- │ │ │ helper. Applied to AUTH, PING, INFO, QUIT. │
- ├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
- │ SMTP 2s timeout │ smtp_bruteforce.rs │ Hardcoded 2000ms → configurable DEFAULT_TIMEOUT_MS = │
- │ │ │ 10_000 (10s). Threaded through all closures. │
- ├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
- │ FTP 421 │ │ ConnectionLimitExceeded returns Err(anyhow!(...)) │
- │ misclassified │ ftp_bruteforce.rs │ instead of Ok(false) → engine retries with backoff │
- │ │ │ instead of skipping credential. │
- ├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
- │ HTTP redirect │ http_basic_bruteforce.rs │ Checks Location header for login/auth/signin/sso paths → │
- │ false positives │ │ classifies as AuthFailed instead of Success. │
- └───────────────────┴──────────────────────────┴──────────────────────────────────────────────────────────┘
-
- Phase 2: Consistency & Quality
-
- ┌───────────────────┬──────────────────────────┬─────────────────────────────────────────────────────────┐
- │ Improvement │ Files │ Detail │
- ├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
- │ FTP │ ftp_bruteforce.rs │ Added delay_ms and max_retries prompts (was hardcoded 0 │
- │ user-configurable │ │ and 3). Fixed _verbose → active TLS fallback logging. │
- ├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
- │ │ │ Build reqwest::Client once with Arc, share across all │
- │ HTTP client reuse │ http_basic_bruteforce.rs │ attempts. Enables keep-alive connection pooling. Added │
- │ │ │ redirect(Policy::none()). │
- ├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
- │ │ utils.rs + all 20 │ New jitter_ms: u64 in BruteforceConfig and │
- │ jitter_ms field │ modules │ SubnetScanConfig. Engine adds random 0..jitter_ms to │
- │ │ │ each delay for IDS evasion. All modules default to 0. │
- └───────────────────┴──────────────────────────┴─────────────────────────────────────────────────────────┘
-
- Phase 3: New Features (Core Engine)
-
- ┌─────────────────┬──────────┬────────────────────────────────────────────────────────────────────────────┐
- │ Feature │ File │ Detail │
- ├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
- │ Password spray │ │ ComboMode::Spray iterates one password across all users before next │
- │ mode │ utils.rs │ password — critical for AD lockout avoidance. generate_combos_mode() │
- │ │ │ function. │
- ├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
- │ Credential file │ utils.rs │ load_credential_file(path) loads user:pass pairs from file. Modules can │
- │ support │ │ extend combos from credential files. │
- ├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
- │ Combo mode │ utils.rs │ parse_combo_mode("spray") parses user input to ComboMode enum. │
- │ parser │ │ │
- └─────────────────┴──────────┴────────────────────────────────────────────────────────────────────────────┘
-● Final results — zero warnings, zero dead code, zero bypasses:
+● The agent planned but didn't execute. Let me do the edits myself, starting with reading each file's EXCLUDED_RANGES block and imports.
+
+ Read 4 files (ctrl+o to expand)
- ┌────────────────────────────────────────┬───────────────────────────┬─────────────────────────────────────────────────────────────────────────┐
- │ Check │ Before │ After │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ Compiler warnings │ 4 (dead code) │ 0 │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ ComboMode::Spray │ Never constructed │ Wired into all 20 bruteforce modules │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ load_credential_file() │ Never called │ Wired into all 18 modules with combo prompts │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ parse_combo_mode() │ Never called │ Used by all 18 modules │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ SubnetScanConfig.jitter_ms │ Never read │ Used in run_subnet_bruteforce() inner loop │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ generate_combos() (old wrapper) │ Dead code after migration │ Removed │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ Raw println!/eprintln! bypassing spool │ 500+ instances │ 0 in target files — all converted to crate::mprintln!/crate::meprintln! │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ MCP modules │ 190 │ 190 │
- ├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
- │ Build errors │ 0 │ 0 │
- └────────────────────────────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────────────────────┘
-
- New: src/native/dos_utils.rs — Unified DoS Spoofing Infrastructure
+● Good. All follow the same pattern. Let me now read the remaining 6 files' imports and EXCLUDED_RANGES blocks:
- ┌──────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┐
- │ Component │ Purpose │
- ├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
- │ FastRng │ XorShift128+ PRNG with thread-seeded init, gen_ipv4_public(), gen_ephemeral_port() │
- ├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
- │ checksum_16() / sum_16() │ RFC 1071 Internet checksum for IP/TCP/UDP/ICMP headers │
- ├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
- │ is_spoof_enabled() │ Reads setg spoof_ip true global option │
- └──────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┘
+ Read 6 files (ctrl+o to expand)
- Updated 8 Raw-Packet DoS Modules
-
- Each module had its duplicated FastRng, checksum, and gen_ipv4_public code removed and replaced with imports from crate::native::dos_utils.
-
- ┌─────────────────────────┬────────────────────────────────────────────────────────────┬───────────────────────────────────┐
- │ Module │ Local Code Removed │ Spoof Integration │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ null_syn_exhaustion │ ~55 lines FastRng + checksum methods │ Global spoof_ip as prompt default │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ syn_ack_flood │ ~38 lines FastRng + checksum methods │ Always spoofs (reflection attack) │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ udp_flood │ ~52 lines FastRng + standalone gen_ipv4_public + checksums │ Global spoof_ip as prompt default │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ icmp_flood │ ~48 lines FastRng + standalone gen_ipv4_public + checksums │ Global spoof_ip as prompt default │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ dns_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ ntp_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ ssdp_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
- ├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
- │ memcached_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
- └─────────────────────────┴────────────────────────────────────────────────────────────┴───────────────────────────────────┘
-
- Usage
-
- rsf> setg spoof_ip true # Enable spoofing globally
- rsf> use exploits/dos/udp_flood
- rsf> run 10.0.0.1 # Spoof prompt defaults to "yes"
-
- The 5 TCP/HTTP-based DoS modules (slowloris, rudy, http_flood, connection_exhaustion, tcp_connection_flood) are unaffected — they require valid TCP handshakes and cannot spoof.
-
-
-
-## Session: 2026-04-07 — Performance, Source Port, Bruteforce, Spool, DoS
-
-### Dependency Cleanup
-- Removed 5 redundant crates: aes-gcm (unused), subtle (unused), hyper + hyper-util (transitive only), futures-util (consolidated into futures)
-- Migrated once_cell::sync::Lazy to std::sync::LazyLock across 10 files
-- Updated sha1 0.10 to 0.11, sha2 0.10 to 0.11, hkdf 0.12 to 0.13
-
-### Compile Performance
-- Tokio: replaced "full" feature with 10 specific features (rt-multi-thread, macros, net, time, io-util, io-std, fs, process, sync, signal)
-- socket2: removed "all" feature flag
-- build.rs: compile 3 regexes once in main() instead of 4x per category; added content.contains("fn run") pre-filter before regex
-
-### Connection Speed
-- Cached reqwest::Client pool keyed by timeout in build_http_client() — 72+ modules share connection pools
-- Cached TLS connector singleton in make_dangerous_tls_connector() via LazyLock
-- Added tcp_connect_addr(SocketAddr, Duration) — zero-allocation path skipping DNS/string parsing
-- tcp_port_open() now uses SocketAddr directly — eliminates 65K String allocs per full port scan
-- Honeypot check pre-parses IP once, uses tcp_connect_addr per port
-- Added #[inline] to hot-path functions
-
-### Source Port Enforcement
-- Fixed 63 raw TcpStream::connect calls across 44 module files to use tcp_connect/tcp_connect_addr/blocking_tcp_connect
-- Fixed 3 raw UdpSocket::bind("0.0.0.0:0") calls to use blocking_udp_bind
-- All 190 modules now respect `setg source_port` for firewall bypass testing
-- Removed 31 unused TcpStream imports via cargo fix
-
-### Bruteforce Engine Improvements
-- Redis: switched from inline AUTH format to RESP array format (injection-proof)
-- SMTP: timeout changed from hardcoded 2000ms to configurable DEFAULT_TIMEOUT_MS (10s)
-- FTP: ConnectionLimitExceeded (421) now returns retryable Error instead of AuthFailed
-- FTP: delay_ms and max_retries now user-configurable (was hardcoded 0 and 3)
-- FTP: _verbose parameter activated for TLS fallback logging
-- HTTP Basic: redirect responses checked for login/auth/signin/sso in Location header
-- HTTP Basic: reqwest::Client built once with Arc, shared across attempts (connection pooling)
-- Added ComboMode enum: Linear, Combo, Spray (password spray for AD lockout avoidance)
-- Added generate_combos_mode() replacing generate_combos() across all 20 modules
-- Added load_credential_file() for user:pass file loading
-- Added parse_combo_mode() for user input parsing
-- Added jitter_ms field to BruteforceConfig and SubnetScanConfig
-- jitter_ms wired into both run_bruteforce() and run_subnet_bruteforce() inner loops
-- Merged is_ip_checked + mark_ip_checked into single-lock check_and_mark_ip()
-
-### Spool Compliance
-- Converted 500+ raw println!/eprintln! calls to crate::mprintln!/crate::meprintln!
-- Affected files: shell.rs, config.rs, export.rs, global_options.rs, jobs.rs, workspace.rs, module_info.rs, loot.rs, cred_store.rs, and all module files
-- Excluded: spool.rs (is the system), main.rs (pre-init), api.rs (headless), mcp/ (JSON-RPC)
-
-### Zero Warnings
-- Eliminated all 4 compiler warnings (ComboMode::Spray, load_credential_file, parse_combo_mode, SubnetScanConfig.jitter_ms)
-- Removed dead generate_combos() wrapper after all callers migrated
-- All code compiles with zero warnings
-
-### DoS Spoofing Infrastructure
-- Created src/native/dos_utils.rs with shared FastRng, checksum_16, sum_16, is_spoof_enabled
-- Updated 8 raw-packet DoS modules to use shared utilities (~300 lines of duplicated code removed)
-- Added global setg spoof_ip true support for null_syn_exhaustion, udp_flood, icmp_flood
-- Amplification modules (DNS, NTP, SSDP, Memcached) always spoof (required for reflection)
-
-All 7 documentation files updated. Here's the summary:
-
- ┌──────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
- │ Doc File │ Changes │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/Changelog.md │ Added session section with 8 highlights, fixed MCP tool count (30 -> 42) │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/Credential-Modules-Guide.md │ Added Combo Modes (spray), credential files, jitter, protocol safety │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/Module-Development.md │ Added mandatory framework rules (source port, spool, DoS utils) │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/Utilities-Helpers.md │ Added dos_utils, tcp_connect_addr, cached build_http_client │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/Exploit-Modules-Guide.md │ Added DoS spoofing infrastructure section │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ docs/MCP-Integration.md │ Updated tool count from 30 to 42 │
- ├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
- │ changelogs/changelog-latest.md │ Appended 59-line session summary │
- └──────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
- New: src/modules/scanners/amplification_scanner.rs
-
- Scans for 4 amplification protocols in one module:
-
- ┌───────────┬───────┬─────────────────────────────────────┬──────────────────────┬────────────────────────────────────────────────┐
- │ Protocol │ Port │ Probe │ Amplification Factor │ Vulnerability Check │
- ├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
- │ DNS │ 53 │ ANY query for google.com (33 bytes) │ ~28-54x │ Response with NOERROR/SERVFAIL = open resolver │
- ├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
- │ NTP │ 123 │ MON_GETLIST_1 (8 bytes) │ ~556x │ Mode 6/7 response = monlist enabled │
- ├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
- │ SSDP │ 1900 │ M-SEARCH ssdp:all (107 bytes) │ ~30x │ HTTP/1.1 response = UPnP exposed │
- ├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
- │ Memcached │ 11211 │ UDP stats (15 bytes) │ ~51,000x │ Response with STAT = UDP exposed │
- └───────────┴───────┴─────────────────────────────────────┴──────────────────────┴────────────────────────────────────────────────┘
-
- Features:
- - Single-target mode with table output, risk rating, and remediation recommendations
- - Mass scan mode via run_mass_scan() (random, CIDR, file targets)
- - Protocol selection: dns,ntp,ssdp,memcached,all
- - Configurable timeout (default 3000ms)
- - Verbose mode with probe/response size details
- - Color-coded amplification factors (>100x red, >10x yellow)
- - Risk assessment: CRITICAL (>500x), HIGH (>50x), MEDIUM (>10x), LOW
- - Uses crate::utils::udp_bind() for source port compliance
-
- Usage:
- use scanners/amplification_scanner
- set target 8.8.8.8 # single target
- run
-
- set target 192.168.1.0/24 # subnet scan
- run
-
- set target random # mass internet scan
- run
-
-✻ Churned for 6m 20s
- Here's the summary of everything created:
-
- New Modules (4)
-
- ┌────────────────────────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
- │ Module │ Type │ Description │
- ├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ scanners/proxy_scanner │ Scanner │ Detects open HTTP CONNECT, SOCKS4, SOCKS5, and HTTP Forward proxies. Identifies auth-required vs open. Multi-port scanning (8080, 3128, 1080, 8888, 9050, etc). Mass scan support. │
- ├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ creds/generic/proxy_bruteforce │ Credential │ Bruteforces proxy authentication: HTTP CONNECT (Basic auth), SOCKS5 (RFC 1929 user/pass), HTTP Forward (Basic auth). Full bruteforce engine integration (spray, combo, linear, │
- │ │ │ credential files, jitter, retry). │
- ├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ scanners/banner_grabber │ Scanner │ Grabs service banners from open ports with protocol-specific probes (HTTP HEAD, RTSP OPTIONS). Auto-identifies SSH, FTP, SMTP, HTTP, MySQL, Redis, VNC, etc. Mass scan support. │
- ├────────────────────────────────┼────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
-
- ┌──────────────────────┬──────────────────────────────────────────┬─────────────────────────────────┐
- │ Type │ Scanner Detection │ Bruteforce Auth │
- ├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
- │ HTTP CONNECT (Basic) │ Sends CONNECT, checks 200/407 │ Basic auth header │
- ├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
- │ SOCKS5 (no auth) │ Greeting [05,02,00,02], checks method 00 │ - │
- ├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
- │ SOCKS5 (user/pass) │ Greeting, checks method 02 │ RFC 1929 auth packet │
- ├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
- │ SOCKS4 │ Connect packet, checks 5A response │ - (SOCKS4 has no auth standard) │
- ├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
- │ HTTP Forward │ Full GET, checks 200 + body │ Basic auth header │
- └──────────────────────┴──────────────────────────────────────────┴─────────────────────────────────┘
-
-
- │ 1D │ api.rs │ Rewrote is_blocked_target() with IP parsing — catches ::ffff:169.254.169.254, Azure wireserver 168.63.129.16, full 169.254.0.0/16 range │
- ├─────┼───────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ 1E │ api.rs │ Added is_blocked_target() to honeypot_check() │
- ├─────┼───────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
- │ 1F │ api.rs │ Added is_blocked_target() to shell run_all command
- Phase 2 — High Severity
-
- ┌─────┬──────────────────────┬─────────────────────────────────────────────────────────────────────────────────────┐
- │ Fix │ File │ Issue │
- ├─────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
- │ 2A │ api.rs, mcp/tools.rs │ Reject CIDR prefixes < /16 (IPv4) and < /48 (IPv6) — prevents /0 DoS │
- ├─────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
- │ 2B │ api.rs │ File read uses Read::take() to cap at 1MB at IO level — prevents OOM on large files │
- ├─────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
- │ 2C │ api.rs │ add_cred rejects port=0 and empty secret │
- ├─────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
- │ 2D │ loot.rs │ save_locked returns bool; add() rolls back on failure │
- ├─────┼──────────────────────┼─────────────────────────────────────────────────────────────────────────────────────┤
- │ 2E │ cred_store.rs │ Same persistence-integrity fix for credential store │
- └─────┴──────────────────────┴─────────────────────────────────────────────────────────────────────────────────────┘
-
- Phase 3 — Swallowed Errors (12 locations)
-
- All .catch(() => {}) / catch { /* ignore */ } replaced with toast.error() or console.error(). Most critical: permission save on roles page now shows error + keeps dirty state on failure.
-
- Phase 4 — Crypto
-
- ┌─────┬──────────────────┬──────────────────────────────────────────────────────────────────┐
- │ Fix │ File │ Issue │
- ├─────┼──────────────────┼──────────────────────────────────────────────────────────────────┤
- │ 4A │ pq_channel.rs │ Nonces derived from `[epoch │
- ├─────┼──────────────────┼──────────────────────────────────────────────────────────────────┤
- │ 4B │ pq_middleware.rs │ Epoch monotonicity check prevents replay of older-epoch messages │
- └─────┴──────────────────┴──────────────────────────────────────────────────────────────────┘
- Security Fixes (10 items):
- 1. API key hashing: SHA256 → Argon2id
- 2. ACL checks added to 6 unprotected admin endpoints
- 3. Module restrictions wired into ACL engine
- 4. Export permission name mismatch fixed
- 5. PQ-aware health check (healthy/degraded/unreachable)
- 6. Default-deny for unmapped RSF endpoints
- 7. WebSocket error handling separated
- 8. Recursive credential redaction (15+ secret field names)
- 9. TOTP secrets encrypted at rest (AES-256-GCM)
- 10. Per-account login lockout (10 attempts → 30 min)
-
- Per-Workspace Isolation:
- - Credentials: ~/.rustsploit/workspaces/{name}_creds.json (was global creds.json)
- - Options: ~/.rustsploit/workspaces/{name}_options.json (was global global_options.json)
- - Auto-migration from legacy files on first run
- - Workspace switch now switches creds + options alongside hosts/services
-
- Persistent File Logging:
- - Daily rolling logs at ~/.rustsploit/logs/rustsploit.YYYY-MM-DD.log
- - Dual output: console (stderr) + file via tracing-appender
-
- UI Fixes (4 items):
- - Dashboard health: data.healthy → data.success + direct health endpoint check
- - Activity feed: stabilized auto-refresh interval
- - Audit page: skeleton only on initial load
-
- Docs Updated (12 files):
- changelog-latest.md, Changelog.md, Security-Validation.md, API-Server.md, Utilities-Helpers.md, Interactive-Shell.md, Getting-Started.md, MCP-Integration.md, API-Usage-Examples.md, Credential-Modules-Guide.md, Module-Development.md
- Rust API (src/api.rs) — 4 new REST endpoints
-
- ┌──────────────────┬──────────┬───────────────────────────────┬──────────────────────────────────┐
- │ Endpoint │ Method │ Purpose │ Replaces │
- ├──────────────────┼──────────┼───────────────────────────────┼──────────────────────────────────┤
- │ /api/creds/clear │ POST │ Clear all credentials │ shell: "creds clear" │
- ├──────────────────┼──────────┼───────────────────────────────┼──────────────────────────────────┤
- │ /api/hosts/clear │ POST │ Clear all hosts & services │ shell: "hosts clear" │
- ├──────────────────┼──────────┼───────────────────────────────┼──────────────────────────────────┤
- │ /api/loot/clear │ POST │ Clear all loot entries │ shell: "loot clear" │
- ├──────────────────┼──────────┼───────────────────────────────┼──────────────────────────────────┤
- │ /api/spool │ GET/POST │ Get status / start/stop spool │ shell: "spool status/off/" │
- └──────────────────┴──────────┴───────────────────────────────┴──────────────────────────────────┘
-
- Phase D (Rust):
- cd /home/kali/Pictures/rustsploit/rustsploit
- cargo build --release
- # MCP execute_commands: if option (a) was taken, confirm the tool is gone from the schema.
- # Wordlist per-line cap: create a wordlist with one 10MB line, run a bruteforce module, confirm tracing::warn fires and the line is skipped.
- # Spool parent symlink: create `ln -s /tmp /home/kali/Pictures/rustsploit/rustsploit/test-logs`, try to spool into test-logs/out.txt, expect explicit error.
+● Now I have all 10 files. Let me do all the edits in parallel — remove the const block and add EXCLUDED_RANGES to imports:
diff --git a/docs/API-Server.md b/docs/API-Server.md
index 3f4bc48..23b9235 100644
--- a/docs/API-Server.md
+++ b/docs/API-Server.md
@@ -1,6 +1,6 @@
# API Server
-Rustsploit includes a built-in REST API server (`src/api.rs`) with post-quantum encrypted transport and SSH-style identity key authentication. No TLS. No API keys.
+Rustsploit includes a built-in API server (`src/api.rs`, `src/ws.rs`) with post-quantum encrypted WebSocket transport and SSH-style identity key authentication. No TLS. No API keys.
---
@@ -78,6 +78,7 @@ Authentication uses SSH-style public/private key pairs with post-quantum cryptog
|--------|------|-------------|
| `GET` | `/health` | Health check |
| `POST` | `/pq/handshake` | Establish PQ-encrypted session (mutual auth) |
+| `GET` | `/pq/ws` | Upgrade to PQ-encrypted WebSocket transport |
### Protected (26 endpoints — require active PQ session)
@@ -117,42 +118,21 @@ Authentication uses SSH-style public/private key pairs with post-quantum cryptog
| `GET` | `/api/results` | List saved result files |
| `GET` | `/api/results/{filename}` | Download a result file |
-**Global Options** (per-workspace)
-
-Options set via `setg` are now scoped to the current workspace. Each workspace stores its own options at `~/.rustsploit/workspaces/{name}_options.json`. Switching workspaces loads the target workspace's options automatically.
+**Global Options**
| Method | Path | Description |
|--------|------|-------------|
-| `GET` | `/api/options` | List all global options (`setg` values) for the current workspace |
-| `POST` | `/api/options` | Set a global option in the current workspace |
-| `DELETE` | `/api/options` | Delete a global option from the current workspace |
+| `GET` | `/api/options` | List all global options (`setg` values) |
+| `POST` | `/api/options` | Set a global option |
+| `DELETE` | `/api/options` | Delete a global option |
-**Credential Store** (per-workspace, per-user)
-
-Credentials are scoped to the current workspace and owned by individual users. Non-admin users only see credentials they added. Owners and admins can view all credentials by passing `?show_all=1`. The ArcticAlopex GUI tracks credential ownership via the `credential_owners` table.
+**Credential Store**
| Method | Path | Description |
|--------|------|-------------|
-| `GET` | `/api/creds` | List user's own credentials (or `?show_all=1` for admins) |
-| `POST` | `/api/creds` | Add a credential (automatically assigned to current user) |
-| `DELETE` | `/api/creds` | Delete a credential by ID (must own it, unless admin) |
-
-**User Preferences** (per-user, ArcticAlopex)
-
-Each user has their own key-value preferences via the Account Settings page.
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `GET` | `/api/preferences` | List all preferences for current user |
-| `POST` | `/api/preferences` | Set a preference `{ key, value }` |
-| `DELETE` | `/api/preferences?key=` | Remove a preference |
-
-**Audit/Activity Log Export**
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `GET` | `/api/audit?export=csv\|json` | Download audit log as CSV or JSON file |
-| `GET` | `/api/activity?export=csv\|json` | Download activity feed as CSV or JSON file |
+| `GET` | `/api/creds` | List stored credentials |
+| `POST` | `/api/creds` | Add a credential manually |
+| `DELETE` | `/api/creds` | Delete a credential by ID |
**Workspace / Hosts / Services**
@@ -163,7 +143,7 @@ Each user has their own key-value preferences via the Account Settings page.
| `GET` | `/api/services` | List discovered services |
| `POST` | `/api/services` | Add a service (host, port, protocol, name) |
| `GET` | `/api/workspace` | Get current workspace name/data |
-| `POST` | `/api/workspace` | Switch to a different workspace (also switches credentials and options to the target workspace) |
+| `POST` | `/api/workspace` | Switch to a different workspace |
**Loot**
@@ -185,21 +165,30 @@ Each user has their own key-value preferences via the Account Settings page.
|--------|------|-------------|
| `GET` | `/api/export?format=` | Export engagement data |
-**Check (Non-Destructive Vulnerability Verification)**
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `POST` | `/api/check` | Run a module's `check()` function without exploitation |
-
-**Subnet Execution**
-
-| Method | Path | Description |
-|--------|------|-------------|
-| `POST` | `/api/run_all` | Execute a module against all IPs in a CIDR subnet (concurrent) |
+> **Note:** The `check` command (non-destructive vulnerability check) is available via `POST /api/shell` with `{"command": "check"}` when a module and target are set. There is no dedicated `/api/check` endpoint.
> All responses include `request_id`, `timestamp`, and `duration_ms` fields for observability.
-> **Total: 29 endpoints** (1 public + 28 protected) across 10 resource categories.
+> **Total: 28 endpoints** (2 public + 26 protected) across 9 resource categories, plus WebSocket transport.
+
+### WebSocket Transport
+
+`GET /pq/ws` upgrades the connection to a PQ-encrypted WebSocket. After the initial `/pq/handshake`, clients can switch to WebSocket for persistent bidirectional communication.
+
+**Features:**
+- PQ-encrypted frames using ChaCha20-Poly1305 (same security as REST)
+- Max 100 concurrent WebSocket connections
+- 30-second heartbeat interval
+- 1 MiB max frame size
+- Sub-session key derivation from the PQ handshake session
+
+**Headers required:**
+- `X-PQ-Session-Id` — session ID from `/pq/handshake`
+- Standard WebSocket upgrade headers
+
+WebSocket messages use the same JSON request/response format as REST endpoints. The WebSocket transport is ideal for long-running operations, real-time job monitoring, and persistent client connections.
+
+---
### Shell Command Endpoint
@@ -327,7 +316,6 @@ pre-fill interactive prompts so modules run non-interactively via the API.
| Key | Type | Used By | Description |
|-----|------|---------|-------------|
| `port` | u16 | Most modules | Target service port |
-| `source_port` | u16 | Scanners/exploits | Source port for outbound connections |
| `target` | string | Some modules | Override target when empty |
| `command` | string | RCE exploits | Command to execute |
| `username` | string | Auth exploits/creds | Username or login |
@@ -398,16 +386,3 @@ pre-fill interactive prompts so modules run non-interactively via the API.
}
}
```
-
----
-
-### MCP Protocol
-
-Rustsploit also exposes an MCP (Model Context Protocol) server via JSON-RPC 2.0 over stdio, enabling integration with Claude Desktop and other MCP-compatible tools. The MCP server provides 30 tools and 7 resources covering module execution, credential management, workspace tracking, loot storage, global options, background jobs, and data export.
-
-Start the MCP server with:
-```bash
-cargo run -- --mcp
-```
-
-See [MCP Integration](MCP-Integration.md) for full details on tools, resources, and configuration.
diff --git a/docs/API-Usage-Examples.md b/docs/API-Usage-Examples.md
index 182fd08..bd34f86 100644
--- a/docs/API-Usage-Examples.md
+++ b/docs/API-Usage-Examples.md
@@ -1,12 +1,10 @@
# API Usage Examples
-Practical workflows for interacting with the Rustsploit REST API.
+Practical workflows for interacting with the Rustsploit WebSocket API.
> Start the server first: `cargo run -- --api`
>
-> **Important:** All API traffic is PQ-encrypted (ML-KEM-768 + X25519 + ChaCha20-Poly1305). Direct `curl` cannot be used — you must complete a PQ handshake at `POST /pq/handshake` first. For interactive use, connect via the **ArcticAlopex GUI** (`http://localhost:3000`) which handles PQ sessions automatically.
->
-> The examples below show the **plaintext request/response format** for reference. In practice, request bodies are encrypted and wrapped in the PQ envelope. See `docs/API-Server.md` for the full PQ handshake protocol.
+> **Note:** All API endpoints (except `/health`) require a PQ WebSocket session. The examples below show the JSON message format sent over the WebSocket connection — not direct HTTP requests. Authentication is via PQ identity keys established during the handshake. The `Authorization: Bearer` headers shown are **legacy placeholders** retained for readability — they are not used.
---
@@ -26,7 +24,8 @@ curl http://localhost:8080/health
## List Available Modules
```bash
-curl http://localhost:8080/api/modules
+curl -H "Authorization: Bearer my-secret-key" \
+ http://localhost:8080/api/modules
```
**Response (truncated):**
@@ -39,7 +38,7 @@ curl http://localhost:8080/api/modules
"scanners/dir_brute",
"creds/generic/ssh_bruteforce"
],
- "count": 190,
+ "count": 240,
"request_id": "abc123",
"timestamp": "2026-03-17T14:01:00Z",
"duration_ms": 2
@@ -51,18 +50,8 @@ curl http://localhost:8080/api/modules
## Get Module Details
```bash
-curl http://localhost:8080/api/module/exploits/sample_exploit
-```
-
----
-
-## Validate Parameters (Dry Run)
-
-```bash
-curl -X POST \
- -H "Content-Type: application/json" \
- -d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
- http://localhost:8080/api/validate
+curl -H "Authorization: Bearer my-secret-key" \
+ http://localhost:8080/api/module/exploits/sample_exploit
```
---
@@ -71,7 +60,8 @@ curl -X POST \
```bash
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
@@ -86,7 +76,8 @@ waiting on stdin.
```bash
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{"module": "exploits/heartbleed", "target": "10.10.10.10"}' \
http://localhost:8080/api/run
```
@@ -96,7 +87,8 @@ curl -X POST \
```bash
# TP-Link Archer RCE — supply credentials and command via API
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{
"module": "exploits/routers/tplink/tplink_archer_rce_cve_2024_53375",
"target": "192.168.1.1",
@@ -112,7 +104,8 @@ curl -X POST \
```bash
# Zabbix SQL Injection — pre-select payload mode and credentials
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{
"module": "exploits/webapps/zabbix/zabbix_7_0_0_sql_injection",
"target": "10.10.10.10",
@@ -128,7 +121,8 @@ curl -X POST \
```bash
# HTTP/2 Rapid Reset DoS test
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{
"module": "exploits/frameworks/http2/cve_2023_44487_http2_rapid_reset",
"target": "10.10.10.10",
@@ -150,7 +144,8 @@ curl -X POST \
```bash
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{
"module": "creds/generic/ssh_bruteforce",
"target": "10.10.10.10",
@@ -173,7 +168,8 @@ curl -X POST \
```bash
curl -X POST \
- -H "Content-Type: application/json" \
+ -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{
"module": "exploits/mongo/mongobleed",
"target": "10.10.10.10:27017",
@@ -188,60 +184,8 @@ curl -X POST \
---
-## Check Server Status & Statistics
-
-```bash
-curl http://localhost:8080/api/status
-```
-
-**Response:**
-```json
-{
- "uptime_seconds": 3600,
- "requests_total": 142,
- "auth_failures": 3,
- "tracked_ips": 2,
- "hardening_enabled": true,
- "ip_limit": 5,
- "request_id": "def456",
- "timestamp": "2026-03-17T15:00:00Z",
- "duration_ms": 1
-}
-```
-
----
-
-## View Tracked IPs
-
-```bash
-curl http://localhost:8080/api/ips
-```
-
----
-
-## View Auth Failure Stats
-
-```bash
-curl http://localhost:8080/api/auth-failures
-```
-
----
-
-## Manually Rotate API Key
-
-```bash
-curl -X POST \
- http://localhost:8080/api/rotate-key
-```
-
-The response includes the **new key** — store it immediately as the old key is invalidated.
-
----
-
## Global Options
-> **Note:** Global options are scoped to the current workspace. Switching workspaces loads that workspace's own set of options.
-
```bash
# Set global options
curl -X POST http://localhost:8080/api/options \
@@ -258,8 +202,6 @@ curl http://localhost:8080/api/options \
## Credential Store
-> **Note:** Credentials are scoped to the current workspace. Switching workspaces loads that workspace's own credential store.
-
```bash
# Add a credential
curl -X POST http://localhost:8080/api/creds \
@@ -493,7 +435,8 @@ curl http://localhost:8080/health
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Port scan
-curl -X POST -H "Content-Type: application/json" \
+curl -X POST -H "Authorization: Bearer my-secret-key" \
+ -H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
@@ -503,56 +446,3 @@ curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
-
----
-
-## Multi-Target Examples
-
-The API supports multiple target formats: single IP, CIDR subnets, comma-separated lists, and hostname resolution.
-
-```bash
-# Single IP
-curl -X POST http://localhost:8080/api/run \
- -H "Authorization: Bearer my-secret-key" \
- -H "Content-Type: application/json" \
- -d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}'
-
-# CIDR subnet
-curl -X POST http://localhost:8080/api/run \
- -H "Authorization: Bearer my-secret-key" \
- -H "Content-Type: application/json" \
- -d '{"module": "scanners/port_scanner", "target": "192.168.1.0/24"}'
-
-# Comma-separated list
-curl -X POST http://localhost:8080/api/run \
- -H "Authorization: Bearer my-secret-key" \
- -H "Content-Type: application/json" \
- -d '{"module": "scanners/port_scanner", "target": "10.0.0.1,10.0.0.2,10.0.0.3"}'
-
-# Hostname (resolved via DNS)
-curl -X POST http://localhost:8080/api/run \
- -H "Authorization: Bearer my-secret-key" \
- -H "Content-Type: application/json" \
- -d '{"module": "exploits/heartbleed", "target": "vulnerable.example.com"}'
-```
-
----
-
-## MCP Integration
-
-The MCP (Model Context Protocol) server runs over stdio with JSON-RPC 2.0 transport. It is designed for integration with Claude Desktop and other MCP-compatible clients.
-
-```bash
-# Start the MCP server
-cargo run -- --mcp
-```
-
-MCP tools can be invoked by any MCP-compatible client. Example tool calls (JSON-RPC 2.0 format):
-
-```json
-{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "list_modules", "arguments": {"category": "exploits"}}}
-{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "run_module", "arguments": {"module_path": "scanners/port_scanner", "target": "192.168.1.1"}}}
-{"jsonrpc": "2.0", "id": 3, "method": "resources/read", "params": {"uri": "rustsploit:///status"}}
-```
-
-See [MCP Integration](MCP-Integration.md) for the full tool and resource reference.
diff --git a/docs/CLI-Reference.md b/docs/CLI-Reference.md
index 80760ce..11e54d9 100644
--- a/docs/CLI-Reference.md
+++ b/docs/CLI-Reference.md
@@ -36,7 +36,8 @@ An optional positional argument (`exploit`, `scanner`, `creds`) can be used to s
| `--list-modules` | | Print all available modules and exit |
| `--verbose` | `-v` | Enable detailed logging |
| `--output-format` | | Control output: `text` (default) or `json` |
-| `--api` | | Start the PQ-encrypted REST API server |
+| `--api` | | Start the PQ-encrypted REST + WebSocket API server |
+| `--mcp` | | Start as MCP (Model Context Protocol) server on stdio |
| `--interface ` | | Bind address for API server (default: `127.0.0.1:8080`) |
| `--pq-host-key ` | | PQ host key file (default: `~/.rustsploit/pq_host_key`) |
| `--pq-authorized-keys ` | | Authorized client keys file (default: `~/.rustsploit/pq_authorized_keys`) |
diff --git a/docs/Changelog.md b/docs/Changelog.md
index 0bc7ea1..8411886 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -4,87 +4,70 @@ A high-level summary of significant changes. For the full detailed log, see [`ch
---
-## v0.5.0 — Security & Isolation Hardening (2026-04-09)
-
-See [changelog-latest.md](../changelogs/changelog-latest.md) for full details.
-
-**Highlights:**
-- 10 security fixes in ArcticAlopex frontend (API key hashing, missing ACL checks, TOTP encryption, per-account lockout, credential redaction, WebSocket hardening)
-- Per-workspace isolation for credentials and options (each workspace gets its own stores)
-- Per-user credential ownership and preferences in ArcticAlopex GUI (each user sees only their own creds)
-- Audit/activity log export to CSV and JSON files
-- Persistent file logging with daily rotation (`~/.rustsploit/logs/`)
-- Dashboard health check fixes and activity/audit UI stability improvements
-
----
-
-## v0.4.9 (2026-04-08)
-
-### Security Hardening (30+ fixes)
-- **SSRF**: Rewrote `is_blocked_target()` with IP parsing — blocks IPv4-mapped IPv6, Azure wireserver, full link-local range. Added to `honeypot_check`, shell `run_all`, and `run_module`.
-- **RBAC**: Added permission checks to user modification (`users.update`/`users.delete`), role permissions (`roles.manage`), and export endpoint (`export.data`).
-- **Auth**: Fixed X-Forwarded-For rate-limit bypass (use second-to-last IP, not leftmost). Fixed OOM via rate limiter map leak. Added hard cap (50K entries) against IP spray attacks.
-- **SQL Injection**: Added `escapeLike()` to all LIKE queries in audit/activity routes.
-- **TOCTOU**: File retrieval uses `O_NOFOLLOW` + fd-based path resolution. Spool and export files use `O_NOFOLLOW` to prevent symlink attacks.
-- **Crypto**: PQ nonces derived from counter (not random). Epoch monotonicity check prevents replay.
-- **Input validation**: CIDR /0 DoS prevention (min /16 IPv4, /48 IPv6). Credential add rejects port=0 and empty secret. Target validator rejects XSS payloads. ACL whitespace-padding bypass fixed.
-- **Error handling**: Replaced 12 swallowed `.catch(() => {})` blocks with toast notifications.
-- **Data integrity**: Loot and credential store rollback on disk write failure.
-- **TOTP**: Raw secret no longer returned in API response.
-
-### ArcticAlopex Features
-- **First-time setup flow**: One-time 6-digit PIN generated by stack launcher, `/setup` page creates owner account and tenant, permanently locked after use.
-- **CIDR subnet execution**: Modules page auto-detects CIDR targets and uses `run_all` for concurrent scanning.
-- **Host notes UI**: Expandable host rows in Workspaces page with note viewing and adding.
-- **Honeypot detection UI**: Integrated into Targets page with port scan results.
-- **Console spool UI**: Start/stop spool logging from Settings page.
-- **Bulk clear operations**: Credentials, loot, and hosts clearing with confirmation dialogs.
-- **RSF connection editor**: Endpoint URL is now editable in Settings (was read-only).
-- **RSF error propagation**: Backend 401/500 errors now surface to the frontend instead of silent success.
-
-### Tools
-- `arcticalopex/tools/stack.py` — Unified stack manager (merged from `start_stack.py` + `setup_guide.py`). Subcommands: `launch`, `setup`, `stop`, `check`, `troubleshoot`, `commands`, `config`.
-
-### Bug Fixes
-- Audit page React crash: API now joins users table, stringifies jsonb fields, maps field names correctly.
-- Activity feed empty rows: Same field-mapping fix.
-- Tenant parsing on localhost: Handles bare hostnames without dots.
-- Auth error handler: Distinguishes SyntaxError (400) from DB errors (503).
-- Credentials CSV export: Fixed variable ordering (filtered defined before exportCSV).
-- BullMQ worker crash: Added `getBullMQConnection()` with `maxRetriesPerRequest: null`.
-- Reports URL validation: Protocol check prevents data:/javascript: XSS.
-
----
-
-## v0.4.8 (2026-04-03)
+## v0.4.8 (2026-04-19)
### Module Totals
-- **137 exploit modules** (24 with `check()`) — cameras, routers, network infrastructure, webapps, frameworks, SSH, DoS, crypto, FTP, IPMI, telnet, Bluetooth, VoIP, Windows, payload generators
-- **24 scanner modules**
-- **28 credential modules** — all with full mass scan support (random, CIDR, file, comma-separated targets)
+- **183 exploit modules** — cameras, routers, network infrastructure, webapps, frameworks, SSH, VNC, DoS, crypto, FTP, IPMI, telnet, Bluetooth, VoIP, Windows, payload generators, honeypot exploits (Cowrie, Dionaea, HoneyTrap, SNARE), WAF (SafeLine)
+- **27 scanner modules**
+- **29 credential modules** — all with full mass scan support (random, CIDR, file, comma-separated targets)
- **1 plugin module**
-- **190 total modules**
+- **240 total modules**
+
+### New in April 2026
+
+#### 46 New Exploit Modules
+
+| Category | Modules |
+|----------|---------|
+| Cowrie (SSH honeypot) | `ansi_log_injection`, `llm_prompt_injection`, `ssrf_ipv6` |
+| Dionaea (honeypot) | `mqtt_underflow`, `mssql_dos`, `mysql_sqli`, `tftp_crash` |
+| HoneyTrap (honeypot) | `docker_panic`, `ftp_panic` |
+| SafeLine (WAF) | `cookie_attributes`, `nginx_injection`, `no_auth_probe`, `pre_auth_tfa`, `session_secret_entropy`, `unauth_writes` |
+| Snare (honeypot) | `cookie_dos`, `tanner_version_mitm` |
+| VNC | `rfb`, `libvnc_checkrect_overflow`, `libvnc_tight_filtergradient`, `libvnc_ultrazip`, `libvnc_websocket_overflow`, `libvnc_zrle_tile`, `tigervnc_rre_overflow`, `tigervnc_timing_oracle`, `tightvnc_decompression_bomb`, `tightvnc_des_hardcoded_key`, `tightvnc_ft_path_traversal`, `tightvnc_predictable_challenge`, `tightvnc_rect_overflow`, `x11vnc_dns_injection`, `x11vnc_env_injection`, `x11vnc_unixpw_inject` |
+| SSH | `asyncssh_beginauthpass`, `libssh2_rogue_server`, `paramiko_authnonepass`, `paramiko_unknown_method` |
+| Frameworks | `apache_camel/cve_2025_27636_camel_header_injection`, `php/cve_2025_51373_php_rce` |
+| Network Infra | `commvault/cve_2025_34028_commvault_rce`, `kubernetes/cve_2025_1974_ingress_nginx_rce` |
+| WebApps | `misp_rce_cve_2025_27364`, `nextjs_middleware_bypass_cve_2025_29927`, `vite_path_traversal_cve_2025_30208`, `zimbra_sqli_auth_bypass_cve_2025_25064` |
+
+#### 3 New Scanner Modules
+
+- `proxy_scanner` — HTTP CONNECT, SOCKS4/5, transparent proxy discovery
+- `reflect_scanner` — UDP amplification vulnerability scanner (DNS, NTP, SSDP, Memcached)
+- `vuln_checker` — Fingerprint-based vulnerability scanner across all exploit modules
+
+#### 10 New Credential Modules
+
+`couchdb_bruteforce`, `elasticsearch_bruteforce`, `http_basic_bruteforce`, `imap_bruteforce`, `memcached_bruteforce`, `mysql_bruteforce`, `postgres_bruteforce`, `proxy_bruteforce`, `redis_bruteforce`, `vnc_bruteforce`
+
+#### Infrastructure
+
+- **WebSocket transport** (`src/ws.rs`) — PQ-encrypted WebSocket endpoint at `/pq/ws` with 100-connection cap and heartbeat
+- **Root privilege helper** (`src/utils/privilege.rs`) — `require_root()` for raw-socket modules (DoS, ping sweep, ICMP)
+- **Unified HTTP client** (`src/utils/network.rs`) — `build_http_client()` and `build_http_client_with(HttpClientOpts)` replacing hand-rolled clients in 50+ modules
+- **TCP connect helpers** — `tcp_connect_addr()`, `tcp_connect_str()`, `blocking_tcp_connect()`, `udp_bind()` centralizing socket creation
+- **MCP hardening** — `isolate_protocol_stdout()` prevents module println! from corrupting JSON-RPC; `MAX_LINE_BYTES` (1 MiB) caps; binary-safe reads
+- **Spool hardening** — `O_NOFOLLOW` flag, parent symlink check, lock-first file creation, `write_line()` returns Result
+- **build.rs** — `check_available()` dispatch for capability queries without a target; optimized regex compilation
+
+#### Module Audit
+
+Systematic quality pass across all 183 exploit modules:
+- Replaced `std::thread::sleep` with async alternatives in SSH and scanner modules
+- Migrated raw `TcpStream::connect` to `tcp_connect_addr()` framework utility
+- Standardized 50+ modules from hand-rolled `reqwest::Client::builder` to `build_http_client()`
+- Added `require_root()` checks to all raw-socket modules (DoS, ping sweep, ICMP flood)
+- Added `zeroize` crate for sensitive data cleanup
+
+---
### Highlights
-- **20 new exploit modules** — XWiki RCE, Dify default creds, SolarWinds WHD, MCPJam RCE, Langflow RCE, SAP NetWeaver (CVSS 10.0), SharePoint ToolPane, Craft CMS x2, Laravel Livewire, CitrixBleed 2, HPE OneView (CVSS 10.0), F5 BIG-IP, SonicWall SMA, Ivanti ICS x2, Tomcat PUT, WSUS, Erlang SSH (CVSS 10.0), FreePBX
-- **8 new scanners** — ssl_scanner, service_scanner, redis_scanner, vnc_scanner, snmp_scanner, waf_detector, subdomain_scanner, nbns_scanner
-- **9 new credential modules** — vnc, imap, mysql, postgres, redis, elasticsearch, couchdb, memcached, http_basic bruteforce
-- **MCP integration** — Model Context Protocol server with 42 tools and 7 resources for Claude Desktop and external tool connectivity
-- **Payload mutation engine** — 9 WAF bypass strategies (URL encode, case toggle, comment injection, unicode homoglyph, etc.) in `src/native/payload_engine.rs`
-- **Native RDP bruteforce** — TCP+TLS+CredSSP/NTLM authentication, no xfreerdp/rdesktop dependency
-- **Mass scan engine unification** — shared `run_mass_scan()` engine replaced ~900 lines of duplicated code across 13 cred modules
-- **Bruteforce uplift** — ETA countdown, exponential backoff with jitter, account lockout detection across all 10 active bruteforce modules
-- **Output system rewrite** — task-local `mprintln!` macros replace 4683 println! calls, enabling concurrent API module execution
-- **Async prompts** — all cfg_prompt_* functions now async via `spawn_blocking`, freeing the tokio runtime during stdin reads
-- **Source port binding** — `set source_port` for firewall bypass testing across TCP, UDP, and SSH connections
-- **Streaming wordlists** — 100GB+ password files processed with constant memory via chunked streaming
-- **WPair BLE exploit** — 51 device database, 6 exploit strategies, TUI mode, headless API mode
-- **Framework-level multi-target dispatcher** — comma-separated, CIDR, file-based, and random target modes work for ALL 190 modules
+- **Framework-level multi-target dispatcher** — comma-separated, CIDR, file-based, and random target modes now work for ALL modules, handled by the framework rather than individual module code
- **All modules use `cfg_prompt_*`** — ensures full API/CLI/MCP compatibility via the priority chain (custom_prompts > global_options > stdin)
- **Honeypot detection system** — warns operators when a target exhibits honeypot characteristics
-- **4-round security audit** — 32+ bug fixes across 22 files (path traversal, SSRF, race conditions, silent save failures, input validation)
+- **`#[cfg(unix)]` guards** on Unix-specific permissions code for cross-platform compilation
- **Bug fixes:**
- SharePoint exploit: fixed header typo
- Langflow exploit: corrected escape order
@@ -92,17 +75,6 @@ See [changelog-latest.md](../changelogs/changelog-latest.md) for full details.
- Jenkins LFI: fixed async deadlock
- Apache Tomcat: replaced hardcoded session IDs with proper generation
-### Performance & Infrastructure (2026-04-07)
-
-- **Dependency cleanup** — removed 5 redundant crates (aes-gcm, subtle, hyper, hyper-util, futures-util), migrated once_cell to std::sync::LazyLock (10 files), updated sha1/sha2/hkdf to latest
-- **Compile performance** — trimmed tokio from "full" to 10 specific features, removed socket2 "all", optimized build.rs regex (compile once, pre-filter before regex match)
-- **Connection speed** — cached reqwest::Client pool (72+ modules benefit), cached TLS connector singleton, zero-alloc tcp_connect_addr() eliminating 65K String allocs per full port scan
-- **Source port enforcement** — 63 raw TcpStream::connect and 3 UdpSocket::bind bypasses fixed across 44 files; all 190 modules now respect `setg source_port`
-- **Bruteforce engine** — fixed Redis command injection (RESP format), SMTP 2s to 10s timeout, FTP 421 retry, HTTP redirect false positives; added ComboMode::Spray, jitter_ms, credential file loading, HTTP client reuse
-- **Spool compliance** — converted 500+ raw println!/eprintln! to crate::mprintln!/crate::meprintln! across all modules and infrastructure
-- **Zero warnings** — eliminated all compiler warnings, dead code, unused imports
-- **DoS spoofing infrastructure** — unified FastRng, checksum, gen_ipv4_public into src/native/dos_utils.rs; global `setg spoof_ip true` support; ~300 lines of duplicated code removed from 8 modules
-
---
## Recent Changes
diff --git a/docs/Contributing.md b/docs/Contributing.md
index 84933ae..0b04496 100644
--- a/docs/Contributing.md
+++ b/docs/Contributing.md
@@ -27,8 +27,6 @@ Contributions are welcome — bug reports, new modules, framework improvements,
| Scanner | `src/modules/scanners/` |
| Credential | `src/modules/creds/generic/` or `creds//` |
| Plugin | `src/modules/plugins/` |
-| MCP tool | `src/mcp/tools.rs` |
-| Payload mutation strategy | `src/native/payload_engine.rs` |
Use subfolders for vendor families (e.g., `exploits/cisco/`, `exploits/cameras/`).
diff --git a/docs/Credential-Modules-Guide.md b/docs/Credential-Modules-Guide.md
index 6920e25..1ee7d87 100644
--- a/docs/Credential-Modules-Guide.md
+++ b/docs/Credential-Modules-Guide.md
@@ -41,54 +41,12 @@ All bruteforce modules use the shared engine (`crate::modules::creds::utils`):
| `run_bruteforce()` | Single-target credential testing with concurrency, progress, retry |
| `run_subnet_bruteforce()` | CIDR subnet scanning with per-host credential testing |
| `run_mass_scan()` | Random/file/CIDR mass scanning with lightweight probes |
-| `generate_combos_mode()` | Generate user/password pairs (linear, combo, or spray mode) |
-| `load_credential_file()` | Load user:pass pairs from a colon-separated file |
+| `generate_combos()` | Generate user/password pairs (combo or linear mode) |
Avoid custom concurrency — always use the engine which handles semaphores, progress reporting, lockout detection, and credential storage.
---
-## Combo Modes
-
-All bruteforce modules support three credential combination strategies via `ComboMode`:
-
-| Mode | Ordering | Use Case |
-|------|----------|----------|
-| `Linear` | Pair user[i] with pass[i], cycling shorter list | Paired credentials from a breach dump |
-| `Combo` | Full cross product (every user x every password) | Standard bruteforce |
-| `Spray` | For each password, try all users before next password | Active Directory lockout avoidance |
-
-Modules prompt: `Combo mode (linear/combo/spray) [combo]:`
-
----
-
-## Credential File Support
-
-Modules can load `user:pass` pairs directly from a file (one pair per line, colon-separated):
-
-```rust
-use crate::modules::creds::utils::load_credential_file;
-let extra = load_credential_file("creds.txt")?;
-combos.extend(extra);
-```
-
----
-
-## Jitter Support
-
-`BruteforceConfig.jitter_ms` adds random delay (0..jitter_ms) between attempts to evade IDS pattern detection. Default is 0 (disabled). Also supported in `SubnetScanConfig`.
-
----
-
-## Protocol Safety
-
-- **Redis** — all commands use RESP array format (length-prefixed) to prevent \r\n injection
-- **HTTP** — redirect responses are inspected for login/auth/signin/sso paths to prevent false positives
-- **FTP** — 421 (connection limit) responses are classified as retryable errors with backoff
-- **SMTP** — connection timeout is 10 seconds (configurable), not the previous 2 seconds
-
----
-
## IPv6 Support
Use `format_addr` to wrap IPv6 addresses in brackets and handle port suffixes:
@@ -126,8 +84,6 @@ Accept invalid certificates for offensive tooling convenience (e.g., `danger_acc
## Result Persistence
-> **Note:** Discovered credentials are stored per-workspace. When you switch workspaces (`workspace `), the credential store is scoped to that workspace. This keeps engagement data isolated between different assessments.
-
Offer to write `host -> user:pass` pairs to a local file (default `./results.txt`):
```rust
@@ -221,54 +177,3 @@ All 28 credential modules support mass scanning via the framework's multi-target
- **Comma-separated targets** — splits and runs against each target
Modules use `is_mass_scan_target()` to detect mass-scan mode and `run_mass_scan()` to delegate to the framework dispatcher. This is handled at the framework level, so individual modules do not need custom mass-scan loops.
-
----
-
-### Mass Scan Engine
-
-The mass scan engine (`crate::modules::creds::utils::run_mass_scan`) provides a high-performance framework for internet-wide credential testing. It handles:
-
-- **Random IP generation** with `EXCLUDED_RANGES` enforcement (bogons, private, reserved, documentation, and public DNS ranges)
-- **Persistent state tracking** via `is_ip_checked()` / `mark_ip_checked()` to resume interrupted scans
-- **Configurable concurrency** with semaphore-based worker pools (default 500 workers for mass scan)
-- **Graceful shutdown** on Ctrl+C with state persistence
-
-Modules call `run_mass_scan()` with a closure that performs the per-host probe. The engine manages IP generation, deduplication, and state. See `telnet_hose` and `camxploit` for reference implementations.
-
----
-
-### ETA, Backoff, and Lockout Detection
-
-Credential modules should implement lockout-aware brute forcing to avoid triggering account lockout policies:
-
-- **Exponential backoff** -- When repeated authentication failures are detected from a single target, increase delay between attempts. The bruteforce engine tracks consecutive failures per host.
-- **Lockout detection** -- Monitor for protocol-specific lockout indicators:
- - SSH: `Too many authentication failures` or connection refused after N attempts
- - RDP: `ERR_CONNECT_LOGON_TYPE_NOT_GRANTED` or `ERRCONNECT_ACCOUNT_LOCKED_OUT`
- - HTTP: `429 Too Many Requests` or `403 Forbidden` after prior successes
- - SMTP: `421` temporary rejection
-- **ETA calculation** -- `BruteforceStats` tracks attempts/second and remaining combinations. Call `stats.print_progress()` for inline ETA display.
-- **Per-host cooldown** -- When lockout is detected, pause attempts against that host for a configurable duration (default 300 seconds) while continuing against other targets in subnet/mass-scan mode.
-
----
-
-### Streaming Wordlists
-
-For large wordlist files (>150 MB), credential modules should use streaming mode instead of loading the entire file into memory. The streaming approach reads and processes entries in chunks:
-
-```rust
-use tokio::io::{AsyncBufReadExt, BufReader};
-use tokio::fs::File;
-
-let file = File::open(&wordlist_path).await?;
-let reader = BufReader::new(file);
-let mut lines = reader.lines();
-
-while let Some(line) = lines.next_line().await? {
- let entry = line.trim().to_string();
- if entry.is_empty() { continue; }
- // ... process entry ...
-}
-```
-
-The RDP bruteforce module (`rdp_bruteforce`) demonstrates this pattern with chunked password processing. When wordlists exceed the streaming threshold, the module reads passwords in batches of 10,000 rather than loading all entries at once. This keeps memory usage constant regardless of wordlist size.
diff --git a/docs/Credits.md b/docs/Credits.md
index 7b0b43a..97a2fd0 100644
--- a/docs/Credits.md
+++ b/docs/Credits.md
@@ -49,13 +49,6 @@
| `des` / `aes` / `cipher` | Cryptographic primitives |
| `chrono` | Date/time handling |
| `uuid` | Unique identifier generation |
-| `ratatui` | Terminal UI framework (WPair TUI) |
-| `crossterm` | Terminal backend for ratatui |
-| `btleplug` | Bluetooth Low Energy (WPair BLE) |
-| `rlimit` | Safe resource limit management |
-| `tracing` | Structured logging framework |
-| `num_cpus` | CPU core detection |
-| `gag` | Stdout capture (deprecated, replaced by mprintln!) |
---
diff --git a/docs/Exploit-Modules-Guide.md b/docs/Exploit-Modules-Guide.md
index 1a65ba0..1aa9053 100644
--- a/docs/Exploit-Modules-Guide.md
+++ b/docs/Exploit-Modules-Guide.md
@@ -133,88 +133,3 @@ All exploit modules automatically benefit from the framework's multi-target disp
- **Random targets** — `random` or `0.0.0.0/0` generates random public IPs with `EXCLUDED_RANGES` enforcement
The dispatcher calls the module's `run()` function once per resolved target. Modules only need to handle a single target string.
-
----
-
-### Payload Mutation Engine
-
-The payload mutation engine (`src/native/payload_engine.rs`) is available to all exploit modules for encoding and obfuscating payloads before delivery. This is useful for bypassing signature-based detection (AV, IDS/IPS, WAF).
-
-**Available transformations:**
-
-| Encoding | Description |
-|----------|-------------|
-| XOR | Single-byte or multi-byte XOR with configurable key |
-| Base64 | Standard and URL-safe Base64 encoding |
-| Hex | Hexadecimal string encoding |
-| Zero-width | Unicode zero-width character steganography |
-| Custom | User-defined alphabet substitution |
-
-**Integration example:**
-
-```rust
-use crate::native::payload_engine::{encode_payload, EncodingType};
-
-// XOR-encode a command payload
-let payload = b"id; cat /etc/passwd";
-let encoded = encode_payload(payload, EncodingType::Xor { key: 0x42 })?;
-
-// Chain multiple encodings
-let double_encoded = encode_payload(&encoded, EncodingType::Base64)?;
-```
-
-Modules that generate payloads (`narutto_dropper`, `polymorph_dropper`, `payload_encoder`, `batgen`, `lnkgen`) use the mutation engine internally. New exploit modules that deliver payloads should prefer using the engine over inline encoding.
-
----
-
-### WPair BLE Module
-
-The `exploits/bluetooth/wpair` module exploits a flaw in the Google Fast Pair protocol to hijack Bluetooth accessories. This is the framework's first Bluetooth Low Energy (BLE) module.
-
-**Attack flow:**
-
-1. **Discovery** -- Scans for BLE advertisements matching Fast Pair service UUIDs
-2. **Bonding hijack** -- Initiates pairing before the legitimate device by responding faster to the Fast Pair provider
-3. **Account key injection** -- Writes a crafted Account Key to the device's Fast Pair service, associating it with the attacker's Google account
-4. **Audio interception** -- Once bonded, audio streams (A2DP/HFP) route through the attacker's device
-
-**Requirements:**
-
-- Linux with BlueZ 5.50+
-- Bluetooth adapter supporting BLE (most USB adapters work)
-- Root privileges for raw HCI access
-
-**Usage notes:**
-
-- The module requires physical proximity to the target Bluetooth accessory (typically <10 meters)
-- Success depends on winning the race condition against the legitimate pairing device
-- Some accessories implement additional pairing verification that may prevent the attack
-
----
-
-## DoS Module Development
-
-### Shared Infrastructure
-
-All raw-packet DoS modules use shared utilities from `crate::native::dos_utils`:
-
-| Utility | Purpose |
-|---------|---------|
-| `FastRng::with_thread_seed(id)` | Per-thread XorShift128+ PRNG |
-| `FastRng::gen_ipv4_public()` | Random public IPv4 (avoids private/reserved/multicast) |
-| `FastRng::gen_ephemeral_port()` | Random port 49152-65535 |
-| `checksum_16(data)` | RFC 1071 Internet checksum |
-| `sum_16(data, init)` | Partial checksum accumulator |
-| `is_spoof_enabled()` | Check `setg spoof_ip true` global option |
-
-### Source IP Spoofing
-
-**Raw-packet modules** (SYN flood, UDP flood, ICMP flood, amplification attacks) support source IP spoofing via `socket2::Type::RAW` with `IP_HDRINCL`. Enable globally:
-
-```
-setg spoof_ip true
-```
-
-Modules with user prompts (null_syn_exhaustion, udp_flood, icmp_flood) use the global as the default value. Amplification modules (DNS, NTP, SSDP, Memcached) always spoof the victim IP as source.
-
-**TCP-based modules** (slowloris, RUDY, HTTP flood, connection exhaustion) cannot spoof — TCP requires a valid 3-way handshake.
diff --git a/docs/Future-Features.md b/docs/Future-Features.md
index 9155132..f62c7e9 100644
--- a/docs/Future-Features.md
+++ b/docs/Future-Features.md
@@ -20,21 +20,6 @@ The following Metasploit-inspired features have been implemented:
- **Background Jobs** (`run -j`/`jobs`) — Async module execution
- **Export/Reporting** (`export`) — JSON, CSV, and summary reports
-### MCP Integration
-Model Context Protocol server with 30 tools and 7 resources for programmatic access from Claude Desktop and other MCP-compatible clients. Supports module execution, target management, credential/host/loot tracking, and data export via JSON-RPC 2.0 over stdio.
-
-### Payload Mutation Engine
-Dynamic payload mutation with 9 WAF bypass strategies (URL encode, double URL, case toggle, comment injection, whitespace swap, concat/split, null byte, unicode homoglyph, boundary wrap) in `src/native/payload_engine.rs`.
-
-### Native RDP Authentication
-Pure Rust TCP+TLS+CredSSP/NTLM brute force — no xfreerdp or rdesktop dependency. 10-50x faster than CLI process spawning.
-
-### Mass Scan Engine Unification
-Shared `run_mass_scan()` engine with `MassScanConfig` replaced ~900 lines of duplicated mass scan code across 13 credential modules. Single source of truth for exclusion ranges, state tracking, and progress reporting.
-
-### Source Port Binding
-Framework-level `set source_port` command for firewall bypass testing. Applied to TCP, UDP, and SSH connections via `tcp_connect()`, `udp_bind()`, and `blocking_tcp_connect()` helpers.
-
---
## Planned Features
@@ -44,22 +29,27 @@ Currently, modules are configured interactively or via API JSON payloads. We pla
- **Goal:** Allow users to save their favorite scan parameters (wordlists, threads, timeouts) to a `.toml` or `.yaml` file and load them instantly.
- **Usage Idea:** `run exploits/tomcat_rce --config profiles/aggressive.toml` or `set config profiles/aggressive.toml` in the shell.
-### 2. Session/Handler Management
+### 2. Dynamic Source Port Modification
+While we currently support advanced networking like IP spoofing in specific flood modules, we plan to bring dynamic source port control to the framework level.
+- **Goal:** Allow scanners and exploit modules to bind to specific source ports (e.g., source port 53) to bypass poorly configured firewalls that trust traffic originating from privileged ports.
+- **Implementation:** Extending the global configuration and socket helpers to accept an optional `bind_port` parameter.
+
+### 3. Session/Handler Management
Add Metasploit-style session management with reverse/bind shell handlers.
- **Goal:** Multi/handler listener, session listing/interaction, background sessions.
- **Implementation:** Listener framework with TCP/HTTP handlers, session tracking with numeric IDs.
-### 3. Post-Exploitation Modules
+### 4. Post-Exploitation Modules
Add a `post/` module category for post-exploitation tasks.
- **Goal:** Privilege escalation checks, persistence mechanisms, credential extraction, lateral movement.
- **Implementation:** New module category auto-discovered by build.rs.
-### 4. Network Pivoting
+### 5. Network Pivoting
Route traffic through compromised hosts.
- **Goal:** SOCKS proxy, port forwarding, autoroute through sessions.
- **Implementation:** Requires session management (Feature 3) first.
-### 5. Nmap Integration
+### 6. Nmap Integration
Import scan results directly into the workspace.
- **Goal:** `db_import` command for Nmap XML, populate hosts/services automatically.
- **Implementation:** Parse Nmap XML output and feed into workspace.
diff --git a/docs/Getting-Started.md b/docs/Getting-Started.md
index ddc9824..794db19 100644
--- a/docs/Getting-Started.md
+++ b/docs/Getting-Started.md
@@ -1,6 +1,6 @@
# Getting Started
-Rustsploit is a modular offensive tooling framework for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. It ships an interactive shell, a CLI runner, a REST API server, and an ever-growing library of exploits, scanners, and credential modules.
+Rustsploit is a modular offensive tooling framework for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. It ships an interactive shell, a CLI runner, a WebSocket API server with post-quantum encryption, and an ever-growing library of exploits, scanners, and credential modules.
---
@@ -10,23 +10,22 @@ Rustsploit is a modular offensive tooling framework for embedded targets, writte
**Debian / Ubuntu / Kali:**
```bash
-sudo apt update
-sudo apt install pkg-config libssl-dev rustc libdbus-1-dev
+sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake
```
**Arch Linux:**
```bash
-sudo pacman -S pkgconf openssl freerdp rustc
+sudo pacman -S base-devel pkgconf openssl dbus cmake
```
**Gentoo:**
```bash
-sudo emerge dev-libs/openssl dev-util/pkgconf net-misc/freerdp
+sudo emerge dev-libs/openssl dev-util/pkgconf sys-apps/dbus dev-build/cmake
```
**Fedora / RHEL:**
```bash
-sudo dnf install pkgconf-pkg-config openssl-devel freerdp rustc
+sudo dnf install gcc make pkgconf-pkg-config openssl-devel dbus-devel cmake
```
### Rust & Cargo
@@ -36,7 +35,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
-> The minimum supported Rust version tracks stable. Run `rustup update` to stay current.
+> Rust 1.85+ is required (edition 2024). Run `rustup update` to stay current.
---
@@ -77,27 +76,6 @@ cargo run -- --api
This starts the PQ-encrypted API server on port 8080. On first run it generates a host key pair at `~/.rustsploit/pq_host_key` and prints its fingerprint. Clients must be listed in `~/.rustsploit/pq_authorized_keys` to connect. No TLS or API keys — authentication uses SSH-style post-quantum identity keys. See [API Server](API-Server.md) and [API Usage Examples](API-Usage-Examples.md) for details.
-### MCP Integration
-```bash
-cargo run -- --mcp
-```
-
-This starts the MCP (Model Context Protocol) server over stdio using JSON-RPC 2.0 transport. It exposes 30 tools and 7 resources for integration with Claude Desktop and other MCP-compatible clients. No network listener is opened — communication is over stdin/stdout.
-
-To use with Claude Desktop, add to your `claude_desktop_config.json`:
-```json
-{
- "mcpServers": {
- "rustsploit": {
- "command": "/path/to/rustsploit",
- "args": ["--mcp"]
- }
- }
-}
-```
-
-See [MCP Integration](MCP-Integration.md) for the full tool and resource reference.
-
---
## Docker Deployment
@@ -150,53 +128,6 @@ docker compose -f docker-compose.rustsploit.yml up -d --build
---
-## ArcticAlopex Web GUI
-
-ArcticAlopex is a multi-tenant web frontend for Rustsploit with RBAC, audit logging, and PQ-encrypted transport.
-
-### Prerequisites
-
-- Node.js 20+
-- Docker & Docker Compose (for PostgreSQL, Redis, MinIO)
-
-### Setup
-
-```bash
-cd arcticalopex
-
-# Start infrastructure
-docker compose up -d postgres redis minio
-
-# Configure
-cp .env.example .env
-sed -i "s/^MASTER_KEY=.*/MASTER_KEY=$(openssl rand -hex 32)/" .env
-
-# Install and initialize
-npm install
-npm run db:push
-
-# Start everything (from arcticalopex/)
-python3 tools/stack.py launch --docker
-```
-
-On first run the terminal prints a **6-digit setup PIN**. Open `http://localhost:3000/setup` to create the owner account.
-
-### Stack Manager
-
-| Command | Description |
-|---------|-------------|
-| `python3 tools/stack.py launch --docker` | Start all services with Docker infra |
-| `python3 tools/stack.py setup` | Interactive 7-step setup guide |
-| `python3 tools/stack.py stop` | Stop all running services |
-| `python3 tools/stack.py check` | Verify configuration only |
-| `python3 tools/stack.py troubleshoot` | Diagnose common issues |
-| `python3 tools/stack.py commands` | Print all commands (copy-paste) |
-| `python3 tools/stack.py config` | Create `.env` interactively |
-
-See [arcticalopex/README.md](../arcticalopex/README.md) for full documentation.
-
----
-
## Privacy / VPN
The built-in proxy system has been removed in favor of system-level VPN solutions.
@@ -211,24 +142,4 @@ Connect the VPN on your host before running Rustsploit and all traffic routes th
---
-## Data Storage
-
-All persistent data is stored under `~/.rustsploit/`. Key paths:
-
-| Path | Description |
-|------|-------------|
-| `~/.rustsploit/workspaces/{name}.json` | Per-workspace hosts and services |
-| `~/.rustsploit/workspaces/{name}_creds.json` | Per-workspace credential store |
-| `~/.rustsploit/workspaces/{name}_options.json` | Per-workspace global options (`setg` values) |
-| `~/.rustsploit/workspaces/{name}_loot.json` | Per-workspace loot entries |
-| `~/.rustsploit/logs/` | Framework log files |
-| `~/.rustsploit/results/` | Saved module output and scan results |
-| `~/.rustsploit/pq_host_key` | API server post-quantum host key pair |
-| `~/.rustsploit/pq_authorized_keys` | Authorized client public keys |
-| `~/.rustsploit/startup.rc` | Auto-loaded resource script on shell startup |
-
-Each workspace isolates its own credentials, options, hosts, services, and loot. Switching workspaces (via `workspace `, `POST /api/workspace`, or the MCP `switch_workspace` tool) loads the target workspace's data automatically.
-
----
-
-> For authorized security testing and research only. Obtain explicit written permission before targeting any system you do not own.
+> ⚠️ For authorized security testing and research only. Obtain explicit written permission before targeting any system you do not own.
diff --git a/docs/Home.md b/docs/Home.md
index 31b6619..2dc6c77 100644
--- a/docs/Home.md
+++ b/docs/Home.md
@@ -13,19 +13,18 @@ Welcome to the Rustsploit documentation hub. Use the links below to navigate to
| [Getting Started](Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
-| [API Server](API-Server.md) | REST API startup, endpoints, auth, rate limiting, hardening |
+| [API Server](API-Server.md) | WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](API-Usage-Examples.md) | Practical curl workflows, request/response samples |
-| [Module Catalog](Module-Catalog.md) | All 190 modules by category — 137 exploits, 24 scanners, 28 creds, 1 plugin |
+| [Module Catalog](Module-Catalog.md) | All 240 modules by category — 183 exploits, 27 scanners, 29 creds, 1 plugin |
| [Module Development](Module-Development.md) | How to author new modules, lifecycle, dispatcher |
| [Security & Validation](Security-Validation.md) | Input validation constants, security patterns, honeypot detection |
-| [Credential Modules Guide](Credential-Modules-Guide.md) | Best practices for 28 cred modules — mass scan, cfg_prompt_*, concurrency |
-| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Best practices for 137 exploit modules — multi-target, cfg_prompt_*, validation |
+| [Credential Modules Guide](Credential-Modules-Guide.md) | Best practices for 29 cred modules — mass scan, cfg_prompt_*, concurrency |
+| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Best practices for 183 exploit modules — multi-target, cfg_prompt_*, validation |
| [Utilities & Helpers](Utilities-Helpers.md) | `utils.rs` public API, target normalization, honeypot check |
| [Testing & QA](Testing-QA.md) | Build checks (0 errors, 0 warnings), smoke tests, wordlist validation |
| [Changelog](Changelog.md) | Release notes and version history (current: v0.4.8) |
| [Contributing](Contributing.md) | Fork guide, PR checklist, code style |
| [Credits](Credits.md) | Authors, acknowledgements, legal notice |
-| [MCP Integration](MCP-Integration.md) | MCP server setup, 30 tools, Claude Desktop configuration |
| [Future Features](Future-Features.md) | Roadmap and completed features (plugins, metadata, global options, etc.) |
| [About Me](About-Me.md) | Information about the author |
| [Donation](Donation.md) | Ways to support the project |
diff --git a/docs/Interactive-Shell.md b/docs/Interactive-Shell.md
index 02e7163..fc7d261 100644
--- a/docs/Interactive-Shell.md
+++ b/docs/Interactive-Shell.md
@@ -137,7 +137,7 @@ rsf> show options
rsf> unsetg port
```
-Global options are now per-workspace. They are saved to `~/.rustsploit/workspaces/{name}_options.json` (where `{name}` is the current workspace name) and loaded automatically when switching workspaces.
+Global options are saved to `~/.rustsploit/global_options.json` and loaded on startup.
### Common Global Options
@@ -215,6 +215,6 @@ Key details from `src/shell.rs`:
- **`execute_single_command()`** — the command dispatcher, extracted as a standalone function for resource script support.
- **`split_command` / `resolve_command`** — normalize shortcut aliases to canonical keys.
- **`render_help()`** — prints the colorized command table.
-- **Selective persistence** — per-workspace options (`workspaces/{name}_options.json`), per-workspace credentials (`workspaces/{name}_creds.json`), workspace files, and loot are persisted across sessions in `~/.rustsploit/`. Transient shell state (selected module, current target, verbose flag) is reset on exit.
+- **Selective persistence** — `global_options.json`, `creds.json`, workspace files, and loot are persisted across sessions in `~/.rustsploit/`. Transient shell state (selected module, current target, verbose flag) is reset on exit.
Tab completion and command history are powered by `rustyline`.
diff --git a/docs/Module-Catalog.md b/docs/Module-Catalog.md
index 7a1a58e..ff989cd 100644
--- a/docs/Module-Catalog.md
+++ b/docs/Module-Catalog.md
@@ -4,7 +4,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
> **Module categories:** `exploits/`, `scanners/`, `creds/`, `plugins/` -- all auto-discovered at build time. Adding a new subdirectory under `src/modules/` automatically creates a new category.
-**Totals:** 137 exploit modules (24 with `check()`), 24 scanners, 28 credential modules, 1 plugin.
+**Totals:** 183 exploit modules, 27 scanners, 29 credential modules, 1 plugin.
---
@@ -27,6 +27,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/cameras/reolink/reolink_rce_cve_2019_11001` | Reolink camera authenticated OS command injection via TestEmail (CVE-2019-11001) |
| `exploits/cameras/uniview/uniview_nvr_pwd_disclosure` | Uniview NVR remote credential extraction and decoding |
+### Cowrie (SSH Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/cowrie/ansi_log_injection` | Injects ANSI/OSC escape sequences into cowrie session logs via unsanitized crontab arguments for terminal-level code execution on replay |
+| `exploits/cowrie/llm_prompt_injection` | Exploits cowrie LLM mode where attacker commands are concatenated into the system prompt, coercing the LLM to echo real configuration data |
+| `exploits/cowrie/ssrf_ipv6` | Bypasses cowrie SSRF blocklist via IPv6 addresses (fc00::/7, fe80::/10, ::ffff:0:0/96) and DNS-rebinding TOCTOU |
+
### Crypto
| Module Path | Description |
@@ -34,6 +42,15 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/crypto/geth_dos_cve_2026_22862` | Go-Ethereum ECIES panic DoS via malformed encrypted messages (CVE-2026-22862) |
| `exploits/crypto/heartbleed` | OpenSSL Heartbleed memory leak exploitation (CVE-2014-0160) |
+### Dionaea (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/dionaea/mqtt_underflow` | Malformed MQTT PUBLISH with TopicLength exceeding MessageLength triggers parser desync/UnicodeDecodeError in dionaea |
+| `exploits/dionaea/mssql_dos` | Crafted TDS7 LOGIN7 packet with misaligned password slice triggers unhandled UnicodeDecodeError in dionaea MSSQL handler |
+| `exploits/dionaea/mysql_sqli` | MySQL COM_FIELD_LIST with SQLite injection in table name leaks dionaea internal DB schema |
+| `exploits/dionaea/tftp_crash` | Malformed TFTP RRQ without trailing NUL causes struct.error in dionaea options parser |
+
### DoS / Stress Testing
| Module Path | Description |
@@ -50,12 +67,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/dos/ssdp_amplification` | Spoofed SSDP M-SEARCH requests for ~30x amplification |
| `exploits/dos/syn_ack_flood` | SYN packets to reflectors with spoofed victim source IP for SYN-ACK reflection |
| `exploits/dos/tcp_connection_flood` | High-concurrency TCP connection flood with optional RST close and HTTP payload |
+| `exploits/dos/telnet_iac_flood` | Telnet IAC negotiation flood exploiting unbounded SB/SE parsing and rapid WILL/DO option cycling |
| `exploits/dos/udp_flood` | High-speed UDP flood with random, null, and pattern payload modes |
### Frameworks
| Module Path | Description |
|-------------|-------------|
+| `exploits/frameworks/apache_camel/cve_2025_27636_camel_header_injection` | Apache Camel < 4.10.2 HTTP header injection via Simple expression language for OS command execution (CVE-2025-27636) |
| `exploits/frameworks/apache_tomcat/catkiller_cve_2025_31650` | Apache Tomcat memory leak via invalid HTTP/2 priority headers (CVE-2025-31650) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_apache_tomcat_rce` | Apache Tomcat deserialization RCE (CVE-2025-24813) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_tomcat_put_rce` | Apache Tomcat unauthenticated RCE via partial PUT and Java deserialization (CVE-2025-24813) |
@@ -67,6 +86,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/frameworks/mongo/mongobleed` | MongoDB zlib decompression heap memory disclosure (CVE-2025-14847) |
| `exploits/frameworks/nginx/nginx_pwner` | Nginx misconfiguration scanner: alias traversal, CRLF injection, PHP detection, and more |
| `exploits/frameworks/php/cve_2024_4577` | PHP CGI argument injection on Windows XAMPP for RCE (CVE-2024-4577) |
+| `exploits/frameworks/php/cve_2025_51373_php_rce` | PHP CGI on Windows soft hyphen code-page conversion allows argument injection for auto_prepend_file RCE (CVE-2025-51373) |
| `exploits/frameworks/wsus/cve_2025_59287_wsus_rce` | Unauthenticated RCE in Windows Server Update Services (CVE-2025-59287) |
### FTP
@@ -76,12 +96,25 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/ftp/ftp_bounce_test` | FTP bounce attack test via PORT commands to third-party hosts |
| `exploits/ftp/pachev_ftp_path_traversal_1_0` | Directory traversal in Pachev FTP Server 1.0 to read files outside FTP root |
+### HoneyTrap (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/honeytrap/docker_panic` | POST /v1.40/images/create without fromImage causes nil map panic in HoneyTrap Docker emulation — daemon exit |
+| `exploits/honeytrap/ftp_panic` | Malformed FTP PORT command with insufficient fields causes slice out-of-range panic in HoneyTrap — daemon exit |
+
### IPMI
| Module Path | Description |
|-------------|-------------|
| `exploits/ipmi/ipmi_enum_exploit` | IPMI enumeration with cipher 0 bypass, default credential brute force, and RAKP hash dumping |
+### Network Infrastructure -- Commvault
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/network_infra/commvault/cve_2025_34028_commvault_rce` | Commvault Command Center < 11.38.0 unauthenticated path traversal file upload to RCE (CVE-2025-34028) |
+
### Network Infrastructure -- Citrix
| Module Path | Description |
@@ -114,6 +147,12 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/network_infra/hpe/cve_2025_37164_hpe_oneview_rce` | Unauthenticated RCE via REST API command injection in HPE OneView (CVE-2025-37164) |
+### Network Infrastructure -- Kubernetes
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/network_infra/kubernetes/cve_2025_1974_ingress_nginx_rce` | ingress-nginx admission webhook config injection via annotations for arbitrary NGINX config, file read, and RCE (CVE-2025-1974) |
+
### Network Infrastructure -- Ivanti
| Module Path | Description |
@@ -241,14 +280,42 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/routers/zyxel/zyxel_cpe_ci_cve_2024_40890` | Zyxel legacy CPE unauthenticated HTTP command injection (CVE-2024-40890) |
+### Sample
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/sample_exploit` | Template exploit module demonstrating info(), check(), and run() with cfg_prompt integration |
+
+### SafeLine (WAF)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/safeline/cookie_attributes` | SafeLine session cookie lacks HttpOnly, Secure, and SameSite attributes enabling XSS session theft and CSRF |
+| `exploits/safeline/nginx_injection` | SafeLine tcontrollerd inserts Ports field verbatim into nginx config via fmt.Sprintf for arbitrary directive injection |
+| `exploits/safeline/no_auth_probe` | Detects SafeLine NO_AUTH env bypass where `len(noAuth) >= 0` (always true) disables auth middleware |
+| `exploits/safeline/pre_auth_tfa` | Fresh SafeLine install unauthenticated TFA secret rotation via /api/OTPUrl for full account takeover |
+| `exploits/safeline/session_secret_entropy` | SafeLine JWT signing secret generated with math/rand seeded by time.Now().UnixNano() — as low as 39 bits effective entropy |
+| `exploits/safeline/unauth_writes` | SafeLine publicRouters expose unauthenticated POST to /api/Behaviour and /api/FalsePositives for analytics pollution and request amplification |
+
+### Snare (Honeypot)
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/snare/cookie_dos` | HTTP Cookie header without '=' separator causes IndexError crash in snare tanner_handler.py worker |
+| `exploits/snare/tanner_version_mitm` | Rogue HTTP server on port 8090 returns forged version response to snare's unauthenticated GET /version check |
+
### SSH
| Module Path | Description |
|-------------|-------------|
+| `exploits/ssh/asyncssh_beginauthpass` | AsyncSSH server begin_auth() returning False causes USERAUTH_SUCCESS bypass for unauthenticated session access |
| `exploits/ssh/erlang_otp_ssh_rce_cve_2025_32433` | Erlang/OTP SSH server unauthenticated RCE (CVE-2025-32433) |
+| `exploits/ssh/libssh2_rogue_server` | Rogue SSH server capturing credentials from libssh2 clients that accept USERAUTH_SUCCESS without verifying KEX state |
| `exploits/ssh/libssh_auth_bypass_cve_2018_10933` | libSSH server authentication bypass (CVE-2018-10933) |
| `exploits/ssh/openssh_regresshion_cve_2024_6387` | OpenSSH sshd signal handler race condition for unauthenticated RCE (CVE-2024-6387) |
| `exploits/ssh/opensshserver_9_8p1race_condition` | OpenSSH 9.8p1 race condition for heap-based RCE |
+| `exploits/ssh/paramiko_authnonepass` | Paramiko SSH server check_auth_none() returning AUTH_SUCCESSFUL allows unauthenticated session access |
+| `exploits/ssh/paramiko_unknown_method` | Paramiko SSH server unrecognized auth method fallthrough to check_auth_none() allows authentication bypass |
| `exploits/ssh/sshpwn_auth_passwd` | OpenSSH auth2-passwd.c password length DoS, change info leak, timing enumeration |
| `exploits/ssh/sshpwn_pam` | OpenSSH auth-pam.c environment injection, memory leak DoS, username validation bypass |
| `exploits/ssh/sshpwn_scp_attacks` | OpenSSH SCP path traversal, command injection, and brace expansion DoS |
@@ -261,6 +328,27 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
|-------------|-------------|
| `exploits/telnet/telnet_auth_bypass_cve_2026_24061` | Telnet authentication bypass on vulnerable devices (CVE-2026-24061) |
+### VNC
+
+| Module Path | Description |
+|-------------|-------------|
+| `exploits/vnc/libvnc_checkrect_overflow` | LibVNCClient signed 32-bit bounds check integer overflow for heap overflow RCE |
+| `exploits/vnc/libvnc_tight_filtergradient` | LibVNCClient Tight decoder unclamped numRows out-of-bounds write past allocated buffer |
+| `exploits/vnc/libvnc_ultrazip` | LibVNCClient Ultra encoding unbounded cache rect loop for heap overflow (CVE-2018-20750) |
+| `exploits/vnc/libvnc_websocket_overflow` | LibVNCServer WebSocket unbounded 64-bit payloadLen for heap overflow |
+| `exploits/vnc/libvnc_zrle_tile` | LibVNCClient ZRLE decoder truncated RLE tile buffer over-read |
+| `exploits/vnc/rfb` | Shared RFB protocol helpers for VNC exploit modules |
+| `exploits/vnc/tigervnc_rre_overflow` | TigerVNC RRE decoder unbounded numSubrects loop for heap over-read |
+| `exploits/vnc/tigervnc_timing_oracle` | TigerVNC VNC auth DES response timing side-channel for bit-by-bit key recovery |
+| `exploits/vnc/tightvnc_decompression_bomb` | TightVNC FileUploadData uncapped uncompressedSize for heap exhaustion DoS |
+| `exploits/vnc/tightvnc_des_hardcoded_key` | TightVNC hardcoded 8-byte DES key for offline Windows registry password decryption |
+| `exploits/vnc/tightvnc_ft_path_traversal` | TightVNC file-transfer handler directory traversal for arbitrary file read/write |
+| `exploits/vnc/tightvnc_predictable_challenge` | TightVNC srand(time(0)) predictable 16-byte RFB challenge for replay attacks |
+| `exploits/vnc/tightvnc_rect_overflow` | TightVNC signed int32 multiplication overflow in Rect::area() for heap buffer overflow RCE |
+| `exploits/vnc/x11vnc_dns_injection` | x11vnc reverse-DNS hostname passed unsanitized to system() for shell injection via crafted PTR record |
+| `exploits/vnc/x11vnc_env_injection` | x11vnc RFB_CLIENT_IP environment variable injection into hook scripts |
+| `exploits/vnc/x11vnc_unixpw_inject` | x11vnc -unixpw mode newline injection in plaintext username to confuse PAM flow |
+
### VoIP
| Module Path | Description |
@@ -278,8 +366,10 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/webapps/flowise/cve_2025_59528_flowise_rce` | Flowise < 3.0.5 unauthenticated API RCE (CVE-2025-59528) |
| `exploits/webapps/langflow_rce_cve_2025_3248` | Langflow unauthenticated RCE via Python exec() in code validation (CVE-2025-3248) |
| `exploits/webapps/laravel_livewire_rce_cve_2025_47949` | Laravel Livewire RCE via unsafe deserialization (CVE-2025-47949) |
+| `exploits/webapps/misp_rce_cve_2025_27364` | MISP < 2.5.3 authenticated file upload to PHP webshell RCE via /events/upload_sample (CVE-2025-27364) |
| `exploits/webapps/mcpjam/cve_2026_23744_mcpjam_rce` | MCPJam Inspector <= 1.4.2 unauthenticated RCE (CVE-2026-23744) |
| `exploits/webapps/n8n/n8n_rce_cve_2025_68613` | n8n workflow automation RCE via expression injection (CVE-2025-68613) |
+| `exploits/webapps/nextjs_middleware_bypass_cve_2025_29927` | Next.js < 15.2.3 middleware bypass via unauthenticated x-middleware-subrequest header (CVE-2025-29927) |
| `exploits/webapps/react/react2shell` | React Server Components / Next.js RCE via RSC Flight protocol deserialization |
| `exploits/webapps/roundcube/roundcube_postauth_rce` | Roundcube webmail post-auth RCE via deserialization in file upload |
| `exploits/webapps/sap_netweaver_rce_cve_2025_31324` | SAP NetWeaver Visual Composer unauthenticated file upload to RCE (CVE-2025-31324) |
@@ -288,12 +378,14 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `exploits/webapps/solarwinds/cve_2025_40551_solarwinds_whd_rce` | SolarWinds Web Help Desk unauthenticated Java deserialization RCE (CVE-2025-40551) |
| `exploits/webapps/spotube/spotube` | Spotube API path traversal via WebSocket and denial of service |
| `exploits/webapps/termix/termix_xss_cve_2026_22804` | Termix File Manager stored XSS via SVG upload in Electron context (CVE-2026-22804) |
+| `exploits/webapps/vite_path_traversal_cve_2025_30208` | Vite dev server < 6.2.3 /@fs/ path traversal via ?import&raw query parameter bypass (CVE-2025-30208) |
| `exploits/webapps/wordpress/vitepos_file_upload_cve_2025_13156` | Vitepos for WooCommerce authenticated arbitrary PHP file upload (CVE-2025-13156) |
| `exploits/webapps/wordpress/wp_bricks_rce_cve_2024_25600` | Bricks Builder for WordPress unauthenticated RCE via render_element (CVE-2024-25600) |
| `exploits/webapps/wordpress/wp_litespeed_rce_cve_2024_28000` | LiteSpeed Cache weak hash brute force for WordPress admin escalation (CVE-2024-28000) |
| `exploits/webapps/wordpress/wp_royal_elementor_rce_cve_2024_32suspended` | Royal Elementor Addons unauthenticated PHP webshell upload |
| `exploits/webapps/xwiki/cve_2025_24893_xwiki_rce` | XWiki SolrSearch unauthenticated RCE via Groovy template injection (CVE-2025-24893) |
| `exploits/webapps/zabbix/zabbix_7_0_0_sql_injection` | Zabbix 7.0.0 time-based SQL injection in API endpoints |
+| `exploits/webapps/zimbra_sqli_auth_bypass_cve_2025_25064` | Zimbra ZCS < 10.0.12 unauthenticated SQL injection via /service/home~ for email metadata extraction (CVE-2025-25064) |
### Windows
@@ -317,7 +409,9 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `scanners/nbns_scanner` | NBNS name queries to UDP 137 for Windows host discovery |
| `scanners/ping_sweep` | Host discovery via ICMP echo, TCP connect, SYN, and ACK probes with CIDR support |
| `scanners/port_scanner` | TCP/UDP port scanner with service detection, banner grabbing, and configurable ranges |
+| `scanners/proxy_scanner` | HTTP CONNECT, SOCKS4, SOCKS5, and transparent proxy discovery with authentication detection |
| `scanners/redis_scanner` | Redis instance discovery and unauthenticated access detection |
+| `scanners/reflect_scanner` | UDP amplification vulnerability scanner for DNS, NTP monlist, SSDP, and Memcached reflectors |
| `scanners/sample_scanner` | Demonstration scanner checking HTTP/HTTPS reachability and response codes |
| `scanners/sequential_fuzzer` | Character-based HTTP fuzzer with 10+ encodings, custom charsets, and concurrent requests |
| `scanners/service_scanner` | Service port banner grabbing and version identification |
@@ -330,6 +424,7 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| `scanners/stalkroute_full_traceroute` | Advanced traceroute with ICMP/TCP/UDP probes, OS fingerprint spoofing, and decoy packets |
| `scanners/subdomain_scanner` | Subdomain brute-force enumeration via DNS resolution |
| `scanners/vnc_scanner` | VNC protocol version and security type enumeration |
+| `scanners/vuln_checker` | Fingerprint-based vulnerability scanner with detection signatures across all exploit modules |
| `scanners/waf_detector` | Web Application Firewall and CDN provider detection via HTTP response analysis |
---
@@ -340,32 +435,33 @@ All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use
| Module Path | Description |
|-------------|-------------|
-| `creds/generic/ssh_bruteforce` | SSH password brute force with default credential testing, combo mode, and subnet scanning |
-| `creds/generic/rdp_bruteforce` | RDP auth brute force with NLA, TLS, Standard RDP, and Negotiate security levels |
-| `creds/generic/ftp_bruteforce` | FTP/FTPS brute force with combo mode, concurrent connections, and subnet scanning |
-| `creds/generic/telnet_bruteforce` | Telnet brute force with full IAC negotiation, multiple attack modes, and subnet scanning |
-| `creds/generic/smtp_bruteforce` | SMTP auth brute force supporting PLAIN and LOGIN mechanisms with combo mode |
-| `creds/generic/pop3_bruteforce` | POP3/POP3S brute force with SSL/TLS support, retry logic, and subnet scanning |
-| `creds/generic/mqtt_bruteforce` | MQTT 3.1.1 auth testing with TLS/SSL, anonymous detection, and multiple attack modes |
-| `creds/generic/snmp_bruteforce` | SNMPv1/v2c community string brute force with read/write detection and subnet scanning |
-| `creds/generic/rtsp_bruteforce` | RTSP auth brute force for IP cameras with path bruting and custom headers |
-| `creds/generic/l2tp_bruteforce` | L2TP/IPsec VPN CHAP auth brute force against L2TP concentrators |
+| `creds/generic/couchdb_bruteforce` | CouchDB session cookie and HTTP Basic auth brute force with default credential testing and subnet scanning |
+| `creds/generic/elasticsearch_bruteforce` | Elasticsearch HTTP Basic auth brute force against cluster root and security API with subnet scanning |
+| `creds/generic/enablebruteforce` | Raises file descriptor limits (ulimit) for high-concurrency brute-force operations |
| `creds/generic/fortinet_bruteforce` | Fortinet FortiGate SSL VPN web auth brute force with certificate pinning and realm support |
| `creds/generic/ftp_anonymous` | FTP anonymous access check with FTPS, IPv4/IPv6, and mass scanning support |
-| `creds/generic/telnet_hose` | Mass internet Telnet default credential scanner with 500 workers and disk-based state |
-| `creds/generic/ssh_user_enum` | SSH username enumeration via timing-based side-channel attack (CVE-2018-15473 inspired) |
-| `creds/generic/ssh_spray` | SSH password spray across multiple targets with lockout-aware delays |
-| `creds/generic/enablebruteforce` | Raises file descriptor limits (ulimit) for high-concurrency brute-force operations |
+| `creds/generic/ftp_bruteforce` | FTP/FTPS brute force with combo mode, concurrent connections, and subnet scanning |
+| `creds/generic/http_basic_bruteforce` | HTTP Basic Authentication brute force with HTTPS support, default credentials, and subnet scanning |
+| `creds/generic/imap_bruteforce` | IMAP/IMAPS LOGIN command brute force over raw TCP with TLS support and subnet scanning |
+| `creds/generic/l2tp_bruteforce` | L2TP/IPsec VPN CHAP auth brute force against L2TP concentrators |
+| `creds/generic/memcached_bruteforce` | Memcached open instance detection and SASL PLAIN auth brute force over binary protocol |
+| `creds/generic/mqtt_bruteforce` | MQTT 3.1.1 auth testing with TLS/SSL, anonymous detection, and multiple attack modes |
+| `creds/generic/mysql_bruteforce` | MySQL native password wire protocol brute force with HandshakeV10 parsing and subnet scanning |
+| `creds/generic/pop3_bruteforce` | POP3/POP3S brute force with SSL/TLS support, retry logic, and subnet scanning |
+| `creds/generic/postgres_bruteforce` | PostgreSQL protocol v3 brute force supporting cleartext and MD5 auth with subnet scanning |
+| `creds/generic/proxy_bruteforce` | HTTP CONNECT, SOCKS5, and HTTP forward proxy authentication brute force |
+| `creds/generic/rdp_bruteforce` | RDP auth brute force with NLA, TLS, Standard RDP, and Negotiate security levels |
+| `creds/generic/redis_bruteforce` | Redis AUTH brute force supporting legacy and ACL mode with server info gathering on success |
+| `creds/generic/rtsp_bruteforce` | RTSP auth brute force for IP cameras with path bruting and custom headers |
| `creds/generic/sample_cred_check` | Sample module testing HTTP Basic Auth with default admin:admin credentials |
-| `creds/generic/vnc_bruteforce` | VNC DES challenge-response brute force with password-only auth and bit-reversed DES key |
-| `creds/generic/imap_bruteforce` | IMAP/IMAPS brute force with LOGIN command, implicit TLS, and RFC 3501 escaping |
-| `creds/generic/mysql_bruteforce` | MySQL native wire protocol brute force with HandshakeV10 parsing and SHA1 salt extraction |
-| `creds/generic/postgres_bruteforce` | PostgreSQL wire protocol brute force with MD5 and cleartext auth support |
-| `creds/generic/redis_bruteforce` | Redis AUTH brute force with legacy and ACL (Redis 6+) support, INFO version detection |
-| `creds/generic/elasticsearch_bruteforce` | Elasticsearch HTTP Basic Auth brute force with cluster detection and open-access check |
-| `creds/generic/couchdb_bruteforce` | CouchDB session auth brute force with Basic fallback and `/_all_dbs` verification |
-| `creds/generic/memcached_bruteforce` | Memcached SASL PLAIN brute force via binary protocol with open-access detection |
-| `creds/generic/http_basic_bruteforce` | HTTP Basic Authentication brute force with HTTPS, custom paths, and redirect detection |
+| `creds/generic/smtp_bruteforce` | SMTP auth brute force supporting PLAIN and LOGIN mechanisms with combo mode |
+| `creds/generic/snmp_bruteforce` | SNMPv1/v2c community string brute force with read/write detection and subnet scanning |
+| `creds/generic/ssh_bruteforce` | SSH password brute force with default credential testing, combo mode, and subnet scanning |
+| `creds/generic/ssh_spray` | SSH password spray across multiple targets with lockout-aware delays |
+| `creds/generic/ssh_user_enum` | SSH username enumeration via timing-based side-channel attack (CVE-2018-15473 inspired) |
+| `creds/generic/telnet_bruteforce` | Telnet brute force with full IAC negotiation, multiple attack modes, and subnet scanning |
+| `creds/generic/telnet_hose` | Mass internet Telnet default credential scanner with 500 workers and disk-based state |
+| `creds/generic/vnc_bruteforce` | VNC DES challenge-response brute force with bit-reversed key derivation and subnet scanning |
### Camera
diff --git a/docs/Module-Development.md b/docs/Module-Development.md
index ba6bafb..c238fb1 100644
--- a/docs/Module-Development.md
+++ b/docs/Module-Development.md
@@ -25,38 +25,6 @@ Because it's generated at build time, there is **no manual registry drift** as l
---
-## Naming Convention
-
-### No Underscore Prefixes
-
-Do not use underscore prefixes on function names or variable names. All identifiers use standard Rust `snake_case` without leading underscores.
-
-```rust
-// CORRECT
-fn validate_input(data: &str) -> bool { ... }
-fn build_enriched_entry(path: &str) -> serde_json::Value { ... }
-pub async fn run(target: &str) -> Result<()> { ... }
-
-// WRONG — no leading underscores
-fn _validate_input(data: &str) -> bool { ... }
-let _unused = something();
-```
-
-### Variables Must Be Used
-
-All declared variables must be consumed. Do not use the `_` prefix on variables to suppress unused warnings. If a variable is unused, remove it or use it.
-
-```rust
-// WRONG — suppressing unused warning
-let _result = some_operation();
-
-// CORRECT — use the variable
-let result = some_operation();
-log::debug!("Result: {:?}", result);
-```
-
----
-
## Code Rules
- **No dead code.** All code must be intentional and used. Do not leave unused functions, imports, or variables.
@@ -74,29 +42,34 @@ rustsploit/
│ ├── main.rs # Entry point — CLI or shell mode, input validation
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
-│ ├── api.rs # REST API server — auth, rate limiting, hardening
+│ ├── api.rs # REST + WebSocket API server — PQ encryption, rate limiting
+│ ├── ws.rs # PQ-encrypted WebSocket transport (/pq/ws)
│ ├── config.rs # Global config and target validation
│ ├── module_info.rs # ModuleInfo, CheckResult, ModuleRank types
-│ ├── global_options.rs # Per-workspace options (setg/unsetg)
-│ ├── cred_store.rs # Per-workspace credential store (JSON persistence)
+│ ├── global_options.rs # Persistent global options (setg/unsetg)
+│ ├── cred_store.rs # Credential store (JSON persistence)
│ ├── spool.rs # Console output logging
│ ├── workspace.rs # Host/service tracking + workspaces
│ ├── loot.rs # Loot/evidence management
│ ├── export.rs # JSON/CSV/summary report export
│ ├── jobs.rs # Background job management
+│ ├── mcp/
+│ │ ├── mod.rs # MCP server entry point (--mcp flag)
+│ │ ├── server.rs # JSON-RPC stdio transport with binary-safe reads
+│ │ └── tools.rs # 38 MCP tool implementations
│ ├── commands/
│ │ ├── mod.rs # Module discovery, fuzzy matching, multi-target dispatch
│ │ ├── exploit.rs
│ │ ├── scanner.rs
│ │ └── creds.rs
│ ├── modules/
-│ │ ├── exploits/ # Exploit modules (137 modules, 24 with check)
-│ │ ├── scanners/ # Scanner modules (24 modules)
-│ │ ├── creds/ # Credential modules (28 modules)
+│ │ ├── exploits/ # Exploit modules (183 modules, 21 categories)
+│ │ ├── scanners/ # Scanner modules (27 modules)
+│ │ ├── creds/ # Credential modules (29 modules)
│ │ └── plugins/ # Plugin modules (1 module)
│ ├── native/ # Native integrations
│ │ ├── mod.rs
-│ │ ├── rdp.rs # xfreerdp/rdesktop wrapper
+│ │ ├── rdp.rs # Native RDP auth (X.224, TLS, CredSSP/NTLM)
│ │ ├── payload_engine.rs # Payload encoding/generation
│ │ ├── url_encoding.rs # URL encoding utilities
│ │ └── async_tls.rs # Async TLS helpers
@@ -105,7 +78,8 @@ rustsploit/
│ ├── prompt.rs # Config-aware prompts (cfg_prompt_*)
│ ├── sanitize.rs # Input validation, length limits
│ ├── target.rs # Target normalization (IPv4/IPv6/CIDR/hostname)
-│ ├── network.rs # Network utilities
+│ ├── network.rs # HTTP client builders, TCP/UDP connect helpers
+│ ├── privilege.rs # Root privilege check (require_root)
│ └── modules.rs # Module discovery helpers
├── docs/ # This wiki
├── lists/ # Wordlists and data files
@@ -181,8 +155,7 @@ The `check` shell command and `POST /api/check` endpoint run this without exploi
Modules can auto-store discovered data:
```rust
-// Store a found credential (stored in the current workspace's credential store)
-// Credentials are isolated per workspace — switching workspaces switches the store.
+// Store a found credential
crate::cred_store::store_credential(host, port, "ssh", username, password,
crate::cred_store::CredType::Password, "creds/generic/ssh_bruteforce");
@@ -310,64 +283,3 @@ fn is_excluded_ip(ip: Ipv4Addr) -> bool { ... }
The `EXCLUDED_RANGES` constant covers bogons, private, reserved, documentation CIDRs, and public DNS servers. Copy this pattern from an existing mass-scan module (e.g., `telnet_hose` or `hikvision_rce`).
Honeypot detection is disabled in mass-scan mode to avoid interactive prompts.
-
----
-
-### MCP Tool Development
-
-Rustsploit exposes an MCP (Model Context Protocol) tool interface under `src/mcp/`. MCP tools allow AI agents to invoke framework functionality programmatically through a structured JSON-RPC protocol.
-
-**Adding an MCP tool:**
-
-1. Create a new handler in `src/mcp/` that implements the MCP tool interface.
-2. Define the tool's input schema (JSON Schema) and output format.
-3. Register the tool in the MCP tool registry so it appears in `tools/list` responses.
-4. Use the existing module dispatch system to route MCP tool calls to the appropriate exploit, scanner, or credential module.
-
-MCP tools follow the same security model as the REST API: input validation, sanitization, and rate limiting all apply. The MCP layer is a thin adapter over the existing shell command dispatch -- it does not bypass any framework security controls.
-
----
-
-### Payload Mutation Engine
-
-The payload mutation engine (`src/native/payload_engine.rs`) provides encoding, obfuscation, and transformation of payloads for AV/EDR evasion. Module authors can use it to dynamically encode payloads before delivery.
-
-**Supported encodings:**
-
-- XOR with configurable key
-- Base64 (standard and URL-safe)
-- Hex encoding
-- Zero-width Unicode steganography
-- Custom alphabet substitution
-
-**Usage in modules:**
-
-```rust
-use crate::native::payload_engine::{encode_payload, EncodingType};
-
-let encoded = encode_payload(raw_payload, EncodingType::Xor { key: 0x41 })?;
-```
-
-The engine is used by the `payload_encoder` and `narutto_dropper` exploit modules. When writing new exploit modules that deliver payloads, prefer using the mutation engine over hardcoded encoding to benefit from future encoding additions.
-
----
-
-## Mandatory Framework Rules
-
-### Network Connections
-- **TCP**: MUST use `crate::utils::network::tcp_connect()` or `tcp_connect_addr()` — never raw `TcpStream::connect`
-- **UDP**: MUST use `crate::utils::network::udp_bind()` or `blocking_udp_bind()` — never raw `UdpSocket::bind("0.0.0.0:0")`
-- **HTTP**: MUST use `crate::utils::build_http_client()` for reqwest clients (cached, connection pooling)
-- **TLS**: Use `crate::native::async_tls::make_dangerous_tls_connector()` (cached singleton)
-- **Blocking TCP**: Use `crate::utils::blocking_tcp_connect()` for SSH and other blocking protocols
-
-These utilities automatically respect `setg source_port` for firewall bypass testing.
-
-### Console Output
-- MUST use `crate::mprintln!()` / `crate::meprintln!()` — never raw `println!` / `eprintln!`
-- This ensures output is captured by the spool logging system when active
-
-### DoS Modules
-- Use `crate::native::dos_utils::FastRng` for packet randomization
-- Use `crate::native::dos_utils::checksum_16()` for IP/TCP/UDP checksums
-- Use `crate::native::dos_utils::is_spoof_enabled()` to check global `setg spoof_ip true`
diff --git a/docs/Security-Validation.md b/docs/Security-Validation.md
index 9e0fe40..c5e5e06 100644
--- a/docs/Security-Validation.md
+++ b/docs/Security-Validation.md
@@ -106,81 +106,57 @@ loop {
When reading files:
1. Validate path does not contain `..`
-2. Open with `O_NOFOLLOW` to atomically reject symlinks (prevents TOCTOU)
-3. Resolve canonical path via `/proc/self/fd/N` on the open file descriptor
-4. Check file size before reading (ref: `MAX_FILE_SIZE`)
-5. Use `Read::take()` to cap memory usage at IO level
-
-When writing files (spool, export):
-1. Validate filename (no traversal, no absolute paths, no symlinks)
-2. Open with `O_NOFOLLOW` via `custom_flags(libc::O_NOFOLLOW)` on `OpenOptions`
-3. Write to temp file, then atomic rename
-
-### 8. SSRF Protection
-
-The `is_blocked_target()` function blocks cloud metadata and internal endpoints:
-- Parses target as `std::net::IpAddr` (handles IPv4, IPv6, mapped addresses)
-- Blocks: `169.254.0.0/16` (link-local), `168.63.129.16` (Azure), `100.100.100.200` (Alibaba), `fd00:ec2::*` (AWS IPv6)
-- Hostname check: `metadata.google.internal`
-- Applied to: `run_module`, `run_all` (per-IP), `check_module`, `honeypot_check`, shell `run`/`run_all`/`check`
-
-### 9. CIDR Size Limits
-
-`run_all` rejects subnets that would cause resource exhaustion:
-- IPv4: minimum /16 (65,536 hosts max)
-- IPv6: minimum /48
-
-### 10. SQL LIKE Escaping (ArcticAlopex)
-
-All `like()` queries escape user input with `escapeLike()`:
-```typescript
-function escapeLike(v: string): string {
- return v.replace(/[%_\\]/g, (ch) => `\\${ch}`);
-}
-```
-
-### 11. ACL Shell Command Gating (ArcticAlopex)
-
-Shell commands are normalized before ACL matching:
-```typescript
-const cmd = command.trim().toLowerCase().replace(/\s+/g, " ");
-```
-This prevents whitespace-padding bypasses like `loot delete`.
-
-**Per-account login lockout:** In addition to IP-based rate limiting, a per-account lockout is enforced: 10 failed login attempts trigger a 30-minute lock on the account.
-
-**Module restrictions enforcement:** The ACL `resolve()` engine now receives `role_module_restrictions` loaded from the database. Previously, an empty array was always passed, effectively bypassing module-level restrictions for all roles.
+2. Use `canonicalize()` to resolve the real path
+3. Check file size before reading (ref: `MAX_FILE_SIZE`)
+4. Skip symlinks for security
---
-## API Security (Rustsploit Backend)
+## API Security
The API server (`api.rs`) implements:
-- **Post-quantum encryption** — ML-KEM-768 + X25519 hybrid, ChaCha20-Poly1305 AEAD, Double Ratchet forward secrecy
- **`RequestBodyLimitLayer`** — prevents DoS via oversized payloads (1 MB max)
-- **SSRF protection** — `is_blocked_target()` with IP parsing on all execution endpoints
-- **CIDR limits** — rejects prefixes below /16 IPv4, /48 IPv6
-- **Shell metacharacter blocking** — `contains_shell_metacharacters()` on all shell commands
-- **Module name validation** — alphanumeric + `/` `_` `-` only, max 256 chars
-- **File read cap** — `Read::take(1MB)` prevents OOM on large result files
-- **Epoch monotonicity** — PQ session rejects replayed messages from older epochs
-- **Counter-based nonces** — deterministic nonces derived from `[epoch|counter]`, no birthday risk
+- **Rate limiting** — 3 failed auth attempts → 30 s block per IP
+- **Auto-cleanup** — old entries purged at 100,000 entries
+- **IP tracking + key rotation** — suspicious activity triggers auto-rotation in hardening mode
+- **Secure defaults** — by default, considers `127.0.0.1` as the intended private bind
+- **WebSocket limits** — max 100 concurrent connections, 1 MiB frame cap, 30s heartbeat
-## API Security (ArcticAlopex Frontend)
+---
-The frontend proxy (`rsf-proxy.ts`, `rsf/[...path]/route.ts`) implements:
+## MCP Server Security
-- **RBAC enforcement** — every RSF command re-gated against user's role permissions
-- **Per-tenant mutex** — serializes write operations to prevent race conditions
-- **Input validation** — module names and targets validated before shell command construction
-- **Error propagation** — RSF backend errors (401, 500) surfaced to UI instead of silent success
-- **Rate limiting** — per-user, Redis-backed, 30 module executions per minute
-- **Rate limiting (auth)** — per-IP with second-to-last XFF extraction, 50K entry hard cap, stale cleanup
-- **Session security** — HttpOnly, Secure, SameSite=Strict cookies, 8-hour TTL
-- **CSRF protection** — SameSite=Strict prevents cross-origin requests
-- **SQL injection** — Drizzle ORM parameterized queries + LIKE wildcard escaping
-- **TOTP 2FA** — Optional Argon2id + TOTP, secret never exposed in API responses. TOTP secrets are encrypted at rest with AES-256-GCM via MASTER_KEY before DB storage.
+The MCP server (`mcp/server.rs`) implements:
+
+- **`isolate_protocol_stdout()`** — redirects fd 1 to /dev/null so module `println!` cannot corrupt the JSON-RPC stream
+- **`MAX_LINE_BYTES`** — 1 MiB cap on incoming lines to prevent memory exhaustion
+- **Binary-safe reads** — uses `read_until()` instead of `read_line()` for no UTF-8 requirement
+- **Non-UTF-8 error handling** — returns proper JSON-RPC error responses for malformed input
+
+---
+
+## Spool Security
+
+The spool system (`spool.rs`) implements:
+
+- **`O_NOFOLLOW`** — prevents TOCTOU race conditions on symlinked spool files
+- **Parent symlink check** — rejects spool paths with symlinked parent directories
+- **Lock-first pattern** — acquires write lock before creating files to prevent orphaned files
+- **`write_line()` returns `Result`** — callers handle write failures instead of silently dropping output
+
+---
+
+## Privilege Checks
+
+Modules requiring raw sockets call `require_root()` at startup:
+
+```rust
+use crate::utils::privilege::require_root;
+require_root("ICMP raw socket")?;
+```
+
+Returns a descriptive error with the current euid instead of a cryptic "permission denied" from the socket layer. Used by DoS modules, ping sweep, and raw packet scanners.
---
@@ -231,40 +207,12 @@ All persistent data uses atomic write-to-temp-then-rename to prevent corruption:
| File | Purpose | Sensitivity |
|------|---------|-------------|
-| `~/.rustsploit/workspaces/{name}_options.json` | Per-workspace options | Low — user preferences |
-| `~/.rustsploit/workspaces/{name}_creds.json` | Per-workspace credential store | **High — contains passwords/hashes** |
+| `~/.rustsploit/global_options.json` | Global options (setg) | Low — user preferences |
+| `~/.rustsploit/creds.json` | Discovered credentials | **High — contains passwords/hashes** |
| `~/.rustsploit/workspaces/.json` | Hosts, services, notes | Medium — engagement data |
| `~/.rustsploit/loot_index.json` | Loot metadata | Medium |
| `~/.rustsploit/loot/` | Loot files | **High — may contain sensitive data** |
| `~/.rustsploit/results/` | Module output files | Medium |
| `~/.rustsploit/history.txt` | Shell command history | Medium |
-| `~/.rustsploit/logs/rustsploit.*.log` | Daily rolling log files | Low — operational logs |
**Important:** The `creds.json` and `loot/` files may contain sensitive data. Protect `~/.rustsploit/` with appropriate file permissions (e.g., `chmod 700`).
-
----
-
-## Cloud Metadata SSRF Protection
-
-The API server (`api.rs`) blocks requests targeting cloud metadata endpoints to prevent SSRF attacks:
-
-| Blocked Target | Cloud Provider |
-|----------------|---------------|
-| `169.254.169.254` | AWS / GCP / Azure metadata |
-| `metadata.google.internal` | GCP metadata |
-| `100.100.100.200` | Alibaba Cloud metadata |
-
-These checks apply to all module execution requests (`POST /api/run`) and target-setting operations. Requests targeting these addresses are rejected before any module code executes.
-
----
-
-## MCP Input Validation
-
-The MCP server (`src/mcp/`) applies the same validation rules as the REST API:
-
-- **Tool parameters** are validated before dispatch. Missing required fields return a JSON-RPC error (`-32602 Invalid params`).
-- **Target injection prevention**: The `run_module` tool strips any `target` key from the `prompts` object to prevent prompt-injection SSRF bypass. The target is only accepted from the top-level `target` parameter.
-- **Module path validation**: Module paths are checked against the build-time discovered module list before execution.
-- **Credential redaction**: The `rustsploit:///credentials` resource redacts secrets (first 3 characters + `***`) to prevent accidental exposure through MCP resource reads.
-- **No file system access**: MCP tools do not expose direct file read/write operations. Export data is returned inline as JSON, not written to disk.
-- **Rate limiting**: MCP runs over stdio (single client), so no per-IP rate limiting is needed. Concurrency is bounded by the framework's semaphore (CPU count, min 4).
diff --git a/docs/Testing-QA.md b/docs/Testing-QA.md
index 76524be..2cf1a66 100644
--- a/docs/Testing-QA.md
+++ b/docs/Testing-QA.md
@@ -19,7 +19,7 @@ cargo clippy
cargo check
```
-A clean `cargo check` with **0 errors and 0 warnings** is required. The current codebase (all 190 modules) passes this check cleanly.
+A clean `cargo check` with **0 errors and 0 warnings** is required. The current codebase (all 240 modules) passes this check cleanly.
---
@@ -29,7 +29,7 @@ A clean `cargo check` with **0 errors and 0 warnings** is required. The current
cargo build
```
-`build.rs` regenerates the dispatchers (`exploit_dispatch.rs`, `scanner_dispatch.rs`, `creds_dispatch.rs`, `plugins_dispatch.rs`, `module_registry.rs`) into `OUT_DIR` during compilation. All 190 modules (137 exploits, 28 creds, 24 scanners, 1 plugin) are auto-discovered and dispatched by `build.rs`. If a new module fails to register, ensure `pub mod your_module;` is present in the sibling `mod.rs`.
+`build.rs` regenerates the dispatchers (`exploit_dispatch.rs`, `scanner_dispatch.rs`, `creds_dispatch.rs`, `plugins_dispatch.rs`, `module_registry.rs`) into `OUT_DIR` during compilation. All 240 modules (183 exploits, 27 scanners, 29 creds, 1 plugin) are auto-discovered and dispatched by `build.rs`. If a new module fails to register, ensure `pub mod your_module;` is present in the sibling `mod.rs`.
---
@@ -48,7 +48,7 @@ go # Runs the sample scanner against localhost
### CLI
```bash
-cargo run -- --command scanner --module sample_scanner --target 127.0.0.1
+cargo run -- -m scanners/sample_scanner -t 127.0.0.1
cargo run -- --list-modules # Verify your module is listed
```
@@ -57,8 +57,8 @@ cargo run -- --list-modules # Verify your module is listed
# Start the server
cargo run -- --api
-# Check your module appears
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/modules | grep your_module
+# Verify server starts (module listing requires PQ WebSocket session)
+curl http://localhost:8080/health
```
---
@@ -127,18 +127,10 @@ export json /tmp/test.json # Should create JSON file
```
```bash
-# API smoke test
+# API smoke test — verify server starts and health endpoint responds
cargo run -- --api
-
-# New endpoints
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/options
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/creds
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/hosts
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/services
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/workspace
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/loot
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/jobs
-curl -H "Authorization: Bearer test-key" http://localhost:8080/api/export?format=json
+curl http://localhost:8080/health
+# All other endpoints require a PQ WebSocket session — see API-Server.md
```
---
@@ -161,61 +153,6 @@ curl -H "Authorization: Bearer test-key" http://localhost:8080/api/export?format
---
-## MCP Integration Tests
-
-Verify MCP server functionality after modifying `src/mcp/`:
-
-```bash
-# Start MCP server (stdio transport)
-cargo run -- --mcp
-
-# In another terminal, pipe JSON-RPC requests via stdin:
-echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | cargo run -- --mcp
-
-# Verify tool listing
-echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | cargo run -- --mcp
-
-# Verify resource listing
-echo '{"jsonrpc":"2.0","id":3,"method":"resources/list","params":{}}' | cargo run -- --mcp
-```
-
-Key verification points:
-- `initialize` returns protocol version `2024-11-05` and both `tools` and `resources` capabilities
-- `tools/list` returns 30 tools
-- `resources/list` returns 7 resources
-- `tools/call` with `run_module` validates module path exists before execution
-- `tools/call` with `run_module` strips `target` from prompts (SSRF prevention)
-- Invalid JSON returns error code `-32700` (Parse error)
-- Unknown methods return error code `-32601` (Method not found)
-- Missing required params return error code `-32602` (Invalid params)
-
----
-
-## Native RDP Tests
-
-After modifying `src/native/rdp.rs`:
-
-```bash
-# Verify build compiles (RDP uses raw TCP, no external deps)
-cargo check
-
-# Test against a known RDP target (lab only)
-cargo run
-# In shell:
-use creds/generic/rdp_bruteforce
-set target
-run
-```
-
-Key verification points:
-- TCP connection + X.224 negotiation completes
-- TLS upgrade succeeds on standard RDP port 3389
-- CredSSP/NTLM authentication follows correct sequence
-- Failed auth returns clear error, does not hang
-- No `unsafe` blocks used in implementation
-
----
-
## Known Disabled / Stubbed Code
| Module | Status | Reason |
diff --git a/docs/Utilities-Helpers.md b/docs/Utilities-Helpers.md
index 7b2356a..3f79e11 100644
--- a/docs/Utilities-Helpers.md
+++ b/docs/Utilities-Helpers.md
@@ -5,19 +5,18 @@ Rustsploit provides several utility modules that every module developer should k
| Module | Import Path | Purpose |
|--------|-------------|---------|
| **Core Utils** | `crate::utils` | Target normalization, file loading, config-aware prompts, input validation |
+| **Network Utils** | `crate::utils::network` | HTTP client builders, TCP/UDP connect helpers, honeypot check |
+| **Privilege Utils** | `crate::utils::privilege` | Root privilege check for raw-socket modules |
| **Creds Utils** | `crate::modules::creds::utils` | Bruteforce statistics, subnet helpers, IP exclusion, scan state tracking |
| **Config** | `crate::config` | Global target state, module config, API prompt keys, results directory |
-| **Global Options** | `crate::global_options` | Per-workspace `setg` options — isolated per workspace, checked by `cfg_prompt_*` after custom_prompts |
-| **Cred Store** | `crate::cred_store` | Per-workspace credential store. Each workspace has its own credentials at `workspaces/{name}_creds.json` |
+| **Global Options** | `crate::global_options` | Persistent `setg` options — checked by `cfg_prompt_*` after custom_prompts |
+| **Cred Store** | `crate::cred_store` | Store/query discovered credentials. Call `store_credential()` from modules |
| **Workspace** | `crate::workspace` | Track hosts/services. Call `track_host()` / `track_service()` from modules |
| **Loot** | `crate::loot` | Store collected evidence. Call `store_loot()` from modules |
| **Module Info** | `crate::module_info` | `ModuleInfo`, `ModuleRank`, `CheckResult` types for `info()`/`check()` |
| **Spool** | `crate::spool` | Console output logging. Call `spool::sprintln()` for spool-aware output |
| **Jobs** | `crate::jobs` | Background job management via `JOB_MANAGER` |
| **Export** | `crate::export` | Export engagement data to JSON/CSV/summary |
-| **Output** | `crate::output` | Formatted output helpers with spool integration and color support |
-| **Payload Engine** | `crate::native::payload_engine` | Payload encoding/mutation (XOR, Base64, hex, zero-width, custom) for AV evasion |
-| **DoS Utils** | `crate::native::dos_utils` | FastRng (XorShift128+), checksum_16, sum_16, is_spoof_enabled |
---
@@ -622,13 +621,117 @@ std::fs::write(&out_path, results)?;
---
-### `tcp_connect_addr(addr: SocketAddr, timeout: Duration) -> io::Result`
+## `crate::utils::network` — Network Utilities
-Zero-allocation TCP connection for resolved IP addresses. Skips DNS resolution and string parsing. Automatically binds to global source port if set.
+Import path:
-### `build_http_client(timeout: Duration) -> Result`
+```rust
+use crate::utils::network::{
+ build_http_client, build_http_client_with, HttpClientOpts,
+ tcp_connect_addr, tcp_connect_str, tcp_connect, tcp_port_open,
+ blocking_tcp_connect, udp_bind, quick_honeypot_check,
+};
+```
-Returns a cached reqwest::Client (keyed by timeout). Clients are Arc-based internally so cloning is cheap. Enables HTTP keep-alive and connection pooling across module runs.
+---
+
+### `build_http_client(timeout) → Result`
+
+Creates a standard `reqwest::Client` with sensible defaults (danger-accept invalid certs, no redirect limit). Use this instead of hand-rolling `reqwest::Client::builder()`.
+
+```rust
+use crate::utils::network::build_http_client;
+
+let client = build_http_client(Duration::from_secs(10))?;
+let resp = client.get(&url).send().await?;
+```
+
+---
+
+### `build_http_client_with(timeout, opts) → Result`
+
+Extended HTTP client builder with additional options.
+
+```rust
+use crate::utils::network::{build_http_client_with, HttpClientOpts};
+
+let client = build_http_client_with(Duration::from_secs(10), HttpClientOpts {
+ cookie_store: true,
+ follow_redirects: true,
+ user_agent: Some("Mozilla/5.0".to_string()),
+ ..HttpClientOpts::default()
+})?;
+```
+
+#### `HttpClientOpts` Fields
+
+| Field | Type | Default | Description |
+|-------|------|---------|-------------|
+| `cookie_store` | `bool` | `false` | Enable cookie jar |
+| `follow_redirects` | `bool` | `false` | Follow HTTP redirects |
+| `user_agent` | `Option` | `None` | Custom User-Agent header |
+| `default_headers` | `Option` | `None` | Default headers for all requests |
+
+---
+
+### `tcp_connect_addr(addr, timeout) → io::Result`
+
+Async TCP connection to a `SocketAddr` with timeout and optional source port binding. Preferred over raw `TcpStream::connect` — respects global source port setting.
+
+```rust
+use crate::utils::network::tcp_connect_addr;
+
+let stream = tcp_connect_addr(addr, Duration::from_secs(5)).await?;
+```
+
+---
+
+### `tcp_connect_str(addr_str, timeout) → io::Result`
+
+Async TCP connection from a `"host:port"` string. Resolves DNS and connects.
+
+---
+
+### `tcp_port_open(ip, port, timeout) → bool`
+
+Quick async check if a TCP port is open.
+
+---
+
+### `blocking_tcp_connect(addr, timeout) → io::Result`
+
+Synchronous TCP connection for use in `spawn_blocking` contexts.
+
+---
+
+### `udp_bind(target_ip) → io::Result`
+
+Binds a UDP socket to the appropriate address family (IPv4 or IPv6) for the target.
+
+---
+
+### `quick_honeypot_check(ip) → bool`
+
+Fast honeypot detection — probes common ports and returns `true` if 11+ respond (likely honeypot).
+
+---
+
+## `crate::utils::privilege` — Privilege Checks
+
+### `require_root(context) → Result<()>`
+
+Call at the top of `run()` in modules that need raw sockets (ICMP, SYN scan, packet crafting). Returns a clean error message if the current euid is not root.
+
+```rust
+use crate::utils::privilege::require_root;
+
+pub async fn run(target: &str) -> Result<()> {
+ require_root("ICMP raw socket")?;
+ // ... raw socket operations ...
+}
+```
+
+Used by: DoS modules (icmp_flood, syn_ack_flood, null_syn_exhaustion, dns_amplification, etc.), ping_sweep scanner.
---
diff --git a/plans/compile-time-improvement-plan.md b/plans/compile-time-improvement-plan.md
new file mode 100644
index 0000000..c7ebe40
--- /dev/null
+++ b/plans/compile-time-improvement-plan.md
@@ -0,0 +1,155 @@
+# Plan: Improve Cargo Build/Run Compile Times
+
+## Context
+
+Clean build takes **14m 39s** (879s) across 431 compilation units. Incremental rebuilds are already fast (0.6s). The goal is to reduce clean/cold build times — critical for CI, fresh clones, and dependency updates.
+
+The biggest bottlenecks (from `cargo --timings`):
+- `rustsploit` final crate: **427s** (361 source files compiled as one unit)
+- `aws-lc-sys`: **317s** (C library build for rustls crypto — pulled by reqwest & rustls)
+- `dbus`: **116s** (solely from btleplug — used by 1 file)
+- `tokio`: **105s**
+- `darling_core` + `strum_macros`: **182s** (solely from ratatui — used by 1 file)
+- `regex-automata` (×2): **175s**
+- `clap_builder`: **76s**
+- `h2`: **71s**
+- `serde_derive` + `async-trait`: **135s**
+- `libssh2-sys`: **62s** (C library for ssh2)
+- `hickory-proto`: **60s** (used by 1 file)
+
+---
+
+## Changes (ordered by impact / risk)
+
+### 1. Configure lld linker
+**Savings: ~30-60s | Risk: None | Effort: 2 min**
+
+`lld` is installed at `/usr/bin/lld` but not configured. The default GNU `ld` is slow for a 110K-line binary.
+
+Create `.cargo/config.toml`:
+```toml
+[target.x86_64-unknown-linux-gnu]
+linker = "clang"
+rustflags = ["-C", "link-arg=-fuse-ld=lld"]
+```
+
+---
+
+### 2. Switch rustls crypto from aws-lc-rs to ring
+**Savings: ~250-280s | Risk: Low | Effort: 5 min**
+
+`aws-lc-sys` (317s) compiles a massive C library via cmake. It's pulled in because `rustls 0.23` defaults to `aws-lc-rs`. The `ring` backend is functionally equivalent and compiles in ~30-50s.
+
+`cargo tree -i aws-lc-sys` confirms the chain: `aws-lc-sys → aws-lc-rs → rustls → {reqwest, tokio-rustls, rustsploit}`.
+
+In `Cargo.toml`:
+```toml
+rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
+```
+
+No source code changes — `ring` and `aws-lc-rs` expose the same `rustls::crypto::CryptoProvider` API.
+
+**File:** `Cargo.toml` line 50
+
+---
+
+### 3. Feature-gate btleplug + ratatui + crossterm
+**Savings: ~300s | Risk: Medium | Effort: 30 min**
+
+These three crates are used by exactly **one file**: `src/modules/exploits/bluetooth/wpair.rs`. Their transitive cost:
+
+| Dep chain | Compile time |
+|-----------|-------------|
+| btleplug → dbus | 116s |
+| btleplug → async-trait | 68s |
+| ratatui → strum_macros | 85s |
+| ratatui → darling_core | 97s |
+| ratatui → ratatui-core | 42s |
+| crossterm | ~15s |
+
+Confirmed via `cargo tree -i dbus`, `cargo tree -i strum_macros`, `cargo tree -i darling_core` — all solely from btleplug/ratatui.
+
+**Changes:**
+
+`Cargo.toml` — add features section, make deps optional:
+```toml
+[features]
+default = []
+bluetooth = ["dep:btleplug", "dep:ratatui", "dep:crossterm"]
+```
+
+```toml
+btleplug = { version = "0.12", optional = true }
+ratatui = { version = "0.30", optional = true }
+crossterm = { version = "0.29", optional = true }
+```
+
+`src/modules/exploits/bluetooth/mod.rs` — gate the module:
+```rust
+#[cfg(feature = "bluetooth")]
+pub mod wpair;
+```
+
+`build.rs` — skip bluetooth dir when feature is absent. In `generate_dispatch()` (or the `find_modules` walk), check `env::var("CARGO_FEATURE_BLUETOOTH")` and skip paths containing `bluetooth/` when it's not set. This prevents the generated dispatch from referencing `wpair::run` when the module doesn't exist.
+
+When bluetooth is needed: `cargo build --features bluetooth` or `cargo run --features bluetooth`.
+
+---
+
+### 4. Clean up tokio feature flags
+**Savings: ~5-10s | Risk: None | Effort: 2 min**
+
+Current line is redundant — `"full"` already includes every named feature plus extras like `test-util`:
+```toml
+tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
+```
+
+Replace with only what's actually used:
+```toml
+tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "net", "io-util", "io-std", "fs", "process", "sync", "time", "signal"] }
+```
+
+**File:** `Cargo.toml` line 20
+
+---
+
+### 5. Feature-gate hickory DNS
+**Savings: ~60s | Risk: Low | Effort: 15 min**
+
+`hickory-proto` (60s) + `hickory-client` are used by exactly **one file**: `src/modules/scanners/dns_recursion.rs`.
+
+```toml
+[features]
+dns = ["dep:hickory-client", "dep:hickory-proto"]
+```
+
+```toml
+hickory-client = { version = "0.25", optional = true }
+hickory-proto = { version = "0.25", optional = true }
+```
+
+Gate in the scanner's `mod.rs` with `#[cfg(feature = "dns")]` and update `build.rs` to skip the module when the feature is absent.
+
+---
+
+## Files to modify
+
+| File | Changes |
+|------|---------|
+| `.cargo/config.toml` | **Create** — lld linker config |
+| `Cargo.toml` | rustls features, optional deps, `[features]` section, tokio cleanup |
+| `build.rs` | Skip feature-gated module dirs during code generation |
+| `src/modules/exploits/bluetooth/mod.rs` | `#[cfg(feature = "bluetooth")]` gate |
+| Scanner mod.rs for dns_recursion | `#[cfg(feature = "dns")]` gate |
+
+---
+
+## Verification
+
+1. `cargo clean && cargo build --timings 2>&1` — compare total time to baseline 879s
+2. `cargo build --features bluetooth,dns --timings` — verify full build still works
+3. `cargo run -- --help` — verify binary starts correctly
+4. `cargo run` — enter shell, run a non-bluetooth module (e.g. `use scanners/port_scanner`, `set target 127.0.0.1`, `run`) to confirm dispatch works
+5. `cargo build --features bluetooth` — verify bluetooth module compiles and appears in `list modules`
+
+**Expected result:** Clean build drops from ~879s to ~250-350s (60-70% reduction), with changes 1-3 providing the bulk of the savings.
diff --git a/preview.png b/preview.png
index 6a083f0..ba4ae52 100644
Binary files a/preview.png and b/preview.png differ
diff --git a/src/api.rs b/src/api.rs
index 27f1e50..689ddeb 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -1,1310 +1,246 @@
+use std::net::SocketAddr;
+use std::sync::Arc;
+
use anyhow::{Context, Result};
use axum::{
- extract::{ConnectInfo, Request, State},
- http::{HeaderValue, StatusCode},
- middleware::Next,
- response::{IntoResponse, Response},
+ response::Json,
routing::{get, post},
- Json, Router,
+ Router,
};
-use serde::{Deserialize, Serialize};
-use std::collections::HashMap;
-use std::net::SocketAddr;
-use std::sync::{Arc, Mutex};
-use std::time::Instant;
-use tower::ServiceBuilder;
-use tower_http::{
- limit::RequestBodyLimitLayer,
- trace::TraceLayer,
-};
-
use colored::*;
-use crate::commands;
-
-/// Maximum request body size (1MB)
-const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024;
-
-// ─── IP Whitelist ───────────────────────────────────────────────────
-
-/// Load IP whitelist from ~/.rustsploit/ip_whitelist.conf
-/// File format: one IP per line, '#' comments, blank lines ignored
-fn load_ip_whitelist() -> Vec {
- let path = dirs_path().join("ip_whitelist.conf");
- if !path.exists() {
- return Vec::new();
- }
- match std::fs::read_to_string(&path) {
- Ok(contents) => {
- contents
- .lines()
- .map(|l| l.trim())
- .filter(|l| !l.is_empty() && !l.starts_with('#'))
- .map(|l| l.to_string())
- .collect()
- }
- Err(e) => {
- tracing::warn!(path = %path.display(), error = %e, "Failed to read IP whitelist");
- Vec::new()
- }
- }
-}
-
-/// Get the rustsploit config directory (~/.rustsploit/)
-fn dirs_path() -> std::path::PathBuf {
- let home = home::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."));
- home.join(".rustsploit")
-}
-
-// ─── State ──────────────────────────────────────────────────────────
-
-#[derive(Clone)]
-pub struct ApiState {
- verbose: bool,
- /// Optional IP whitelist — if non-empty, only these IPs can access the API
- ip_whitelist: Arc>,
- /// Persistent module selection (mirrors ShellContext.current_module)
- current_module: Arc>>,
- /// Per-IP rate limiter: IP -> (request_count, window_start)
- rate_limiter: Arc>>,
- /// Limits concurrent module execution to avoid resource exhaustion.
- /// No longer serializes to 1 — per-task output capture allows concurrency.
- run_semaphore: Arc,
-}
-
-/// Max requests per IP per window
-const RATE_LIMIT_MAX_REQUESTS: u32 = 30;
-/// Rate limit window duration in seconds
-const RATE_LIMIT_WINDOW_SECS: u64 = 10;
-
-// ─── Request / Response Types ───────────────────────────────────────
-
-#[derive(Serialize, Deserialize)]
-pub struct ApiResponse {
- pub success: bool,
- pub message: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub data: Option,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub error_code: Option,
-}
-
-fn ok_response(message: impl Into, data: Option) -> ApiResponse {
- ApiResponse {
- success: true,
- message: message.into(),
- data,
- error_code: None,
- }
-}
-
-fn err_response(message: impl Into, code: &str) -> ApiResponse {
- ApiResponse {
- success: false,
- message: message.into(),
- data: None,
- error_code: Some(code.to_string()),
- }
-}
-
-#[derive(Serialize, Deserialize)]
-#[serde(deny_unknown_fields)]
-pub struct RunModuleRequest {
- pub module: String,
- pub target: String,
- // Optional module config fields (passed to modules like the CLI would)
- pub port: Option,
- pub username_wordlist: Option,
- pub password_wordlist: Option,
- pub concurrency: Option,
- pub stop_on_success: Option,
- pub save_results: Option,
- pub output_file: Option,
- pub verbose: Option,
- /// Combo mode: "linear", "combo", or "spray" (bruteforce modules)
- pub combo_mode: Option,
- /// Generic prompt overrides — any key/value pair that modules read via
- /// `cfg_prompt_*()`. Examples: {"mode": "1", "timeout": "5", "retries": "3"}
- pub prompts: Option>,
- /// If true, run as a background job and return job_id immediately.
- /// The caller can then poll GET /api/jobs to track progress.
- pub background: Option,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct SetTargetRequest {
- pub target: String,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct HoneypotCheckRequest {
- pub target: String,
-}
-
-#[derive(Serialize, Deserialize)]
-pub struct ShellRequest {
- /// Single command (backward compatible)
- pub command: Option,
- /// Array of commands for chaining (preferred, more secure than string splitting)
- pub commands: Option>,
-}
-
-#[derive(Serialize, Deserialize)]
-struct ShellResult {
- command: String,
- success: bool,
- output: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- duration_ms: Option,
-}
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
// ─── Validation Helpers ─────────────────────────────────────────────
-fn validate_module_name(module: &str) -> bool {
+pub(crate) fn validate_module_name(module: &str) -> bool {
!module.is_empty()
&& module.len() <= 256
&& module.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9' | '/' | '_' | '-'))
}
-fn validate_target(target: &str) -> bool {
+pub(crate) fn validate_target(target: &str) -> bool {
!target.is_empty() && target.len() <= 2048 && !target.chars().any(|c| c.is_control())
}
-/// Check if a target is a cloud metadata / link-local address that should never be hit
-/// from the API. Loopback and RFC-1918 are intentionally allowed — this is a pentesting
-/// framework and operators need to scan internal ranges. Only metadata endpoints that
-/// indicate a cloud SSRF are blocked.
-fn is_blocked_target(target: &str) -> bool {
+pub(crate) fn is_blocked_target(target: &str) -> bool {
+ const MASS_SCAN_KEYWORDS: &[&str] = &["random", "0.0.0.0", "0.0.0.0/0"];
+ if MASS_SCAN_KEYWORDS.contains(&target) {
+ return false;
+ }
+
let lower = target.to_lowercase();
- // Hostname-based checks (DNS names that resolve to metadata endpoints)
- if lower.contains("metadata.google.internal") {
+ // Try proper URL parsing first — handles @, port, scheme correctly
+ let host_no_port = if let Ok(parsed) = url::Url::parse(&lower) {
+ // Percent-decode the host and check it
+ parsed.host_str().map(|h| {
+ percent_decode_host(h)
+ })
+ } else {
+ None
+ };
+
+ // If URL parsing succeeded, check the extracted host
+ if let Some(ref host) = host_no_port {
+ if check_blocked_hostname(host) {
+ return true;
+ }
+ if let Ok(ip) = host.parse::() {
+ return is_blocked_ip(ip);
+ }
+ }
+
+ // Fallback: manual extraction for bare hostnames/IPs (no scheme)
+ let host_part = lower
+ .strip_prefix("http://")
+ .or_else(|| lower.strip_prefix("https://"))
+ .or_else(|| lower.strip_prefix("ftp://"))
+ .or_else(|| lower.strip_prefix("gopher://"))
+ .unwrap_or(&lower);
+ // Strip userinfo (everything before @)
+ let host_part = if let Some(at_pos) = host_part.find('@') {
+ &host_part[at_pos + 1..]
+ } else {
+ host_part
+ };
+ let host_part = host_part.split('/').next().unwrap_or(host_part);
+ let host_part = host_part.split('?').next().unwrap_or(host_part);
+ let host_part = host_part.split('#').next().unwrap_or(host_part);
+
+ // Strip port from host
+ let bare_host: &str = if host_part.starts_with('[') {
+ host_part
+ .trim_start_matches('[')
+ .split(']')
+ .next()
+ .unwrap_or(host_part)
+ } else if host_part.matches(':').count() == 1 {
+ host_part.split(':').next().unwrap_or(host_part)
+ } else {
+ host_part
+ };
+
+ let decoded = percent_decode_host(bare_host);
+
+ if check_blocked_hostname(&decoded) {
+ return true;
+ }
+ if let Ok(ip) = decoded.parse::() {
+ return is_blocked_ip(ip);
+ }
+ if let Ok(ip) = bare_host.parse::() {
+ return is_blocked_ip(ip);
+ }
+
+ // Block file:// scheme entirely
+ if lower.starts_with("file://") {
return true;
}
- // Strip brackets and port to extract a bare IP for parsing.
- // Handles: "169.254.169.254", "169.254.169.254:80", "[::ffff:169.254.169.254]",
- // "[::ffff:169.254.169.254]:80"
- let stripped = lower
- .trim_start_matches('[')
- .split(']')
- .next()
- .unwrap_or(&lower);
- // For IPv4 with port: "1.2.3.4:80" → "1.2.3.4"
- let bare = if stripped.matches(':').count() == 1 {
- stripped.split(':').next().unwrap_or(stripped)
- } else {
- stripped // IPv6 has multiple colons, don't strip
- };
-
- if let Ok(ip) = bare.parse::() {
- return is_blocked_ip(ip);
- }
- // Also try the bracket-stripped version directly (full IPv6)
- if let Ok(ip) = stripped.parse::() {
- return is_blocked_ip(ip);
- }
false
}
-/// Check an already-parsed IP against blocked cloud metadata / link-local ranges.
-fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
- let ipv4 = match ip {
- std::net::IpAddr::V4(v4) => Some(v4),
- std::net::IpAddr::V6(v6) => {
- let segs = v6.segments();
- // AWS EC2 IPv6 metadata: fd00:ec2::254
- if segs[0] == 0xfd00 && segs[1] == 0x0ec2 {
- return true;
+fn percent_decode_host(host: &str) -> String {
+ let mut result = String::with_capacity(host.len());
+ let bytes = host.as_bytes();
+ let mut i = 0;
+ while i < bytes.len() {
+ if bytes[i] == b'%' && i + 2 < bytes.len() {
+ if let (Some(hi), Some(lo)) = (
+ hex_val(bytes[i + 1]),
+ hex_val(bytes[i + 2]),
+ ) {
+ result.push((hi << 4 | lo) as char);
+ i += 3;
+ continue;
}
- // Check for IPv4-mapped IPv6 (::ffff:A.B.C.D)
- v6.to_ipv4_mapped()
}
- };
-
- if let Some(v4) = ipv4 {
- let o = v4.octets();
- // AWS / GCP / DigitalOcean metadata
- if o == [169, 254, 169, 254] { return true; }
- // Entire link-local range (169.254.0.0/16) — metadata canary
- if o[0] == 169 && o[1] == 254 { return true; }
- // Azure wireserver
- if o == [168, 63, 129, 16] { return true; }
- // Alibaba Cloud metadata
- if o == [100, 100, 100, 200] { return true; }
+ result.push(bytes[i] as char);
+ i += 1;
}
+ result
+}
+fn hex_val(b: u8) -> Option {
+ match b {
+ b'0'..=b'9' => Some(b - b'0'),
+ b'a'..=b'f' => Some(b - b'a' + 10),
+ b'A'..=b'F' => Some(b - b'A' + 10),
+ _ => None,
+ }
+}
+
+fn check_blocked_hostname(host: &str) -> bool {
+ const BLOCKED_HOST_SUFFIXES: &[&str] = &[
+ "metadata.google.internal",
+ "metadata.goog",
+ "metadata.internal",
+ "metadata.azure.com",
+ "metadata.oraclecloud.com",
+ ];
+ const BLOCKED_HOST_EXACT: &[&str] = &[
+ "metadata",
+ "instance-data",
+ ];
+ const BLOCKED_WILDCARD_SUFFIXES: &[&str] = &[
+ ".sslip.io",
+ ".nip.io",
+ ".xip.io",
+ ".traefik.me",
+ ".local.gd",
+ ];
+
+ for suffix in BLOCKED_HOST_SUFFIXES {
+ if host == *suffix || host.ends_with(&format!(".{}", suffix)) {
+ return true;
+ }
+ }
+ for exact in BLOCKED_HOST_EXACT {
+ if host == *exact {
+ return true;
+ }
+ }
+ for wildcard in BLOCKED_WILDCARD_SUFFIXES {
+ if host.ends_with(wildcard) {
+ return true;
+ }
+ }
false
}
-/// Check if exec input contains shell metacharacters that could enable injection.
-fn contains_shell_metacharacters(input: &str) -> bool {
+fn is_blocked_ip(ip: std::net::IpAddr) -> bool {
+ match ip {
+ std::net::IpAddr::V6(v6) => {
+ if v6.is_loopback() { return true; }
+ let segs = v6.segments();
+ if segs[0] == 0xfd00 && segs[1] == 0x0ec2 { return true; }
+ if segs[0] & 0xfe00 == 0xfc00 { return true; }
+ if segs[0] & 0xffc0 == 0xfe80 { return true; }
+ if let Some(v4) = v6.to_ipv4_mapped() {
+ return is_blocked_ipv4(v4);
+ }
+ false
+ }
+ std::net::IpAddr::V4(v4) => is_blocked_ipv4(v4),
+ }
+}
+
+fn is_blocked_ipv4(v4: std::net::Ipv4Addr) -> bool {
+ let o = v4.octets();
+ o == [0, 0, 0, 0]
+ || o[0] == 127
+ || o[0] == 10
+ || (o[0] == 172 && (o[1] & 0xf0) == 16)
+ || (o[0] == 192 && o[1] == 168)
+ || (o[0] == 169 && o[1] == 254)
+ || o == [168, 63, 129, 16]
+ || o == [100, 100, 100, 200]
+}
+
+pub(crate) async fn is_blocked_target_resolved(target: &str) -> bool {
+ const MASS_SCAN_KEYWORDS: &[&str] = &["random", "0.0.0.0", "0.0.0.0/0"];
+ if MASS_SCAN_KEYWORDS.contains(&target) {
+ return false;
+ }
+
+ if is_blocked_target(target) {
+ return true;
+ }
+ let lower = target.to_lowercase();
+ let host_part = lower
+ .strip_prefix("http://")
+ .or_else(|| lower.strip_prefix("https://"))
+ .unwrap_or(&lower);
+ let host_part = host_part.split('/').next().unwrap_or(host_part);
+ let lookup_addr = if host_part.contains(':') {
+ host_part.to_string()
+ } else {
+ format!("{}:80", host_part)
+ };
+ match tokio::time::timeout(
+ std::time::Duration::from_secs(5),
+ tokio::net::lookup_host(&lookup_addr),
+ ).await {
+ Ok(Ok(addrs)) => {
+ for addr in addrs {
+ if is_blocked_ip(addr.ip()) {
+ return true;
+ }
+ }
+ false
+ }
+ Ok(Err(_)) | Err(_) => true,
+ }
+}
+
+pub(crate) fn contains_shell_metacharacters(input: &str) -> bool {
input.chars().any(|c| matches!(c, '&' | '|' | ';' | '`' | '$' | '>' | '<' | '\n' | '\r' | '(' | ')' | '{' | '}'))
|| input.contains("$(")
|| input.contains("${")
}
-// Auth middleware removed — authentication is now via PQ handshake (SSH-style identity keys).
-// The PQ middleware in pq_middleware.rs handles decryption and verifies the session.
-// Unauthenticated requests without X-PQ-Session header are rejected by the PQ middleware.
-
-// ─── Endpoint Handlers ─────────────────────────────────────────────
-
-async fn health_check() -> Json {
- Json(ok_response("API is running", None))
-}
-
-/// GET /api/modules — list all modules (like CLI `modules`)
-async fn list_modules() -> Json {
- let modules = commands::discover_modules();
-
- // Group modules by category dynamically
- let mut by_category: std::collections::BTreeMap> = std::collections::BTreeMap::new();
- for module in &modules {
- let category = module.split('/').next().unwrap_or("other").to_string();
- by_category.entry(category).or_default().push(module.clone());
- }
-
- Json(ok_response(
- format!("{} modules available", modules.len()),
- Some(serde_json::json!({
- "total": modules.len(),
- "categories": commands::categories(),
- "modules": by_category,
- })),
- ))
-}
-
-/// GET /api/modules/enriched — list all modules with full metadata inline
-async fn list_modules_enriched() -> Json {
- let modules = commands::discover_modules();
-
- let mut by_category: std::collections::BTreeMap> =
- std::collections::BTreeMap::new();
-
- for module_path in &modules {
- let entry = build_enriched_entry(module_path);
- let category = module_path.split('/').next().unwrap_or("other").to_string();
- by_category.entry(category).or_default().push(entry);
- }
-
- Json(ok_response(
- format!("{} modules available (enriched)", modules.len()),
- Some(serde_json::json!({
- "total": modules.len(),
- "categories": commands::categories(),
- "modules": by_category,
- })),
- ))
-}
-
-/// Build a single enriched module entry with metadata, info, and capability flags.
-fn build_enriched_entry(module_path: &str) -> serde_json::Value {
- let category = module_path.split('/').next().unwrap_or("other");
- let short_name = module_path.rsplit('/').next().unwrap_or(module_path);
- let info = commands::module_info(module_path);
- let has_info = info.is_some();
- let has_check = commands::has_check(module_path);
-
- let info_data = info.and_then(|i| {
- serde_json::to_value(&i).ok()
- });
-
- serde_json::json!({
- "path": module_path,
- "short_name": short_name,
- "category": category,
- "has_info": has_info,
- "has_check": has_check,
- "info": info_data,
- })
-}
-
-/// GET /api/modules/search?q=keyword — search modules (like CLI `find`)
-async fn search_modules(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Json {
- let keyword = params.get("q").map(|s| s.as_str()).unwrap_or("");
-
- if keyword.is_empty() {
- return Json(err_response(
- "Missing search query parameter 'q'",
- "INVALID_INPUT",
- ));
- }
-
- let modules = commands::discover_modules();
- let kw = keyword.to_lowercase();
- let matches: Vec = modules
- .into_iter()
- .filter(|m| m.to_lowercase().contains(&kw))
- .collect();
-
- Json(ok_response(
- format!("Found {} modules matching '{}'", matches.len(), keyword),
- Some(serde_json::json!({ "modules": matches })),
- ))
-}
-
-/// GET /api/module/*path — check if module exists + return metadata
-/// Accepts any depth: /api/module/scanners/port_scanner or /api/module/creds/generic/ftp_anonymous
-async fn get_module_info(
- axum::extract::Path(module_path): axum::extract::Path,
-) -> Response {
- let module_path = module_path.trim_matches('/').to_string();
- let category = module_path.split('/').next().unwrap_or("").to_string();
- let name = module_path.split('/').last().unwrap_or("").to_string();
-
- if commands::discover_modules().contains(&module_path) {
- let info = commands::module_info(&module_path);
- let info_data = info.map(|i| serde_json::to_value(&i).ok()).flatten();
- (
- StatusCode::OK,
- Json(ok_response(
- "Module found",
- Some(serde_json::json!({
- "module": module_path,
- "category": category,
- "name": name,
- "exists": true,
- "info": info_data,
- })),
- )),
- )
- .into_response()
- } else {
- (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Module '{}' not found", module_path),
- "INVALID_MODULE",
- )),
- )
- .into_response()
- }
-}
-
-// ─── Global Options API ────────────────────────────────────────────
-
-/// GET /api/options — list all global options
-async fn get_options() -> Json {
- let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
- Json(ok_response(
- format!("{} global options", opts.len()),
- Some(serde_json::json!({ "options": opts })),
- ))
-}
-
-/// POST /api/options — set a global option
-async fn set_option(Json(payload): Json>) -> Json {
- let mut set_count = 0;
- for (key, value) in &payload {
- crate::global_options::GLOBAL_OPTIONS.set(key, value).await;
- set_count += 1;
- }
- Json(ok_response(format!("{} option(s) set", set_count), None))
-}
-
-/// DELETE /api/options — clear a specific option (via query param or body)
-async fn delete_option(Json(payload): Json>) -> Json {
- let mut removed = 0;
- for key in payload.keys() {
- if crate::global_options::GLOBAL_OPTIONS.unset(key).await {
- removed += 1;
- }
- }
- Json(ok_response(format!("{} option(s) removed", removed), None))
-}
-
-// ─── Credential Store API ──────────────────────────────────────────
-
-/// GET /api/creds — list all credentials
-async fn list_creds() -> Json {
- tracing::info!("API: credentials listed");
- let creds = crate::cred_store::CRED_STORE.list().await;
- Json(ok_response(
- format!("{} credentials", creds.len()),
- Some(serde_json::to_value(&creds).unwrap_or_default()),
- ))
-}
-
-/// POST /api/creds — add a credential
-async fn add_cred(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let port = payload.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
- let service = payload.get("service").and_then(|v| v.as_str()).unwrap_or("unknown");
- let username = payload.get("username").and_then(|v| v.as_str()).unwrap_or("");
- let secret = payload.get("secret").and_then(|v| v.as_str()).unwrap_or("");
- let cred_type_str = payload.get("cred_type").and_then(|v| v.as_str()).unwrap_or("password");
- let source = payload.get("source_module").and_then(|v| v.as_str()).unwrap_or("api");
-
- if host.is_empty() || username.is_empty() {
- return Json(err_response("host and username are required", "INVALID_INPUT"));
- }
- if port == 0 {
- return Json(err_response("port must be a valid non-zero port number (1-65535)", "INVALID_INPUT"));
- }
- if secret.is_empty() {
- return Json(err_response("secret is required", "INVALID_INPUT"));
- }
-
- let cred_type = match cred_type_str {
- "hash" => crate::cred_store::CredType::Hash,
- "key" => crate::cred_store::CredType::Key,
- "token" => crate::cred_store::CredType::Token,
- _ => crate::cred_store::CredType::Password,
- };
-
- let id = crate::cred_store::CRED_STORE.add(host, port, service, username, secret, cred_type, source).await;
- tracing::info!(host = %host, service = %service, "API: credential added");
- Json(ok_response("Credential added", Some(serde_json::json!({ "id": id }))))
-}
-
-/// DELETE /api/creds — delete a credential by ID
-async fn delete_cred(Json(payload): Json) -> Json {
- let id = payload.get("id").and_then(|v| v.as_str()).unwrap_or("");
- if crate::cred_store::CRED_STORE.delete(id).await {
- tracing::info!(id = %id, "API: credential deleted");
- Json(ok_response("Credential deleted", None))
- } else {
- Json(err_response("Credential not found", "NOT_FOUND"))
- }
-}
-
-/// POST /api/creds/clear — clear all credentials
-async fn clear_creds() -> Json {
- crate::cred_store::CRED_STORE.clear().await;
- tracing::info!("API: all credentials cleared");
- Json(ok_response("All credentials cleared", None))
-}
-
-// ─── Workspace API ─────────────────────────────────────────────────
-
-/// GET /api/hosts — list tracked hosts
-async fn list_hosts() -> Json {
- let hosts = crate::workspace::WORKSPACE.hosts().await;
- Json(ok_response(
- format!("{} hosts", hosts.len()),
- Some(serde_json::to_value(&hosts).unwrap_or_default()),
- ))
-}
-
-/// POST /api/hosts — add a host
-async fn add_host(Json(payload): Json) -> Json {
- let ip = payload.get("ip").and_then(|v| v.as_str()).unwrap_or("");
- if ip.is_empty() {
- return Json(err_response("ip is required", "INVALID_INPUT"));
- }
- // Validate IP format (BUG 36 fix)
- if ip.len() > 256 || ip.chars().any(|c| c.is_control()) {
- return Json(err_response("Invalid IP address format", "INVALID_INPUT"));
- }
- // Accept valid IPv4, IPv6, or hostnames (alphanumeric + dots + colons + hyphens)
- if !ip.chars().all(|c| c.is_alphanumeric() || matches!(c, '.' | ':' | '-' | '[' | ']')) {
- return Json(err_response("IP/hostname contains invalid characters", "INVALID_INPUT"));
- }
- let hostname = payload.get("hostname").and_then(|v| v.as_str());
- let os_guess = payload.get("os_guess").and_then(|v| v.as_str());
- crate::workspace::WORKSPACE.add_host(ip, hostname, os_guess).await;
- Json(ok_response("Host added", None))
-}
-
-/// GET /api/services — list tracked services
-async fn list_services() -> Json {
- let services = crate::workspace::WORKSPACE.services().await;
- Json(ok_response(
- format!("{} services", services.len()),
- Some(serde_json::to_value(&services).unwrap_or_default()),
- ))
-}
-
-/// POST /api/services — add a service
-async fn add_service(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let port = payload.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
- let protocol = payload.get("protocol").and_then(|v| v.as_str()).unwrap_or("tcp");
- let service_name = payload.get("service_name").and_then(|v| v.as_str()).unwrap_or("unknown");
- let version = payload.get("version").and_then(|v| v.as_str());
-
- if host.is_empty() || port == 0 {
- return Json(err_response("host and port are required", "INVALID_INPUT"));
- }
- crate::workspace::WORKSPACE.add_service(host, port, protocol, service_name, version).await;
- Json(ok_response("Service added", None))
-}
-
-/// GET /api/workspace — show current workspace info
-async fn get_workspace() -> Json {
- let name = crate::workspace::WORKSPACE.current_name().await;
- let workspaces = crate::workspace::WORKSPACE.list_workspaces().await;
- Json(ok_response("Workspace info", Some(serde_json::json!({
- "current": name,
- "available": workspaces,
- }))))
-}
-
-/// POST /api/workspace — switch workspace
-async fn switch_workspace(Json(payload): Json) -> Json {
- let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("");
- if name.is_empty() {
- return Json(err_response("name is required", "INVALID_INPUT"));
- }
- // Validate workspace name — same rules as shell.rs
- if name.len() > 64 {
- return Json(err_response("Workspace name too long (max 64 chars)", "INVALID_INPUT"));
- }
- if !name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {
- return Json(err_response("Workspace name must be alphanumeric, underscore, or hyphen only", "INVALID_INPUT"));
- }
- crate::workspace::switch_all(name).await;
- Json(ok_response(format!("Switched to workspace '{}'", name), None))
-}
-
-/// DELETE /api/hosts — delete a host by IP
-async fn delete_host(Json(payload): Json) -> Json {
- let ip = payload.get("ip").and_then(|v| v.as_str()).unwrap_or("");
- if ip.is_empty() {
- return Json(err_response("ip is required", "INVALID_INPUT"));
- }
- if crate::workspace::WORKSPACE.delete_host(ip).await {
- tracing::info!(ip = %ip, "API: host deleted");
- Json(ok_response(format!("Host '{}' and its services removed", ip), None))
- } else {
- Json(err_response(format!("Host '{}' not found", ip), "NOT_FOUND"))
- }
-}
-
-/// POST /api/hosts/clear — clear all hosts and services
-async fn clear_hosts() -> Json {
- crate::workspace::WORKSPACE.clear_hosts().await;
- tracing::info!("API: all hosts and services cleared");
- Json(ok_response("All hosts and services cleared", None))
-}
-
-/// DELETE /api/services — delete a service by host and port
-async fn delete_service(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let port = payload.get("port").and_then(|v| v.as_u64()).unwrap_or(0) as u16;
- if host.is_empty() || port == 0 {
- return Json(err_response("host and port (>0) are required", "INVALID_INPUT"));
- }
- if crate::workspace::WORKSPACE.delete_service(host, port).await {
- tracing::info!(host = %host, port = %port, "API: service deleted");
- Json(ok_response(format!("Service {}:{} removed", host, port), None))
- } else {
- Json(err_response(format!("Service {}:{} not found", host, port), "NOT_FOUND"))
- }
-}
-
-/// POST /api/hosts/notes — add a note to a host
-async fn add_host_note(Json(payload): Json) -> Json {
- let ip = payload.get("ip").and_then(|v| v.as_str()).unwrap_or("");
- let note = payload.get("note").and_then(|v| v.as_str()).unwrap_or("");
- if ip.is_empty() || note.is_empty() {
- return Json(err_response("ip and note are required", "INVALID_INPUT"));
- }
- if note.len() > 4096 {
- return Json(err_response("Note too long (max 4096 chars)", "INVALID_INPUT"));
- }
- if crate::workspace::WORKSPACE.add_note(ip, note).await {
- Json(ok_response(format!("Note added to host '{}'", ip), None))
- } else {
- Json(err_response(format!("Host '{}' not found", ip), "NOT_FOUND"))
- }
-}
-
-/// POST /api/run_all — run a module against all IPs in a CIDR subnet.
-/// No size cap — any CIDR works. Uses semaphore-bounded concurrency (default 50).
-/// Large subnets (>1M) are warned but not blocked.
-async fn run_all(Json(payload): Json) -> Json {
- let module = payload.get("module").and_then(|v| v.as_str()).unwrap_or("");
- let target = payload.get("target").and_then(|v| v.as_str()).unwrap_or("");
- let verbose = payload.get("verbose").and_then(|v| v.as_bool()).unwrap_or(false);
- let concurrency = payload.get("concurrency").and_then(|v| v.as_u64()).unwrap_or(50) as usize;
- let concurrency = concurrency.clamp(1, 500);
-
- if module.is_empty() || target.is_empty() {
- return Json(err_response("module and target (CIDR subnet) are required", "INVALID_INPUT"));
- }
- if !validate_module_name(module) {
- return Json(err_response("Invalid module name", "INVALID_INPUT"));
- }
- let network: ipnetwork::IpNetwork = match target.parse() {
- Ok(n) => n,
- Err(_) => return Json(err_response("target must be a valid CIDR subnet (e.g., 192.168.1.0/24)", "INVALID_INPUT")),
- };
- // Reject excessively large subnets to prevent DoS (OOM / CPU exhaustion)
- match &network {
- ipnetwork::IpNetwork::V4(n) if n.prefix() < 16 => {
- return Json(err_response(
- "IPv4 CIDR prefix must be /16 or more specific (max 65536 hosts)",
- "CIDR_TOO_LARGE",
- ));
- }
- ipnetwork::IpNetwork::V6(n) if n.prefix() < 48 => {
- return Json(err_response(
- "IPv6 CIDR prefix must be /48 or more specific",
- "CIDR_TOO_LARGE",
- ));
- }
- _ => {}
- }
- let host_count = match network {
- ipnetwork::IpNetwork::V4(n) => 2u64.saturating_pow(32 - n.prefix() as u32),
- ipnetwork::IpNetwork::V6(n) => {
- if n.prefix() >= 64 { 2u64.saturating_pow(128 - n.prefix() as u32) } else { u64::MAX }
- }
- };
-
- // Semaphore-bounded concurrent execution — lazy iteration, no memory cap
- let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(concurrency));
- let success = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
- let failed = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
-
- let module_str = module.to_string();
- let skipped = std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0));
- for ip in network.iter() {
- let ip_str = ip.to_string();
-
- // SSRF guard: check each individual IP against blocked targets
- if is_blocked_target(&ip_str) {
- skipped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
- continue;
- }
-
- let permit = match semaphore.clone().acquire_owned().await {
- Ok(p) => p,
- Err(_) => break,
- };
- let sc = success.clone();
- let fc = failed.clone();
- let mod_name = module_str.clone();
-
- tokio::spawn(async move {
- match crate::commands::run_module(&mod_name, &ip_str, verbose).await {
- Ok(_) => { sc.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
- Err(_) => { fc.fetch_add(1, std::sync::atomic::Ordering::Relaxed); }
- }
- drop(permit);
- });
- }
-
- // Wait for all tasks by draining semaphore permits
- for _ in 0..concurrency {
- if let Err(e) = semaphore.acquire().await { crate::meprintln!("[!] Semaphore error: {}", e); }
- }
-
- let s = success.load(std::sync::atomic::Ordering::Relaxed);
- let f = failed.load(std::sync::atomic::Ordering::Relaxed);
- let sk = skipped.load(std::sync::atomic::Ordering::Relaxed);
- Json(ok_response(
- format!("run_all complete: {} success, {} failed, {} skipped (blocked) out of {} IPs", s, f, sk, s + f + sk),
- Some(serde_json::json!({
- "module": module,
- "target": target,
- "host_count": host_count,
- "concurrency": concurrency,
- "success": s,
- "failed": f,
- "skipped_blocked": sk,
- })),
- ))
-}
-
-/// POST /api/check — run a non-destructive vulnerability check
-async fn check_module(Json(payload): Json) -> Json {
- let module = payload.get("module").and_then(|v| v.as_str()).unwrap_or("");
- let target = payload.get("target").and_then(|v| v.as_str()).unwrap_or("");
- if module.is_empty() || target.is_empty() {
- return Json(err_response("module and target are required", "INVALID_INPUT"));
- }
- if !validate_module_name(module) {
- return Json(err_response("Invalid module name", "INVALID_INPUT"));
- }
- match crate::commands::check_module(module, target).await {
- Some(result) => {
- let result_str = format!("{}", result);
- Json(ok_response("Check complete", Some(serde_json::json!({
- "module": module,
- "target": target,
- "result": result_str,
- }))))
- }
- None => Json(err_response(format!("Module '{}' does not support check()", module), "NOT_SUPPORTED")),
- }
-}
-
-/// GET /api/creds/search?q= — search credentials
-async fn search_creds(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Json {
- let query = params.get("q").map(|s| s.as_str()).unwrap_or("");
- if query.is_empty() {
- return Json(err_response("Query parameter 'q' is required", "INVALID_INPUT"));
- }
- let results = crate::cred_store::CRED_STORE.search(query).await;
- Json(ok_response(
- format!("{} credentials matched", results.len()),
- Some(serde_json::to_value(&results).unwrap_or_default()),
- ))
-}
-
-/// GET /api/loot/search?q= — search loot
-async fn search_loot(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Json {
- let query = params.get("q").map(|s| s.as_str()).unwrap_or("");
- if query.is_empty() {
- return Json(err_response("Query parameter 'q' is required", "INVALID_INPUT"));
- }
- let results = crate::loot::LOOT_STORE.search(query).await;
- Json(ok_response(
- format!("{} loot items matched", results.len()),
- Some(serde_json::to_value(&results).unwrap_or_default()),
- ))
-}
-
-/// DELETE /api/loot — delete a loot entry by ID
-async fn delete_loot(Json(payload): Json) -> Json {
- let id = payload.get("id").and_then(|v| v.as_str()).unwrap_or("");
- if id.is_empty() {
- return Json(err_response("id is required", "INVALID_INPUT"));
- }
- if crate::loot::LOOT_STORE.delete(id).await {
- tracing::info!(id = %id, "API: loot deleted");
- Json(ok_response("Loot deleted", None))
- } else {
- Json(err_response("Loot not found", "NOT_FOUND"))
- }
-}
-
-/// POST /api/loot/clear — clear all loot entries
-async fn clear_loot() -> Json {
- crate::loot::LOOT_STORE.clear().await;
- tracing::info!("API: all loot cleared");
- Json(ok_response("All loot cleared", None))
-}
-
-// ─── Spool API ────────────────────────────────────────────────────
-
-/// GET /api/spool — get spool status
-async fn get_spool() -> Json {
- let active = crate::spool::SPOOL.is_active();
- let file = crate::spool::SPOOL.current_file();
- Json(ok_response(
- if active { "Spool active" } else { "Spool inactive" },
- Some(serde_json::json!({
- "active": active,
- "file": file,
- })),
- ))
-}
-
-/// POST /api/spool — start or stop spool
-/// Body: {"action": "start", "filename": "console.log"} or {"action": "stop"}
-async fn spool_action(Json(payload): Json) -> Json {
- let action = payload.get("action").and_then(|v| v.as_str()).unwrap_or("");
- match action {
- "start" => {
- let filename = payload.get("filename").and_then(|v| v.as_str()).unwrap_or("");
- if filename.is_empty() {
- return Json(err_response("filename is required for start action", "INVALID_INPUT"));
- }
- if filename.len() > 255 {
- return Json(err_response("Filename too long (max 255 chars)", "INVALID_INPUT"));
- }
- match crate::spool::SPOOL.start(filename) {
- Ok(()) => {
- tracing::info!(filename = %filename, "API: spool started");
- Json(ok_response(
- format!("Spooling output to '{}'", filename),
- Some(serde_json::json!({ "active": true, "file": filename })),
- ))
- }
- Err(e) => Json(err_response(format!("Failed to start spool: {}", e), "SPOOL_ERROR")),
- }
- }
- "stop" => {
- if let Some(name) = crate::spool::SPOOL.stop() {
- tracing::info!(file = %name, "API: spool stopped");
- Json(ok_response(
- format!("Spool stopped. Output saved to '{}'", name),
- Some(serde_json::json!({ "active": false, "file": name })),
- ))
- } else {
- Json(ok_response("Spool was not active", Some(serde_json::json!({ "active": false }))))
- }
- }
- _ => Json(err_response("action must be 'start' or 'stop'", "INVALID_INPUT")),
- }
-}
-
-// ─── Loot API ──────────────────────────────────────────────────────
-
-/// GET /api/loot — list loot entries
-async fn list_loot() -> Json {
- let loot = crate::loot::LOOT_STORE.list().await;
- Json(ok_response(
- format!("{} loot items", loot.len()),
- Some(serde_json::to_value(&loot).unwrap_or_default()),
- ))
-}
-
-/// POST /api/loot — add loot
-async fn add_loot(Json(payload): Json) -> Json {
- let host = payload.get("host").and_then(|v| v.as_str()).unwrap_or("");
- let loot_type = payload.get("loot_type").and_then(|v| v.as_str()).unwrap_or("other");
- let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("");
- let data = payload.get("data").and_then(|v| v.as_str()).unwrap_or("");
- let source = payload.get("source_module").and_then(|v| v.as_str()).unwrap_or("api");
-
- if host.is_empty() {
- return Json(err_response("host is required", "INVALID_INPUT"));
- }
-
- match crate::loot::LOOT_STORE.add_text(host, loot_type, description, data, source).await {
- Some(id) => Json(ok_response("Loot stored", Some(serde_json::json!({ "id": id })))),
- None => Json(err_response("Failed to store loot", "INTERNAL_ERROR")),
- }
-}
-
-// ─── Jobs API ──────────────────────────────────────────────────────
-
-/// GET /api/jobs — list background jobs with limit metadata
-async fn list_jobs() -> Json {
- let jobs = crate::jobs::JOB_MANAGER.list();
- let job_data: Vec = jobs.iter().map(|(id, module, target, started, status)| {
- serde_json::json!({
- "id": id,
- "module": module,
- "target": target,
- "started": started,
- "status": status,
- })
- }).collect();
- Json(ok_response(
- format!("{} jobs", job_data.len()),
- Some(serde_json::json!({
- "jobs": job_data,
- "running_count": crate::jobs::JOB_MANAGER.running_count(),
- "max_running": crate::jobs::JOB_MANAGER.get_max_running(),
- })),
- ))
-}
-
-/// POST /api/jobs/limit — set the max concurrent running jobs (1-100)
-async fn set_job_limit(Json(payload): Json) -> Json {
- let limit = payload.get("limit").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
- if limit < 1 || limit > 100 {
- return Json(err_response("Limit must be between 1 and 100", "INVALID_INPUT"));
- }
- crate::jobs::JOB_MANAGER.set_max_running(limit);
- Json(ok_response(
- format!("Job limit set to {}", limit),
- Some(serde_json::json!({
- "max_running": limit,
- "running_count": crate::jobs::JOB_MANAGER.running_count(),
- })),
- ))
-}
-
-/// GET /api/jobs/{id} — get single job status
-async fn get_job(axum::extract::Path(id): axum::extract::Path) -> Json {
- let jobs = crate::jobs::JOB_MANAGER.list();
- if let Some((_, module, target, started, status)) = jobs.iter().find(|(jid, _, _, _, _)| *jid == id) {
- Json(ok_response(
- format!("Job {} status: {}", id, status),
- Some(serde_json::json!({
- "id": id,
- "module": module,
- "target": target,
- "started": started,
- "status": status,
- })),
- ))
- } else {
- Json(err_response(format!("Job {} not found", id), "NOT_FOUND"))
- }
-}
-
-/// DELETE /api/jobs/{id} — kill a job
-async fn kill_job_by_id(axum::extract::Path(id): axum::extract::Path) -> Json {
- if crate::jobs::JOB_MANAGER.kill(id) {
- Json(ok_response(format!("Job {} cancelled", id), None))
- } else {
- Json(err_response(format!("Job {} not found", id), "NOT_FOUND"))
- }
-}
-
-// ─── Export API ────────────────────────────────────────────────────
-
-/// GET /api/export — export engagement data
-async fn export_data(
- axum::extract::Query(params): axum::extract::Query>,
-) -> Response {
- let format = params.get("format").map(|s| s.as_str()).unwrap_or("json");
-
- // Gather data inline for API response
- let loot_entries = crate::loot::LOOT_STORE.list().await;
- let data = serde_json::json!({
- "workspace": crate::workspace::WORKSPACE.current_name().await,
- "exported_at": chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
- "hosts": crate::workspace::WORKSPACE.hosts().await,
- "services": crate::workspace::WORKSPACE.services().await,
- "credentials": crate::cred_store::CRED_STORE.list().await,
- "loot": loot_entries,
- });
-
- match format {
- "json" => (
- StatusCode::OK,
- Json(ok_response("Export complete", Some(data))),
- ).into_response(),
- "csv" => {
- match crate::export::export_csv_string().await {
- Ok(csv) => (
- StatusCode::OK,
- Json(ok_response("CSV export complete", Some(serde_json::json!({ "content": csv, "format": "csv" })))),
- ).into_response(),
- Err(e) => (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(err_response(format!("CSV export failed: {}", e), "EXPORT_ERROR")),
- ).into_response(),
- }
- }
- "summary" => {
- match crate::export::export_summary_string().await {
- Ok(summary) => (
- StatusCode::OK,
- Json(ok_response("Summary export complete", Some(serde_json::json!({ "content": summary, "format": "summary" })))),
- ).into_response(),
- Err(e) => (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(err_response(format!("Summary export failed: {}", e), "EXPORT_ERROR")),
- ).into_response(),
- }
- }
- _ => (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid format. Use format=json, format=csv, or format=summary", "INVALID_INPUT")),
- ).into_response(),
- }
-}
-
-/// GET /api/target — show current target (like CLI `show_target`)
-async fn get_target() -> Json {
- let target = crate::config::GLOBAL_CONFIG.get_target();
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
-
- Json(ok_response(
- if target.is_some() {
- "Target retrieved"
- } else {
- "No target set"
- },
- Some(serde_json::json!({
- "target": target,
- "size": size,
- "is_subnet": is_subnet,
- })),
- ))
-}
-
-/// POST /api/target — set target (like CLI `set target `)
-async fn set_target(Json(payload): Json) -> Response {
- let target_raw = payload.target.as_str();
-
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
-
- match crate::config::GLOBAL_CONFIG.set_target(target_raw) {
- Ok(_) => {
- let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- (
- StatusCode::OK,
- Json(ok_response(
- format!("Target set to '{}'", target_raw),
- Some(serde_json::json!({
- "target": target_raw,
- "is_subnet": is_subnet,
- "size": size,
- })),
- )),
- )
- .into_response()
- }
- Err(e) => (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- format!("Failed to set target: {}", e),
- "INVALID_TARGET",
- )),
- )
- .into_response(),
- }
-}
-
-/// DELETE /api/target — clear target (like CLI `clear_target`)
-async fn clear_target() -> Json {
- crate::config::GLOBAL_CONFIG.clear_target();
- Json(ok_response("Target cleared", None))
-}
-
-/// POST /api/run — run module synchronously (like CLI `run`)
-///
-/// This mirrors the CLI: calls `commands::run_module()` directly,
-/// captures stdout/stderr, and returns the output in the response.
-async fn run_module(
- State(state): State,
- Json(payload): Json,
-) -> Response {
- let start = std::time::Instant::now();
- let module_name = payload.module.as_str();
- let target_raw = payload.target.as_str();
-
- // Validate inputs
- if !validate_module_name(module_name) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid module name format",
- "INVALID_MODULE",
- )),
- )
- .into_response();
- }
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
- // SSRF guard — block cloud metadata endpoints
- if is_blocked_target(target_raw) {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response(
- "Target is blocked (cloud metadata / internal endpoint)",
- "BLOCKED_TARGET",
- )),
- )
- .into_response();
- }
- // Check module exists
- if !commands::discover_modules().contains(&module_name.to_string()) {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Module '{}' not found", module_name),
- "INVALID_MODULE",
- )),
- )
- .into_response();
- }
-
- // Validate output_file for path traversal before injecting into prompts
- if let Some(ref f) = payload.output_file {
- let bad = f.contains("..") || f.contains('/') || f.contains('\\') || f.contains('\0');
- if bad {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid output_file: must not contain path separators or traversal sequences",
- "INVALID_OUTPUT_FILE",
- )),
- ).into_response();
- }
- }
-
- // Set module config (like CLI does via prompts, the API passes them in the request)
- let mut module_config = crate::config::ModuleConfig {
- port: payload.port,
- username_wordlist: payload.username_wordlist.clone(),
- password_wordlist: payload.password_wordlist.clone(),
- concurrency: payload.concurrency,
- stop_on_success: payload.stop_on_success,
- save_results: payload.save_results,
- output_file: payload.output_file.clone(),
- verbose: payload.verbose,
- combo_mode: payload.combo_mode,
- custom_prompts: payload.prompts.clone().unwrap_or_default(),
- api_mode: true,
- };
-
- // Inject dedicated fields into custom_prompts so cfg_prompt_* picks them up.
- // Only insert if not already set by the explicit prompts map.
- if let Some(v) = module_config.port {
- module_config.custom_prompts.entry("port".into())
- .or_insert(v.to_string());
- }
- if let Some(ref v) = module_config.username_wordlist {
- module_config.custom_prompts.entry("username_wordlist".into())
- .or_insert(v.clone());
- }
- if let Some(ref v) = module_config.password_wordlist {
- module_config.custom_prompts.entry("password_wordlist".into())
- .or_insert(v.clone());
- }
- if let Some(v) = module_config.concurrency {
- module_config.custom_prompts.entry("concurrency".into())
- .or_insert(v.to_string());
- }
- if let Some(v) = module_config.save_results {
- module_config.custom_prompts.entry("save_results".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(v) = module_config.verbose {
- module_config.custom_prompts.entry("verbose".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(v) = module_config.stop_on_success {
- module_config.custom_prompts.entry("stop_on_success".into())
- .or_insert(if v { "y".into() } else { "n".into() });
- }
- if let Some(ref v) = module_config.combo_mode {
- module_config.custom_prompts.entry("combo_mode".into())
- .or_insert(v.clone());
- }
- if let Some(ref v) = module_config.output_file {
- module_config.custom_prompts.entry("output_file".into())
- .or_insert(v.clone());
- }
-
- // Strip "target" from custom_prompts to prevent SSRF bypass via prompt injection.
- // The validated target is passed directly to run_module() — modules should NOT
- // read a different target from custom_prompts.
- module_config.custom_prompts.remove("target");
-
- let verbose = state.verbose || payload.verbose.unwrap_or(false);
-
- if state.verbose {
- tracing::info!(
- module = module_name,
- target = target_raw,
- verbose,
- "Running module via API"
- );
- }
-
- tracing::info!(module = %module_name, target = %target_raw, "API: dispatching module");
-
- // Background job mode: spawn via JOB_MANAGER and return job_id immediately
- if payload.background.unwrap_or(false) {
- match crate::jobs::JOB_MANAGER.spawn(
- module_name.to_string(), target_raw.to_string(), verbose,
- ) {
- Ok(job_id) => {
- return (
- StatusCode::OK,
- Json(ok_response(
- format!("Job {} started: {} against {}", job_id, module_name, target_raw),
- Some(serde_json::json!({
- "job_id": job_id,
- "module": module_name,
- "target": target_raw,
- "status": "running",
- "running_jobs": crate::jobs::JOB_MANAGER.running_count(),
- "max_jobs": crate::jobs::JOB_MANAGER.get_max_running(),
- })),
- )),
- ).into_response();
- }
- Err(e) => {
- return (
- StatusCode::TOO_MANY_REQUESTS,
- Json(err_response(e, "JOB_LIMIT_REACHED")),
- ).into_response();
- }
- }
- }
-
- // Acquire concurrency permit to avoid resource exhaustion
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- return (
- StatusCode::SERVICE_UNAVAILABLE,
- Json(err_response(
- "Server is shutting down",
- "SERVICE_UNAVAILABLE",
- )),
- )
- .into_response();
- }
- };
-
- // Per-task output capture — no process-global gag, no serialization needed.
- // Each API request gets its own OutputBuffer via task-local storage,
- // so multiple modules can run concurrently without output interleaving.
- let output_buf = crate::output::OutputBuffer::new();
- let buf_clone = output_buf.clone();
-
- // Run inside a task-local RunContext so cfg_prompt_* reads per-request
- // config instead of the process-global MODULE_CONFIG.
- let (result, run_ctx) = crate::context::run_with_context_target(module_config, target_raw.to_string(), || async {
- crate::output::OUTPUT_BUFFER.scope(buf_clone, async {
- commands::run_module(module_name, target_raw, verbose).await
- }).await
- }).await;
-
- let duration_ms = start.elapsed().as_millis().min(u64::MAX as u128) as u64;
- let stdout_output = output_buf.drain_stdout();
- let stderr_output = output_buf.drain_stderr();
- let module_output = run_ctx.output.take();
-
- // Truncate output to prevent huge responses (max 64KB)
- let max_output = 64 * 1024;
- let output_truncated = if stdout_output.len() > max_output {
- format!("{}\n... (output truncated at {} bytes)", &stdout_output[..max_output], stdout_output.len())
- } else {
- stdout_output
- };
-
- match result {
- Ok(_) => (
- StatusCode::OK,
- Json(ok_response(
- format!(
- "Module '{}' executed successfully against '{}'",
- module_name, target_raw
- ),
- Some(serde_json::json!({
- "module": module_name,
- "target": target_raw,
- "status": "completed",
- "duration_ms": duration_ms,
- "output": output_truncated,
- "stderr": stderr_output,
- "findings": module_output.findings,
- })),
- )),
- )
- .into_response(),
- Err(e) => (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(err_response(
- format!(
- "Module '{}' failed on '{}': {}",
- module_name, target_raw, e
- ),
- "EXECUTION_ERROR",
- )),
- )
- .into_response(),
- }
-}
-
-// ─── Results File Retrieval ─────────────────────────────────────────
-
-/// Validate a result filename: ASCII-only, no path components, safe characters, .txt only.
-fn validate_result_filename(name: &str) -> bool {
+pub(crate) fn validate_result_filename(name: &str) -> bool {
!name.is_empty()
&& name.len() <= 255
&& name.is_ascii()
@@ -1316,2171 +252,89 @@ fn validate_result_filename(name: &str) -> bool {
&& name.ends_with(".txt")
}
-/// GET /api/results — list saved .txt result files in the results directory
-/// Only regular .txt files are listed; symlinks, hidden files, and non-.txt files are excluded.
-async fn list_results() -> Json {
- let results_dir = crate::config::results_dir();
- let mut files: Vec = Vec::new();
+// ─── Health Endpoint ────────────────────────────────────────────────
- if let Ok(entries) = std::fs::read_dir(&results_dir) {
- for entry in entries.flatten() {
- // Use symlink_metadata to detect symlinks (metadata() follows symlinks)
- let symlink_meta = match entry.path().symlink_metadata() {
- Ok(m) => m,
- Err(_) => continue,
- };
- // Skip symlinks entirely — prevent symlink-based path escapes
- if symlink_meta.file_type().is_symlink() {
- continue;
- }
- if let Ok(meta) = entry.metadata() {
- if meta.is_file() {
- let name = entry.file_name().to_string_lossy().to_string();
- // Only list .txt files, ASCII names, no hidden files
- if name.is_ascii()
- && name.ends_with(".txt")
- && !name.starts_with('.')
- {
- files.push(serde_json::json!({
- "name": name,
- "size_bytes": meta.len(),
- "modified": meta.modified().ok()
- .and_then(|t| t.duration_since(std::time::SystemTime::UNIX_EPOCH).ok())
- .map(|d| d.as_secs()),
- }));
- }
- }
- }
- }
- }
-
- files.sort_by(|a, b| {
- let a_name = a["name"].as_str().unwrap_or("");
- let b_name = b["name"].as_str().unwrap_or("");
- a_name.cmp(b_name)
- });
-
- Json(ok_response(
- format!("{} result file(s) found", files.len()),
- Some(serde_json::json!({
- "files": files,
- })),
- ))
-}
-
-/// GET /api/results/{filename} — retrieve a specific saved .txt result file
-/// Filename must be ASCII-only, end with .txt, no path separators, safe characters only.
-/// Returns the file content as plain text within a JSON envelope.
-async fn get_result_file(
- axum::extract::Path(filename): axum::extract::Path,
-) -> Response {
- // validate_result_filename enforces .txt extension, ASCII-only, no path separators, etc.
- if !validate_result_filename(&filename) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Invalid filename. Must be ASCII-only, end with .txt, no path separators, only alphanumeric/underscore/dash/dot.",
- "INVALID_FILENAME",
- )),
- )
- .into_response();
- }
-
- let results_dir = crate::config::results_dir();
- let file_path = results_dir.join(&filename);
-
- // Canonicalize the results_dir once for the containment check.
- let results_canonical = match results_dir.canonicalize() {
- Ok(c) => c,
- Err(_) => {
- return (
- StatusCode::INTERNAL_SERVER_ERROR,
- Json(err_response("Results directory not accessible", "INTERNAL_ERROR")),
- )
- .into_response();
- }
- };
-
- // Open the file atomically with O_NOFOLLOW to prevent TOCTOU symlink swaps.
- // All subsequent checks (path containment, regular-file) use the open fd's metadata,
- // so no race window exists between validation and reading.
- #[cfg(unix)]
- let open_result = {
- use std::os::unix::fs::OpenOptionsExt;
- std::fs::OpenOptions::new()
- .read(true)
- .custom_flags(libc::O_NOFOLLOW)
- .open(&file_path)
- };
- #[cfg(not(unix))]
- let open_result = std::fs::File::open(&file_path);
-
- let file = match open_result {
- Ok(f) => f,
- Err(e) => {
- // ELOOP (too many symlinks) means it was a symlink — O_NOFOLLOW rejected it
- let is_symlink_err = e.raw_os_error() == Some(libc::ELOOP);
- if is_symlink_err {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response("Access denied: symlinks are not allowed", "SYMLINK_DENIED")),
- )
- .into_response();
- }
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- };
-
- // Use /proc/self/fd/N to get the true canonical path of the *opened* file descriptor,
- // preventing any further TOCTOU race on the pathname.
- #[cfg(unix)]
- let canonical = {
- use std::os::unix::io::AsRawFd;
- let fd_path = format!("/proc/self/fd/{}", file.as_raw_fd());
- match std::fs::read_link(&fd_path) {
- Ok(p) => p,
- Err(_) => {
- // Fallback: canonicalize the path (still safe since we already hold the fd)
- match file_path.canonicalize() {
- Ok(c) => c,
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- }
- }
- }
- };
- #[cfg(not(unix))]
- let canonical = match file_path.canonicalize() {
- Ok(c) => c,
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- };
-
- // Path containment check on the fd-resolved path
- if !canonical.starts_with(&results_canonical) {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response("Access denied: path escapes results directory", "PATH_TRAVERSAL")),
- )
- .into_response();
- }
-
- // Verify it's a regular file via the open fd's metadata (no TOCTOU)
- match file.metadata() {
- Ok(meta) if !meta.is_file() => {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Requested path is not a regular file", "NOT_A_FILE")),
- )
- .into_response();
- }
- Err(_) => {
- return (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Result file '{}' not found", filename),
- "NOT_FOUND",
- )),
- )
- .into_response();
- }
- _ => {}
- }
-
- // Read from the already-open fd — no pathname re-resolution.
- // Use take() to cap IO at 1MB+1 so we never buffer more than that.
- let max_size: u64 = 1024 * 1024;
- let total_size = file.metadata().map(|m| m.len()).unwrap_or(0);
- let mut content = String::new();
- let mut limited = std::io::Read::take(&file, max_size + 1);
- match std::io::Read::read_to_string(&mut limited, &mut content) {
- Ok(bytes_read) => {
- let size_bytes = if total_size > 0 { total_size as usize } else { bytes_read };
- let content = if bytes_read as u64 > max_size {
- content.truncate(max_size as usize);
- format!("{}\n... (truncated at {} bytes, total: {} bytes)",
- content, max_size, total_size)
- } else {
- content
- };
-
- (
- StatusCode::OK,
- Json(ok_response(
- format!("File '{}' retrieved", filename),
- Some(serde_json::json!({
- "filename": filename,
- "content": content,
- "size_bytes": size_bytes,
- })),
- )),
- )
- .into_response()
- }
- Err(e) => (
- StatusCode::NOT_FOUND,
- Json(err_response(
- format!("Could not read file '{}': {}", filename, e),
- "READ_ERROR",
- )),
- )
- .into_response(),
- }
-}
-
-/// POST /api/honeypot-check — honeypot port scan (like CLI pre-run check)
-async fn honeypot_check(Json(payload): Json) -> Response {
- let target_raw = payload.target.as_str();
-
- if !validate_target(target_raw) {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Invalid target format", "INVALID_TARGET")),
- )
- .into_response();
- }
- // SSRF guard — block cloud metadata endpoints from port scanning
- if is_blocked_target(target_raw) {
- return (
- StatusCode::FORBIDDEN,
- Json(err_response(
- "Target is blocked (cloud metadata / internal endpoint)",
- "BLOCKED_TARGET",
- )),
- )
- .into_response();
- }
-
- let ip = match crate::utils::extract_ip_from_target(target_raw) {
- Some(ip) => ip,
- None => {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response(
- "Could not extract IP from target",
- "INVALID_TARGET",
- )),
- )
- .into_response();
- }
- };
-
- const COMMON_PORTS: &[u16] = &[
- 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389,
- 5900, 8080, 8443, 8888, 9090, 1433, 1521, 5432, 6379, 11211, 27017, 161, 389, 636, 902,
- 1080, 1194, 1883, 5672, 8883, 9200, 15672, 25565, 27018, 28017, 50000, 50070, 61616,
- ];
-
- let scan_timeout = std::time::Duration::from_millis(500);
- let semaphore = Arc::new(tokio::sync::Semaphore::new(50));
- let mut tasks = Vec::new();
-
- for &port in COMMON_PORTS {
- let ip = ip.clone();
- let sem = semaphore.clone();
- tasks.push(tokio::spawn(async move {
- let _permit = match sem.acquire().await {
- Ok(permit) => permit,
- Err(e) => {
- eprintln!("[!] Semaphore acquire failed: {}", e);
- return None;
- }
- };
- let addr = format!("{}:{}", ip, port);
- let conn = crate::utils::network::tcp_connect(&addr, scan_timeout).await;
- if let Ok(_) = conn {
- Some(port)
- } else {
- None
- }
- }));
- }
-
- let mut open_ports: Vec = Vec::new();
- for task in tasks {
- if let Ok(Some(port)) = task.await {
- open_ports.push(port);
- }
- }
- open_ports.sort();
-
- let is_honeypot = open_ports.len() >= 11;
-
- (
- StatusCode::OK,
- Json(ok_response(
- format!("Honeypot check completed for {}", ip),
- Some(serde_json::json!({
- "target": ip,
- "open_ports": open_ports,
- "open_count": open_ports.len(),
- "total_scanned": COMMON_PORTS.len(),
- "is_honeypot": is_honeypot,
- "threshold": 11,
- })),
- )),
- )
- .into_response()
-}
-
-// ─── Shell-Parity Exec Endpoint ─────────────────────────────────────
-
-/// POST /api/shell — run interactive-shell commands remotely (mirrors the interactive `rsf>` shell)
-///
-/// Supports all interactive shell commands: `use`, `set target`, `set subnet`, `show_target`,
-/// `clear_target`, `run`, `run_all`, `find`, `modules`, `back`, `info`, `check`, `setg`, `unsetg`,
-/// `show_options`, `creds`, `hosts`, `services`, `notes`, `workspace`, `loot`, `export`, `jobs`, `spool`.
-///
-/// For direct module execution with prompts, prefer POST /api/run instead.
-/// Supports secure command chaining via JSON array `commands` field.
-/// Each command is individually validated — no shell metacharacters allowed.
-/// Interactive-prompt commands (creds add, services add, loot add) accept inline arguments.
-async fn shell_command(
- State(state): State,
- Json(payload): Json,
-) -> Response {
- // Build command list from either `commands` (array) or `command` (single string)
- let command_list: Vec = if let Some(cmds) = &payload.commands {
- if cmds.is_empty() || cmds.len() > 20 {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Commands array must have 1-20 entries", "INVALID_INPUT")),
- ).into_response();
- }
- cmds.clone()
- } else if let Some(cmd) = &payload.command {
- vec![cmd.clone()]
- } else {
- return (
- StatusCode::BAD_REQUEST,
- Json(err_response("Provide 'command' (string) or 'commands' (array)", "INVALID_INPUT")),
- ).into_response();
- };
-
- let mut results: Vec = Vec::new();
-
- for raw_cmd in &command_list {
- let trimmed = raw_cmd.trim().to_string();
- if trimmed.is_empty() || trimmed.len() > 4096 {
- results.push(ShellResult {
- command: trimmed,
- success: false,
- output: "Command is empty or too long (max 4096 chars)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- // Validate each individual command against shell metacharacters
- if contains_shell_metacharacters(&trimmed) {
- results.push(ShellResult {
- command: trimmed,
- success: false,
- output: "Command contains forbidden characters (& | ; ` $ > <). Use the 'commands' JSON array for chaining.".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let mut parts = trimmed.splitn(2, char::is_whitespace);
- let cmd = match parts.next() {
- Some(c) => c.to_lowercase(),
- None => continue,
- };
- let rest = parts.next().unwrap_or("").trim().to_string();
- let command_key = crate::shell::resolve_command(&cmd);
-
- let start = std::time::Instant::now();
-
- match command_key.as_str() {
- "help" => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Available commands (same as interactive shell):\n\
- \n\
- ── Navigation & Discovery ──\n\
- help | h | ? — This help\n\
- modules | ls | m — List all modules\n\
- find | f1 — Search modules by keyword\n\
- use | u — Select a module\n\
- info [path] | i — Show module metadata\n\
- back | b — Deselect current module\n\
- \n\
- ── Targeting ──\n\
- set target | t — Set target (single IP/hostname)\n\
- set subnet | sn — Set target to CIDR subnet\n\
- set port — Set global port\n\
- show_target | st — Show current target & module\n\
- clear_target | ct — Clear target\n\
- \n\
- ── Execution ──\n\
- run [target] — Run selected module\n\
- run -j — Run module as background job\n\
- run_all [target] — Run all modules against target\n\
- check | ch — Non-destructive vulnerability check\n\
- \n\
- ── Global Options ──\n\
- setg | sg — Set global option\n\
- unsetg | ug — Unset global option\n\
- show_options | so — Display all global options\n\
- \n\
- ── Data Management ──\n\
- creds — List credentials\n\
- creds add [type] — Add credential\n\
- creds search — Search credentials\n\
- creds delete — Delete credential\n\
- creds clear — Clear all credentials\n\
- hosts — List tracked hosts\n\
- hosts add — Add host\n\
- hosts delete — Remove host and its services\n\
- hosts clear — Clear all hosts and services\n\
- services — List tracked services\n\
- services add [ver] — Add service\n\
- services delete — Remove a service\n\
- notes — Add note to host\n\
- workspace [name] | ws — List or switch workspaces\n\
- loot — List loot\n\
- loot add — Add loot\n\
- loot search — Search loot\n\
- loot delete — Delete loot entry\n\
- loot clear — Clear all loot\n\
- \n\
- ── Automation & Export ──\n\
- export — Export engagement data\n\
- spool [off|file] — Control output logging\n\
- jobs | j — List background jobs\n\
- jobs -k — Kill a background job\n\
- jobs clean — Clean up finished jobs\n\
- exit — (no-op in API mode)"
- .to_string(),
- duration_ms: None,
- });
- }
-
- "modules" => {
- let modules = commands::discover_modules();
- let mut by_category: std::collections::BTreeMap<&str, Vec<&str>> = std::collections::BTreeMap::new();
- for m in &modules {
- let cat = m.split('/').next().unwrap_or("other");
- by_category.entry(cat).or_default().push(m.as_str());
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "total": modules.len(),
- "categories": commands::categories(),
- "modules": by_category,
- }).to_string(),
- duration_ms: None,
- });
- }
-
- "find" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: find ".to_string(),
- duration_ms: None,
- });
- } else {
- let query = rest.to_lowercase();
- let modules = commands::discover_modules();
- let matches: Vec<&String> = modules.iter()
- .filter(|m| m.to_lowercase().contains(&query))
- .collect();
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "query": query,
- "matches": matches,
- "count": matches.len(),
- }).to_string(),
- duration_ms: None,
- });
- }
- }
-
- "use" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: use ".to_string(),
- duration_ms: None,
- });
- } else {
- let sanitized = crate::shell::sanitize_module_path(&rest);
- match sanitized {
- Some(path) => {
- let modules = commands::discover_modules();
- let found = modules.iter().any(|m| m == &path)
- || modules.iter().any(|m| {
- m.rsplit_once('/').map(|(_, s)| s == path).unwrap_or(false)
- });
- if found {
- if let Ok(mut cm) = state.current_module.lock() {
- *cm = Some(path.clone());
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Module selected: {}", path),
- duration_ms: None,
- });
- } else {
- // Fuzzy suggestion
- let best = modules.iter()
- .map(|m| (m, strsim::levenshtein(&path, m)))
- .min_by_key(|(_, d)| *d);
- let suggestion = if let Some((s, d)) = best {
- if d < 5 { format!(" Did you mean: {}?", s) } else { String::new() }
- } else {
- String::new()
- };
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Unknown module '{}'.{}", path, suggestion),
- duration_ms: None,
- });
- }
- }
- None => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid module path".to_string(),
- duration_ms: None,
- });
- }
- }
- }
- }
-
- "set" => {
- // Handle "set port " and "set source_port " as global option shortcuts
- if let Some(val) = rest.strip_prefix("port ") {
- let val = val.trim();
- match val.parse::() {
- Ok(p) if p > 0 => {
- crate::global_options::GLOBAL_OPTIONS.set("port", val).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Global port set to: {}", val),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- }
- }
- } else if let Some(val) = rest.strip_prefix("source_port ") {
- let val = val.trim();
- if val == "0" || val.is_empty() {
- crate::global_options::GLOBAL_OPTIONS.unset("source_port").await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Source port cleared (will use OS-assigned).".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- match val.parse::() {
- Ok(p) if p > 0 => {
- crate::global_options::GLOBAL_OPTIONS.set("source_port", val).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Global source port set to: {}", val),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid source port. Must be 1-65535 (or 0 to clear).".to_string(),
- duration_ms: None,
- });
- }
- }
- }
- } else {
- // Mirror shell.rs: `set target `, `t `, `set subnet `, `sn `
- // Peel off leading "target ", "t ", "subnet ", "sn " keywords to extract the value
- let raw_value = if rest.starts_with("target ") {
- rest.strip_prefix("target ").unwrap_or(&rest).trim()
- } else if rest.starts_with("t ") {
- rest.strip_prefix("t ").unwrap_or(&rest).trim()
- } else {
- rest.trim()
- };
-
- if raw_value.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: set target or set subnet or t or sn ".to_string(),
- duration_ms: None,
- });
- } else if !validate_target(raw_value) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid target".to_string(),
- duration_ms: None,
- });
- } else {
- // Strip CIDR prefix if user did `set target 1.2.3.4/24` (single-IP intent)
- let ip_only = if raw_value.contains('/') {
- raw_value.split('/').next().unwrap_or(raw_value)
- } else {
- raw_value
- };
- match crate::config::GLOBAL_CONFIG.set_target(ip_only) {
- Ok(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Target set to: {}", ip_only),
- duration_ms: None,
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Failed to set target: {}", e),
- duration_ms: None,
- });
- }
- }
- }
- } // close else from port/source_port handling
- }
-
- "set_subnet" => {
- // Mirror shell.rs set_subnet: accepts `sn `, `subnet `, `set subnet `
- let raw_value = if rest.starts_with("subnet ") {
- rest.strip_prefix("subnet ").unwrap_or(&rest).trim()
- } else if rest.starts_with("sn ") {
- rest.strip_prefix("sn ").unwrap_or(&rest).trim()
- } else {
- rest.trim()
- };
-
- if raw_value.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: sn or set subnet (e.g. 192.168.1.0/24)".to_string(),
- duration_ms: None,
- });
- } else if !raw_value.contains('/') {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Not a subnet — use CIDR notation (e.g. 192.168.1.0/24). For single IPs use: set target ".to_string(),
- duration_ms: None,
- });
- } else if !validate_target(raw_value) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid CIDR target".to_string(),
- duration_ms: None,
- });
- } else if raw_value.parse::().is_err() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Invalid CIDR notation: {}", raw_value),
- duration_ms: None,
- });
- } else {
- match crate::config::GLOBAL_CONFIG.set_target(raw_value) {
- Ok(_) => {
- let size = crate::config::GLOBAL_CONFIG.get_target_size();
- let msg = match size {
- Some(s) => format!("Subnet set to: {} ({} IPs)", raw_value, s),
- None => format!("Subnet set to: {}", raw_value),
- };
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: msg,
- duration_ms: None,
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Failed to set subnet: {}", e),
- duration_ms: None,
- });
- }
- }
- }
- }
-
- "show_target" => {
- let target = crate::config::GLOBAL_CONFIG.get_target().unwrap_or_else(|| "Not set".to_string());
- let module = state.current_module.lock().ok().and_then(|cm| cm.clone()).unwrap_or_else(|| "None".to_string());
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "target": target,
- "current_module": module,
- }).to_string(),
- duration_ms: None,
- });
- }
-
- "clear_target" => {
- crate::config::GLOBAL_CONFIG.clear_target();
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Target cleared".to_string(),
- duration_ms: None,
- });
- }
-
- "back" => {
- if let Ok(mut cm) = state.current_module.lock() {
- *cm = None;
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Module deselected".to_string(),
- duration_ms: None,
- });
- }
-
- "run" => {
- let background = rest.trim() == "-j" || rest.trim() == "--job";
- let rest_for_target = if background { String::new() } else { rest.clone() };
-
- let module_path = state.current_module.lock().ok().and_then(|cm| cm.clone());
- if let Some(module_path) = module_path {
- // Resolve target: from rest arg, or global config
- let target = if !rest_for_target.is_empty() {
- rest_for_target.clone()
- } else if crate::config::GLOBAL_CONFIG.has_target() {
- crate::config::GLOBAL_CONFIG.get_target().unwrap_or_default()
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set. Use 'set ' first.".to_string(),
- duration_ms: None,
- });
- continue;
- };
-
- // SSRF guard — shell 'run' must also validate the resolved target
- if !validate_target(&target) || is_blocked_target(&target) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (cloud metadata / internal endpoint)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let verbose = state.verbose;
-
- if background {
- // Background job: spawn via JOB_MANAGER
- match crate::jobs::JOB_MANAGER.spawn(
- module_path.clone(), target.clone(), verbose,
- ) {
- Ok(job_id) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Job {} started: {} against {}", job_id, module_path, target),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: e,
- duration_ms: None,
- });
- }
- }
- } else {
- // Foreground execution
- let run_start = std::time::Instant::now();
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Server is shutting down".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- match commands::run_module(&module_path, &target, verbose).await {
- Ok(_) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Module '{}' executed against '{}'", module_path, target),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
- Err(e) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Module failed: {}", e),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'use ' first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- "run_all" => {
- let target = if !rest.is_empty() {
- rest.clone()
- } else if crate::config::GLOBAL_CONFIG.has_target() {
- crate::config::GLOBAL_CONFIG.get_target().unwrap_or_default()
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set for run_all".to_string(),
- duration_ms: None,
- });
- continue;
- };
-
- // SSRF guard — run_all must check the resolved target too
- if !validate_target(&target) || is_blocked_target(&target) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (cloud metadata / internal endpoint)".to_string(),
- duration_ms: None,
- });
- continue;
- }
-
- let modules = commands::discover_modules();
- let verbose = state.verbose;
- let run_start = std::time::Instant::now();
- let mut ok_count = 0usize;
- let mut fail_count = 0usize;
- for m in &modules {
- let _permit = match state.run_semaphore.acquire().await {
- Ok(permit) => permit,
- Err(_) => {
- fail_count += 1;
- continue;
- }
- };
- match commands::run_module(m, &target, verbose).await {
- Ok(_) => ok_count += 1,
- Err(_) => fail_count += 1,
- }
- }
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("run_all complete: {} ok, {} failed out of {} modules",
- ok_count, fail_count, modules.len()),
- duration_ms: Some(run_start.elapsed().as_millis() as u64),
- });
- }
-
- // ═══════════════════════════════════════════════
- // INFO — Module metadata
- // ═══════════════════════════════════════════════
- "info" => {
- let module_path = if !rest.is_empty() {
- Some(rest.clone())
- } else {
- state.current_module.lock().ok().and_then(|cm| cm.clone())
- };
- if let Some(ref path) = module_path {
- if let Some(info) = commands::module_info(path) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "module": path,
- "info": info,
- }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("No metadata available for '{}'. Modules can provide metadata by adding a pub fn info() -> ModuleInfo function.", path),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'info ' or select a module first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // CHECK — Non-destructive vulnerability check
- // ═══════════════════════════════════════════════
- "check" => {
- let module_path = state.current_module.lock().ok().and_then(|cm| cm.clone());
- if let Some(ref path) = module_path {
- let target = crate::config::GLOBAL_CONFIG.get_target();
- if let Some(ref t) = target {
- if !validate_target(t) || is_blocked_target(t) {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Target is invalid or blocked (internal/private address)".to_string(),
- duration_ms: None,
- });
- } else {
- match commands::check_module(path, t).await {
- Some(result) => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({
- "module": path,
- "target": t,
- "result": result,
- }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- None => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Module '{}' does not support the check method.", path),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No target set. Use 'set target ' first.".to_string(),
- duration_ms: None,
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "No module selected. Use 'use ' first.".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // GLOBAL OPTIONS
- // ═══════════════════════════════════════════════
- "setg" => {
- if let Some((key, value)) = rest.split_once(char::is_whitespace) {
- let key = key.trim();
- let value = value.trim();
- if key.is_empty() || value.is_empty() || key.len() > 256 || value.len() > 256 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: setg (max 256 chars each)".to_string(),
- duration_ms: None,
- });
- } else {
- crate::global_options::GLOBAL_OPTIONS.set(key, value).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("{} => {}", key, value),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: setg ".to_string(),
- duration_ms: None,
- });
- }
- }
-
- "unsetg" => {
- let key = rest.trim();
- if key.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: unsetg ".to_string(),
- duration_ms: None,
- });
- } else if crate::global_options::GLOBAL_OPTIONS.unset(key).await {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Unset global option '{}'", key),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: format!("Global option '{}' was not set.", key),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- }
-
- "show_options" => {
- let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "options": opts }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
-
- // ═══════════════════════════════════════════════
- // CREDENTIALS
- // ═══════════════════════════════════════════════
- "creds" => {
- if rest.is_empty() {
- let entries = crate::cred_store::CRED_STORE.list().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "credentials": entries }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(args) = rest.strip_prefix("add ") {
- // Parse: creds add [type]
- let parts: Vec<&str> = args.splitn(6, char::is_whitespace).collect();
- if parts.len() < 5 {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds add [type]".to_string(),
- duration_ms: None,
- });
- } else {
- let host = parts[0].trim();
- let port: u16 = match parts[1].trim().parse() {
- Ok(p) if p > 0 => p,
- _ => {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Invalid port number. Must be 1-65535.".to_string(),
- duration_ms: None,
- });
- continue;
- }
- };
- let service = parts[2].trim();
- let username = parts[3].trim();
- let (secret, cred_type) = if parts.len() >= 6 {
- (parts[4].trim(), match parts[5].trim() {
- "hash" => crate::cred_store::CredType::Hash,
- "key" => crate::cred_store::CredType::Key,
- "token" => crate::cred_store::CredType::Token,
- _ => crate::cred_store::CredType::Password,
- })
- } else {
- (parts[4].trim(), crate::cred_store::CredType::Password)
- };
- let id = crate::cred_store::CRED_STORE.add(host, port, service, username, secret, cred_type, "api-shell").await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: format!("Credential stored (ID: {})", id),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- }
- } else if let Some(query) = rest.strip_prefix("search ") {
- let found = crate::cred_store::CRED_STORE.search(query.trim()).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: serde_json::json!({ "results": found }).to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if let Some(id) = rest.strip_prefix("delete ") {
- let deleted = crate::cred_store::CRED_STORE.delete(id.trim()).await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: deleted,
- output: if deleted {
- format!("Credential '{}' deleted.", id.trim())
- } else {
- format!("Credential '{}' not found.", id.trim())
- },
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if rest == "clear" {
- crate::cred_store::CRED_STORE.clear().await;
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "All credentials cleared.".to_string(),
- duration_ms: Some(start.elapsed().as_millis() as u64),
- });
- } else if rest == "add" {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds add [type]".to_string(),
- duration_ms: None,
- });
- } else {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: false,
- output: "Usage: creds [add [type]|search |delete |clear]".to_string(),
- duration_ms: None,
- });
- }
- }
-
- // ═══════════════════════════════════════════════
- // SPOOL — Output logging
- // ═══════════════════════════════════════════════
- "spool" => {
- if rest.is_empty() {
- results.push(ShellResult {
- command: trimmed.to_string(),
- success: true,
- output: "Use 'spool